Thumbnail / Header for article: Wondering Witches (Part 1)

Wondering Witches (Part 1)

Welcome to the technical devlog of Wondering Witches!

In case you haven’t played it yet, visit Wondering Witches (Game Page).

Read the rules, gather some friends and play yourself a fun little game :)

What is the game about? It’s a cooperative, logical puzzling boardgame which only requires you to grab a single piece of (empty) paper. The game takes place on this paper, but there is an important digital component.

The idea of the game is that, through experimentation, growing the right ingredients, and logic deduction, you must discover the secret recipe!

What’s the digital component? When you go to the official page, you can press a button to generate a random starting setup. Scroll down and you can press another button to generate the actual puzzle.

This website, even though it’s really simple and only a few button clicks, contains a LOT of ideas and algorithms to make the game experience as smooth and exciting as possible.

In this article I’ll explain how I created this “online component” of the game. I’ll also share why I added this component and the things I learned along the way (whenever necessary).

Remark: I also wrote a more general devlog about the game design process. It’s rather long, but if you are interested in boardgames or game design, I’m sure you’ll find it interesting: Devlog Wondering Witches

Remark: of course, the explanations and code samples here are only a small simplified fraction of the full code. You can visit the website and check the source code to see exactly how it all works. (I apologize in advance for pieces of code that are messy, unoptimized or weirdly structured. I’m an “if it works, it works” kind of guy. I do, however, comment and space out everything neatly.)

The Two Elements

The website has two distinct elements: the board generation and the puzzle generation. (In fact, you can check the source code and see that these algorithms reside in two separate files.)

Board Generator: this designs a random layout (based on player count and difficulty) of cauldrons and gardens, adds some special cells and ingredients on top, and draws this to the screen.

Board Front
Board Front

Board Back
Board Back

Puzzle Generator: this generates a random puzzle and gives you an interface to test your solutions.

Interface for Entering Potions
Interface for Entering Potions

A “puzzle” in this game is a secret recipe. Some ingredients are good, some are bad. The good ingredients get a number, and you must put them in the correct order. The bad ingredients try their best to mess with your puzzle, by obscuring the results.

Testing a solution simply means that you input a set of ingredients, press “Use Potion”, and it gives you feedback about the results. For example, if two of the ingredients had a number that was too low, it reports “2 undergrown ingredients”.

Both parts were written in pure vanilla JavaScript. The Board Generation also used the Phaser framework for drawing the board. (I know the framework very well and saw no need to write custom graphics code.)

I’ll talk about the Puzzle Generator first.

Evaluating Potions

This is by far the simplest part of this system! Which is why I wanted to start here.

A potion is represented by a list of ingredients, an array if you wish.

The order matters a lot. To win the game, ingredients need to be in the correct order. (Ingredient with number 2 must come directly after number 1, for example.)

The computer simply loops through the list from start to finish, checking the numbers and executing any effects of each ingredient.

 1function usePotion() {
 2	var cSize = curCauldron.length;
 3
 4	// read ingredients from the interface
 5	// (elements on the page have ids "ingredientSeeds#", where # is the index of the element in the cauldron)
 6	// (perhaps not the cleanest way to do it, so don't focus too much on this piece of code)
 7	for(var i = 0; i < cSize; i++) {
 8		curCauldron[i].mySeeds = parseInt(document.getElementById('ingredientSeeds' + i).value);
 9	}
10
11	// now step through the list and determine whatever happens
12	var growingResult = '';
13	var totalEffectResult = [];
14
15	//
16	// EXECUTE EFFECTS (+ find UNDERGROWN ingredients)
17	// 
18	//  => we go through the cauldron from OLDEST/FIRST element to the newest; order is important
19	var numWrongIngredients = 0;
20	var numUndergrown = 0;
21	var numOvergrown = 0;
22	var elementsConsidered = 0;
23
24	for(var i = 0; i < cSize; i++) {
25		// if the number of seeds = -1, set it to whatever is necessary to make this ingredient report correctly
26		if(curCauldron[i].mySeeds == -1) {
27			curCauldron[i].mySeeds = curCauldron[i].myNum;
28		}
29
30		// check the ingredient!
31		var checkResult = checkIngredient(i);
32
33		var ing = curCauldron[i];
34		var skipEvaluation = false;
35
36		// if this ingredient is an IGNORE DECOY, well, ignore it
37		if(ing.decoyStatus == 0) {
38			skipEvaluation = true;
39		}
40
41		if(!skipEvaluation) {
42			elementsConsidered++;
43
44			// if it is undergrown, count that (and set ingredient to "wrong")
45			if(ing.mySeeds < ing.myNum) {
46			numUndergrown++;
47			checkResult.isWrong = true;
48
49			// if it is overgrown, also count that (and set ingredient to "wrong")
50			} else if(ing.mySeeds > ing.myNum) {
51			numOvergrown++;
52			checkResult.isWrong = true;
53			}
54
55			// if something is wrong with this ingredient, count that
56			if(checkResult.isWrong) {
57			numWrongIngredients++;
58			}
59		}
60
61		// add feedback to the effect feedback bar
62		// also flatten feedback (in case an ingredient has multiple effects; otherwise they stay in order)
63		for(var f = 0; f < checkResult.feedbackText.length; f++) {
64			totalEffectResult.push( checkResult.feedbackText[f] );
65		}
66
67		//
68		// if a DIRECT EFFECT must happen, do that
69		//
70		if(effs['Cutoff']) { break; } // cutoff immediately stops executing this potion
71		if(effs['Enthusiastic']) { i++; } // skip the next element in line		
72	}
73
74	// combine results from growing
75	growingResult = '<p>Potion had <strong>' + numUndergrown + ' undergrown</strong> and <strong>' + numOvergrown + ' overgrown</strong> ingredients.</p>';
76
77	// shuffle effects and combine them into string as well
78	totalEffectResult = shuffle(totalEffectResult);
79	totalEffectResult = totalEffectResult.join("");
80
81	//
82	// determine if the players WON
83	//  => if so, override all messages with a congratulatory one!
84	//
85	if(elementsConsidered >= codeLength && numWrongIngredients == 0) {
86		totalEffectResult = '<p class="winMessage">Congratulations! You won!</p>';
87	}
88
89	// display the results
90	document.getElementById('potionResult').style.display = 'block';
91	document.getElementById('potionResult').innerHTML = growingResult + totalEffectResult;
92
93	// clear the current cauldron
94	clearIngredients();
95}

Effects

Because of this linear structure, I needed to be very careful about what “effects” could do. Once an ingredient has been checked, it’s never checked again. Which means I cannot change anything in a potion that comes BEFORE my current ingredient. I can only change stuff that comes after it.

What does this mean? An effect can only …

  • Report the results from ingredients BEFORE it.

  • Alter the values of the ingredients AFTER it.

“Report” example: the Hugger effect reports whether the ingredient BEFORE itself has a value within a range of 1. (So if the Hugger has value 6, and the one before it has value 5, it will report yes.)

“Alter” example: the Spicy effect changes the value of the ingredient AFTER itself by 1. (So if the element after Spicy has a value of 3, it’s now changed to 4.)

By implementing the effects this way, I ensure that there are never mistakes or inconsistencies about the results.

But, why not change the loop? Why go through the potion linearly? Because of simplicity, both in code and in human deduction. This constraint made the game much easier to play and grasp, and the code much cleaner.

(That’s usually how it works: a creative constraint should be viewed as a good thing you can use to simplify your piece of art. Whatever that may be.)

Of course, there were some ideas I had to throw out, but it was a trade-off I could live with.

 1function checkIngredient(cauldronIndex) {
 2  // get ingredient and initialize empty object for it
 3	var ing = curCauldron[cauldronIndex];
 4	var obj = { 
 5		"isWrong": false, 
 6		"feedbackText": [], 
 7		"directEffects": {
 8			"Cutoff": false,
 9			"Enthusiastic": false,
10			"Fertilizer": false,
11			'Resetter': false,
12			'Coward': false,
13		} 
14	};
15
16	// if the number is greater than 0, check the ordering
17	// ordering is correct if two ingredients are in direct sequence (e.g. 4 => 5)
18	if(cauldronIndex > 0) {
19		obj.isWrong = !(curCauldron[(cauldronIndex-1)].myNum == (ing.myNum - 1));
20	}
21
22	// now check all the effects individually
23	var effects = ing.effects;
24	for(var i = 0; i < effects.length; i++) {
25		checkEffect(cauldronIndex, effects[i], obj);
26	}
27
28	return obj;
29}
30
31function checkEffect(cauldronIndex, effectName, obj, obscureName = false) {
32	var feedbackText = '';
33	var cSize = curCauldron.length;
34
35	var feedbackValue = '';
36	var singular = true;
37	switch(effectName) {
38		case 'Cutoff':
39			obj.directEffects['Cutoff'] = true;
40			feedbackValue = "The ingredient was cut off";
41			break;
42
43		case 'Spicy':
44			if(cauldronIndex < (cSize - 1)) {
45				curCauldron[(cauldronIndex+1)].myNum++; 
46			}
47			feedbackValue = "A spicy ingredient was encountered";
48			break;
49
50		// many more cases here ....
51	}
52
53	// and return feedback here
54}

Effects II

After several playtests, I noticed something annoying:

  • Effects were evenly distributed (intentionally), but this made the game significantly easier. If you knew an ingredient could have, at most, one effect, it became much easier to solve that puzzle.

  • Effects were picked completely at random. This meant that there were games where all effects had the same type, which made the game way less fun changed the dynamic too much.

The first problem was fixed by distributing effects completely randomly. On average, the distribution will be somewhat fair, but it could happen that one ingredient has 2 or 3 effects, while others have none.

The second problem was fixed by splitting the effects into categories. Instead of one giant list, I made several lists (“investigative effects”, “change cauldron effects”, etc.)

First, it grabs one random element from each list. If that is not enough ( = we need more effects), it concatenates all the lists (turning it into one huge list again) and picks randomly from that.

This ensures we have at least one effect of each type, but also keeps some randomness.

  1function createPuzzle() {
  2	// generate a random ordering of numbers
  3	var allNumbers = [];
  4	for(var i = 0; i < numIngredients; i++) {
  5		// these ingredients are part of the recipe, so give them numbers 1,...,n
  6		if(i < codeLength) {
  7			allNumbers[i] = (i+1);
  8
  9		// these ingredients are not, so give them a -1 (this will be replaced with a proper decoy soon)
 10		} else  {
 11			allNumbers[i] = -1;
 12		}
 13		
 14	}
 15	allNumbers = shuffle(allNumbers);
 16
 17  // save the ingredients
 18  // and determine a decoy type for the non-ingredients (those without a number)
 19	for(var i = 0; i < numIngredients; i++) {
 20		var num = allNumbers[i];
 21		var effects = [];
 22
 23		// let's determine a random decoy type (0,1,2)
 24		// 0 = IGNORE; this ingredient will be skipped and not interacted with, although effects execute
 25		// 1 = OVERACHIEVER; this ingredient is always undergrown (0) or overgrown (n+1)
 26		// 2 = IMPOSTER; this ingredient behaves like a regular ingredient, but it has the "imposter" effect
 27		var decoyStatus = -1;
 28		if(num == -1) {
 29			var randNum = Math.random();
 30			if(randNum <= 0.33 || difficulty < 2) {
 31				decoyStatus = 0;
 32			} else if(randNum <= 0.66) {
 33				decoyStatus = 1;
 34
 35				if(Math.random() <= 0.5) {
 36					num = 0;
 37				} else {
 38					num = (codeLength+1);
 39				}
 40			} else {
 41				decoyStatus = 2;
 42				num = Math.floor(Math.random()*codeLength)+1;
 43				effects.push("Imposter");
 44			}
 45		}
 46
 47		var obj = { "myName": ingredientNames[i], "myNum": num, "effects": effects, "decoyStatus": decoyStatus };
 48		curPuzzle[i] = obj;
 49	}
 50
 51	// if effects are enabled ...
 52	//  => randomly distribute effects across the ingredients
 53	//  => shuffle them, grab X of them (however many needed)
 54	if(effectsLevel > 0) {
 55		// DEEP copy the effects array
 56		// (because we'll be adding/removing in a moment)
 57		var effectListCopy = JSON.parse(JSON.stringify(allEffects));
 58
 59		// determine number of effects (based on number of ingredients + effects level)
 60		if(effectsLevel == 1) {
 61			numEffects = Math.floor(numIngredients*0.5);
 62		} else if(effectsLevel == 2) {
 63			numEffects = Math.floor(numIngredients*0.75);
 64		}
 65
 66		// RETRIEVE this number of effects
 67		// there's a specific order to grabbing effects: 1) Change Cauldron, 2) Investigative, 3) Change Player, 4) Change Field, 5) Complex
 68		// once this order is exhausted, it starts picking stuff randomly, according to some probabilities
 69		var possibleEffectTypes = ['ChangeCauldron', 'Investigative', 'ChangePlayers', 'ChangeField'];
 70		var finalEffectList = [];
 71		finalEffectList[0] = shuffle(effectListCopy['ChangeCauldron']).splice(0, 1)[0];
 72		finalEffectList[1] = shuffle(effectListCopy['Investigative']).splice(0, 1)[0];
 73		finalEffectList[2] = shuffle(effectListCopy['ChangePlayers']).splice(0, 1)[0];
 74		finalEffectList[3] = shuffle(effectListCopy['ChangeField']).splice(0, 1)[0];
 75		if(effectsLevel >= 2) {
 76			finalEffectList[4] = shuffle(effectListCopy['Complex']).splice(0, 1)[0];
 77			possibleEffectTypes.push('Complex');
 78		}
 79
 80		// now build a single list consisting of all types
 81		// (this automatically ensures that, when grabbing randomly, we have the right probabilities of grabbing each type)
 82		var bigEffectList = [];
 83		for(var i = 0; i < possibleEffectTypes.length; i++) {
 84			bigEffectList = bigEffectList.concat(effectListCopy[ possibleEffectTypes[i] ]);
 85		}
 86
 87		// once the default order is done, as long as we don't have enough effects yet, grab them completely randomly
 88		while(finalEffectList.length < numEffects) {
 89			finalEffectList.push( bigEffectList.splice(0, 1)[0] );
 90		}
 91
 92		// now apply them randomly
 93		for(var i = 0; i < finalEffectList.length; i++) {
 94			var curEffect = finalEffectList[i];
 95
 96			// access a random ingredient
 97			var randType = Math.floor(Math.random() * numIngredients);
 98			curPuzzle[randType].effects.push( curEffect.name );
 99		}
100	}
101}

Some remarks on implementation

Implementation Remark: at the start of potion evaluation, I create an object. Why? Because objects are passed by reference in JavaScript. As I evaluate each ingredient, I add any results (feedback, changes I need to make) to this object. Once evaluation is complete, this means I have an array of feedback I need to return to the player.

Here’s the most important thing: I shuffle this array. In the first implementations, I forgot to do this, which meant the order of feedback gave away which ingredients had which effects.

Implementation Remark 2: each effect is thrown through a huge switch statement in the code. It’s perhaps not ideal, but it was a very fast and clean way to implement many different effects.

However, there were some “direct effects” that had a delayed execution. For example, the “Enthusiastic” effect means you skip the next ingredient. This effect needed to be remembered and only applied at the end of a loop iteration.

For this, I also added a list of direct effects to that object I described above. At the end of each loop iteration, I check for these effects and handle them accordingly.