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

Wondering Witches (Part 3)

Welcome to part 3 of my Technical Devlog for the game Wondering Witches!

Haven’t read the other entries? Go to the devlog overview.

The Importance of Feedback

Great, our potion evaluator can check if this is the winning potion and execute all necessary effects.

Feedback Level #1

But one big step is missing: players test a potion to gather information. The website should give them some feedback. This feedback should be useful and informative, but not too useful. That would make the game too easy or straightforward.

At first, I created a category of investigative effects to solve this problem. It was a list of ~20 effects which simply reported information about the potion.

For example, the Detective tells you the secret number of a random ingredient from the potion. Or the Inspector tells you how many ingredients had one (or more) special effects in this potion.

These effects were extremely useful and effective! Whenever they were used, I would just save phrases like “Detective says 4”, and print that to the screen (in a random order).

 1// This is a subsection of that whole switch statement in checkEffect()
 2
 3//
 4// INVESTIGATIVE
 5//
 6case 'Liar':
 7  // make list of all numbers
 8  var allNumbers = [];
 9  for(var i = 0; i < codeLength; i++) {
10    allNumbers[i] = i;
11  }
12
13  // go through cauldron and REMOVE numbers that are present
14  for(var i = 0; i < cSize; i++) {
15    var findElem = allNumbers.indexOf(curCauldron[i].myNum);
16    if(findElem > -1) {
17      allNumbers.splice(findElem, 1);
18    }
19  }
20
21  // now pick one randomly
22  singular = false;
23  if(allNumbers.length > 0) {
24    var randNum = allNumbers[Math.floor(Math.random()*allNumbers.length)];
25    feedbackValue = 'number ' + randNum;
26  } else {
27    feedbackValue = 'nothing to say';
28  }
29  break;
30
31case 'Detective':
32  var randNum = curCauldron[Math.floor(Math.random() * cSize)].myNum;
33
34  singular = false;
35  feedbackValue = 'number ' + randNum;
36  break;
37
38case 'General':
39  var totalNumEffects = 0;
40  for(var i = 0; i < cSize; i++) {
41    totalNumEffects += curCauldron[i].effects.length;
42  }
43
44  singular = false;
45  feedbackValue = totalNumEffects;
46  break;
47
48case 'Inspector':
49  var numWithEffects = 0;
50  for(var i = 0; i < cSize; i++) {
51    if(curCauldron[i].effects.length > 0) {
52      numWithEffects++;
53    }
54  }
55
56  singular = false;
57  feedbackValue = numWithEffects;
58  break;
59
60case 'Calculator':
61  var sum = 0;
62  for(var i = 0; i < cSize; i++) {
63    if(curCauldron[i].myNum == -1) { continue; }
64    sum += curCauldron[i].myNum;
65  }
66
67  singular = false;
68  feedbackValue = sum;
69  break;
70
71case 'Revealer':
72  var numDecoys = 0;
73  for(var i = 0; i < cSize; i++) {
74    if(curCauldron[i].decoyStatus != -1) {
75      numDecoys++;
76    }
77  }
78
79  singular = false;
80  feedbackValue = numDecoys;
81  break;

Feedback Level #2

But it was not enough. When it comes to puzzle games, it’s never good to actively withhold 90% of the information from the player. That just makes it an impossible problem, not a fun puzzle.

So I decided to give feedback about any effect. If an effect was not investigative, it would simply say something like “Effect X was encountered” or “Effect X was present”. Knowing that an effect was present, is very valuable information (and easy to code). Not knowing precisely what the effect did, made it into a challenging puzzle.

Feedback Level #3

But there was another level of feedback I was forgetting. What if players didn’t figure out the solution? What if players lost the game? Surely, they would want to know the actual solution.

So I added a button that, when clicked, printed the solution to the screen. (Each ingredient, plus their number, plus any effects.) In hindsight, it was absolutely stupid to forget the inclusion of this button. I’m happy I remembered to do so.

Of course, I was worried about people accidentally clicking it. Or being too tempted to click it. So I made it smaller, moved it away from the interface, colored it red, and clearly marked it as a danger zone. It’s worked perfectly so far.

Feedback Level #3

But wait, is that … another level of feedback I am forgetting? Sure it is!

This realization only came to me after numerous playtests. (Which tells us, again, that playtesting your game is always the way to go.)

At the start of the game, players were just … really unsure about what to do. They had absolutely zero information, so what does it matter which ingredients we grow? What does it matter which potion we try?

I changed this to give the players a flying start. When you generate a puzzle, it gives you one clue “for free”. It might say something like “Free Advice: Parsley is NOT number 1” or “Bonus Tip: Either Parsley, Sage or Thyme is a DECOY”

This gets you started! You already know some ingredients you want to test (or avoid). Additionally, it makes the game a little easier. As it stands, the game is quite hard, so by framing this information as a “free clue”, I can sneakily balance the game a bit more.

 1function generateFirstHint(type) {
 2	var txt = '<p>Here\'s a <strong>free clue</strong> to start the game!</p><p>';
 3	switch(type) {
 4		// reports a group of three elements
 5		// and either says "one of these is a decoy"
 6		// or "one of these has number X" (and thus is a good ingredient)
 7		case 'Group':
 8			var copyPuzzle = JSON.parse(JSON.stringify(curPuzzle));
 9			copyPuzzle = shuffle(copyPuzzle).splice(0,3);
10
11			if(copyPuzzle[0].decoyStatus >= 0) {
12				txt += 'One of these ingredients is a DECOY: ';
13			} else {
14				txt += 'One of these ingredients has number ' + copyPuzzle[0].myNum + ': ';
15			}
16
17			copyPuzzle = shuffle(copyPuzzle);
18			txt += copyPuzzle[0].myName + ', ' + copyPuzzle[1].myName + ' or ' + copyPuzzle[2].myName;
19			break;
20
21		// picks one ingredient and states which number it is NOT
22		case 'Negative':
23			// pick a random ingredient
24			var randNum = Math.floor(Math.random() * curPuzzle.length);
25			var randIng = curPuzzle[randNum];
26			txt += ingredientNames[randNum] + ' is NOT ';
27
28			// if it's a non-imposter decoy, return any valid number
29			// otherwise, report a lower number with 50% chance, or a higher number with 50% chance
30			var realNum = randIng.myNum;
31			if(realNum == -1 || realNum == 0 || realNum == (codeLength+1)) {
32				txt += Math.floor(Math.random()*codeLength) + 1;
33			} else {
34				if(Math.random() <= 0.5) {
35					txt += Math.floor(Math.random()*realNum);
36				} else {
37					txt += Math.floor(math.random()*(codeLength-realNum+1))+realNum+1;
38				}
39			}
40
41			break;
42
43		// pick one ingredient and state whether it has effects or not
44		case 'Effects':
45			var randIng = curPuzzle[Math.floor(Math.random() * curPuzzle.length)];
46			txt += randIng.myName;
47
48			// if effects aren't even enabled, ??
49			if(effectsLevel == 0) {
50				if(randIng.myNum <= 0) {
51					txt += ' is a decoy';
52				} else if(randIng.myNum <= Math.floor(0.5*codeLength)) {
53					txt += ' should be in the first half of the potion';
54				} else {
55					txt += ' should be in the last half of the potion';
56				}
57				
58			
59			// if effects are enabled, return info here
60			} else {
61				txt += ' has ' + randIng.effects.length + ' effects ';
62			}
63
64			break;
65	}
66
67	txt += '</p>';
68	return txt;
69}

Remark: additionally, I changed something about board generation to fix this problem. I’ll talk about it more later, but here’s the general idea. At first, when I generated a starting setup, I placed random ingredients in gardens. Now I changed that so it also places some ingredients within cauldrons. Why? Because this means you can already test a potion within one or two turns, giving the game a quicker start.

What’s the conclusion? When it comes to games, the mantra is “feedback, feedback, feedback” Both boardgames and video games.