Enginursday: Creating Random Sequences with Rules
When you put a bunch of electronics around trampolines in a room, and ask thousands of kids to jump on them, it’s only a matter of time before something fails. About three weeks after Boulder Bounces opened, we got an email telling us that it was failing on occasion.
Background of the project
We recently designed and installed an exhibit at the Museum of Boulder called Boulder Bounces. It’s a memory game much like the Simon Says Soldering Kit, but with trampolines. You can read more about it at this blog post, or watch the following video:
The original Simon Says uses the built-in random() Arduino function. While this works fine for the original game, Boulder Bounces needed some extra rules in the sequence generation to avoid “boring” bounce patterns. It’s not that fun to have many repetitions of the same trampoline, and any repetitions in the beginning of the sequence are not only difficult, but can lead to some serious confusion for the first-time player. Plus, when you’re playing with a group of jumpers (each standing on their own trampoline), it is loads more fun to ensure everyone gets to jump.
Let the investigation begin!
I have a lot of serial debug messages sending out from my Arduino Pro Mini on this project, so I was eager to plug in a FTDI basic and hear what was going on. My original email from the museum said that if they simply cycled power, it would start working again. Because of this, my first thought was that it was most likely failing in software, and not a hardware issue.
When I opened up the panel on the wall, it looked like the LEDs had been mistaken for buttons and pushed completely back into the panel. Those darn kids! I guess I can’t blame them; the gumdrop LEDs are big enough to be a button, but I needed to stay focused on the problem at hand.
Jumping right in
I played through an entire game once. It worked just fine. I played another round. No issue. Again, and again. Nothing. Then on the sixth game, right before I should have heard the winning sounds, it seemed to stall out.
I went to look at my serial debug, but it seemed to stop spitting out any serial debug at all. Hmmmm. When I am troubleshooting a code bug that involves a “stall out” like this, my first approach is to verify the point of failure and then work backward to find out the potential issue.
During my last game (right before the failure), I remember my serial debug was working properly. It was outputting each new sequence after each successful play correctly. But at my “win,” the serial debug stopped, so the failure must be occurring somewhere immediately after the last add_to_moves() was called.
A high-level look at the original code
Before we jump into any of the original code, I think it would be beneficial to share the high-level plan for gameplay, and my higher-level approach to creating the random sequence with rules.
The original gameplay code worked like this:
When you are playing a game, the Arduino will “play” back the sequence to you. At first, it only shows one trampoline light up, and you jump on it. Then the Arduino “randomly” chooses the next trampoline to add to the sequence. It plays back the entire sequence (which at this point is only two jumps long), and you must jump the sequence in order. Then it adds another, and so on. Once the sequence reaches eight and you successfully jump in the correct order, you win!
The specific part of the code that I’m talking about today is a function called “add_to_moves()”. This is a simple enough function in theory. Let’s take a look at the original function from the original Simon Says to start. Note, this code is from the button game, and was not used on the trampolines installation, but it’s a good place to start.
void add_to_moves(void)
{
byte newButton = random(0, 4); //min (included), max (exluded)
// We have to convert this number, 0 to 3, to CHOICEs
if(newButton == 0) newButton = CHOICE_RED;
else if(newButton == 1) newButton = CHOICE_GREEN;
else if(newButton == 2) newButton = CHOICE_BLUE;
else if(newButton == 3) newButton = CHOICE_YELLOW;
gameBoard[gameRound++] = newButton; // Add this new button to the game array
}
First, you notice that the “random(0,4);” is generating the next “newButton” in the sequence. The following five lines of code are not really important in this discussion, but to quickly explain, we need to change them to the defined variables associated with each button choice available. For today, let’s just focus on the creation of newButton and then how it is added to the array, gameBoard[].
Looking at the original function add_to_moves(), it simply uses random() to add a new button to the sequence. This worked fine for the original Simon Says, but for Boulder Bounces, we needed to add in some rules to make it more interesting.
Add a while loop and some rules
My first approach to adding rules was to put it in a while loop. Inside this while loop, it would first grab a random newButton value from random(), then compare it against some rules. If it passes all the rules, then it would use it, and break out of the while loop. If it doesn’t pass a rule, then it would do nothing, stay in the while loop and try again.
The problem with this approach is that it is living on a prayer that random will eventually return something that will pass the rules. I knew this wasn’t perfect, but in all my development work on this project I never saw it get stuck. It usually tried a couple times at most, but eventually returned something that would work. It turns out that it can get stuck there, and something was causing it to stop trying.
For reference, the original code (with the actual rules - aka “if” statements) is as follows. You can see all of it here on my github commit. Specifically, here is my original add_to_moves():
void add_to_moves(void)
{
while (1)
{
newButton = random(0, 3);
if (!((newButton == gameBoard[gameRound - 1]))) // avoid repeats
{
// check to see if that button is already "maxed out"
if ((newButton == 0) && (RED_seq_count == 2)); // do nothing
else if ((newButton == 1) && (GREEN_seq_count == 3)); // do nothing
else if ((newButton == 2) && (BLUE_seq_count == 3)); // do nothing
//else if((newButton == 3) && (YELLOW_seq_count == 2)); // do nothing
else break; // get out of this while loop and continue playing.
}
}
// We have to convert this number, 0 to 3, to CHOICEs
if (newButton == 0)
{
newButton = CHOICE_RED;
RED_seq_count++;
}
else if (newButton == 1)
{
newButton = CHOICE_GREEN;
GREEN_seq_count++;
}
else if (newButton == 2)
{
newButton = CHOICE_BLUE;
BLUE_seq_count++;
}
else if (newButton == 3)
{
newButton = CHOICE_YELLOW;
YELLOW_seq_count++;
}
gameBoard[gameRound++] = newButton; // Add this new button to the game array
}
You may also notice that my rules rely heavily on some new variables: RED_seq_count, BLUE_seq_count, etc. These are helping me keep track of how many times I’ve already included a specific color button in the sequence, so I can avoid favoring any button and ensure everyone gets to jump.
In an attempt to hone in on the problem, I added a little line of debug at the end of my add_to_moves() function.
if(debug) Serial.println("add_to_moves attempting...");
Now I would know whether it was trying at all, getting stuck in this while loop or stalling out elsewhere.
After much jumping and winning, I got another failure in the middle of a round. It stalled out, and my serial monitor looked like this:
Remember, my first failure was just before a “win.” Because this second failure was in the middle of a game, this was a new clue! This means that the failure is caused somewhere else in the code, and probably has nothing to do with the code just before a win.
Right before it stalled out, the entire serial monitor was filled with the message “add_to_moves attemping…” It was sending this message again and again as fast as my little Pro Mini could go. Something strange was going on. Why would it get stuck repeating this message, and then eventually give up?
The speed of the repetition of the debug message was also strange. When it seemed to be trying normally, the message would spit out to the terminal at a moderate pace. But when the failure would occur, the messages would speed up dramatically and eventually cause a stall out. After much finagling and many attempts to cause failures, I eventually gave up trying to find the root cause. I knew there was a better way, and so decided to start a different approach the the random generator with rules.
A better approach
To elliminate the possibility of any stall-outs, I introduced a new array called “on_deck_buttons[].” I would have my rules adjust this array, and then my random function can just choose from the good options. That way, random() will only be called once, and this removes the need to loop back around and try again and again.
I also beefed up my debug to get a better view into what was actually going on with all my variables and arrays.
Here is my new add_to_moves() function:
// Adds a new random button to the game sequence
void add_to_moves(void)
{
byte newButton = random(0, 3); //min (included), max (exluded)
int add_to_moves_counter = 0; // keep track of our while loop below for finding good new buttons, and see if this is jamming the game.
if(gameRound == 0); // do nothing - newButton is randomly set above.
else if((gameRound == 1) || (gameRound == 2)) // jumps 2 and 3 are important. for the first 3 jumps we want these to always hit each tramp once
{
while(1)
{
add_to_moves_counter++; // keep track of how many times we are trying to find a good new button to use (random might be stalling out here)
int on_deck_buttons[2] = {0,0}; // these are used to help reandomize below
newButton = random(0, 3); // pull in a first attempt at a nice new random button
if((newButton == 0) && (RED_seq_count > 0))
{
on_deck_buttons[0] = 1;
on_deck_buttons[1] = 2;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
else if((newButton == 1) && (GREEN_seq_count > 0))
{
on_deck_buttons[0] = 0;
on_deck_buttons[1] = 2;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
else if((newButton == 2) && (BLUE_seq_count > 0))
{
on_deck_buttons[0] = 0;
on_deck_buttons[1] = 1;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
//else if((newButton == 3) && (YELLOW_seq_count == 2)); // do nothing
//else break; // get out of this while loop and continue playing. This means that the new button is good to go.
else
{
Serial.println("error");
break;
}
if(debug) Serial.println("add_to_moves attempting...");
if(debug) Serial.print("gameBoard[gameRound-1]:");
if(debug) Serial.println(gameBoard[gameRound-1]);
if(debug) Serial.print("gameBoard[gameRound-2]:");
if(debug) Serial.println(gameBoard[gameRound-2]);
}
if(debug)
{
Serial.print("add_to_moves_counter: ");
Serial.println(add_to_moves_counter);
Serial.print("RED_seq_count: ");
Serial.println(RED_seq_count);
Serial.print("GREEN_seq_count: ");
Serial.println(GREEN_seq_count);
Serial.print("BLUE_seq_count: ");
Serial.println(BLUE_seq_count);
}
}
else // only after you make it to step 3.
{
// attempt 1, works.
// while((newButton == gameBoard[gameRound-1]) && (newButton == gameBoard[gameRound-2])) newButton = random(0, 4); // keep pulling in more variables until this isn't true.
// basically, if it's the same as the previous 2 buttons, then it will keep finding a random number until it's "fresh".
// attempt 2, attempting to add in limit per button to avoid leaving one jumper out.
while(1)
{
add_to_moves_counter++; // keep track of how many times we are trying to find a good new button to use (random might be stalling out here)
int on_deck_buttons[2] = {0,0}; // these are used to help reandomize below
newButton = random(0, 3); // pull in a first attempt at a nice new random button
// This ensures it's not going to repeat same button 3 times
//if((convert_to_choice_byte(newButton) == gameBoard[gameRound-1]) && (convert_to_choice_byte(newButton) == gameBoard[gameRound-2])); // do nothing, try again
// check to see if that button is already "maxed out" (aka it has been used twice already)
if((newButton == 0) && (RED_seq_count == 2))
{
on_deck_buttons[0] = 1;
on_deck_buttons[1] = 2;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
else if((newButton == 1) && (GREEN_seq_count == 3))
{
on_deck_buttons[0] = 0;
on_deck_buttons[1] = 2;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
else if((newButton == 2) && (BLUE_seq_count == 3))
{
on_deck_buttons[0] = 0;
on_deck_buttons[1] = 1;
newButton = on_deck_buttons[random(0, 2)]; // chooose randomly from on_deck_buttons - aka the only ones we want to choose from
break;
}
//else if((newButton == 3) && (YELLOW_seq_count == 2)); // do nothing
//else break; // get out of this while loop and continue playing. This means that the new button is good to go.
else
{
Serial.println("error");
break;
}
if(debug) Serial.println("add_to_moves attempting...");
if(debug) Serial.print("gameBoard[gameRound-1]:");
if(debug) Serial.println(gameBoard[gameRound-1]);
if(debug) Serial.print("gameBoard[gameRound-2]:");
if(debug) Serial.println(gameBoard[gameRound-2]);
}
if(debug)
{
Serial.print("add_to_moves_counter: ");
Serial.println(add_to_moves_counter);
Serial.print("RED_seq_count: ");
Serial.println(RED_seq_count);
Serial.print("GREEN_seq_count: ");
Serial.println(GREEN_seq_count);
Serial.print("BLUE_seq_count: ");
Serial.println(BLUE_seq_count);
}
}
// We have to convert this number, 0 to 3, to CHOICEs
if(newButton == 0)
{
newButton = CHOICE_RED;
RED_seq_count++;
}
else if(newButton == 1)
{
newButton = CHOICE_GREEN;
GREEN_seq_count++;
}
else if(newButton == 2)
{
newButton = CHOICE_BLUE;
BLUE_seq_count++;
}
else if(newButton == 3)
{
newButton = CHOICE_YELLOW;
YELLOW_seq_count++;
}
gameBoard[gameRound++] = newButton; // Add this new button to the game array
}
The trouble with failures that only happen once in a while is that it can require lots of data points and repeated testing to emulate the failure. For most software issues, this can sometimes be automated and done quickly, however, with exhibits like these that involve physical hardware, the true game play must be cycled again and again to create the failure.
The trampolines were failing randomly every 10-20 game plays, so that meant a lot of jumping. Never in my life have I had such a physically demanding code debug session! I did my fair share of jumping during the development of this project, but that paled in comparison to this debugging session.
If you have experienced any strange stall-out with the random() function, or have any other approaches to creating sequences like this, please share in the comments below. Also, please check out the github repo link below, and if you so feel inclined, we always love seeing any issues, forks, pull requests or comments. Thanks for reading and happy coding!
Via Westline T2B http://www.rssmix.com/
Comments
Post a Comment