Reaction game

A reaction game on the Arduino: a state machine with switch and case, a random wait with random(), timing with millis() and sounds with tone() that don't wait

Arduino Electronics
Reaction game

A real pinball machine is always in some state. While nobody is playing, the lamps blink and the display scrolls a message. This is attract mode, in which the machine tries to attract players. When a player presses Start, the game begins. When the ball falls into the drain, the machine waits for the next ball, and at the end it shows the score and starts attracting players again. The same button means something different in each state, and each state has its own lamps and its own sounds.

A program that works like this is called a state machine. Today we’ll write one for a small game, a reaction game. Along the way, we’ll learn to play sounds that don’t stop the program.

What does this mean for the pinball machine?

Top view of the pinball machine: a blue circle marks part of the left speaker and of the display's first LED matrix, and a large blue circle shows them enlarged: the speaker grille and the lit dots of the letter P

On the top view of our pinball machine, a blue circle marks part of the left speaker and of the first of the four LED matrices on the display, and the large circle shows them enlarged. The lit dots on the matrix are part of the letter P of the word PINBALL, which the display scrolls while the machine attracts players, and the game's sounds come from the speaker. The pinball page explains the other parts of the drawing.

The pinball machine's main Arduino will be a state machine, just with more states: the machine attracts players, a game is running, the ball is lost, the player has shoved the machine too hard (tilt), and the game is over. Each state has its own lamps, sounds and rules for the buttons. When we design the pinball machine, we'll first draw the rules as a state diagram, and only then write the program.

In a pinball machine, several things always happen at once: the lamps blink, a sound plays, and the program must notice the ball at any moment. That's why there mustn't be a delay() there either.

How to play

  1. While the game is waiting for a player, the LED blinks slowly.
  2. You press the button. The LED goes off, and the game waits for a random time, from 1 to 4 seconds.
  3. The LED lights up and the buzzer beeps briefly. Now press the button as fast as you can!
  4. The program prints in the Serial Monitor how long it took you, in milliseconds.

If you press the button before the LED lights up, that’s too early. The buzzer then gives a long, low buzz.

Materials

Each participant gets:

Extra challenge 2 needs one more button and one more 470 Ω resistor.

Wiring

The wiring rules are the same as in the last lesson. Start from the circuit of the last lesson: the LED is on D8 and the button on D2. Add the buzzer in the same place as on the pedestrian traffic light, in row 27.

PartArduino pinWire from the ArduinoResistorRight side
LEDD8a3220 Ω: e3–f3red LED: long leg in j3, short leg in the right – line
ButtonD2a8470 Ω: b8–b12button in e12, e14, f12, f14; wire from j14 to the right – line
BuzzerD5a2768 Ω: e27–f27the buzzer’s + leg in j27, the other leg in the right – line
Common GNDGNDright – line––
Schematic: a red LED on pin D8 through a 220 ohm resistor, a buzzer on pin D5 through a 68 ohm resistor and a button on pin D2 through a 470 ohm resistor, all to GND
Wiring: the LED and the button as in the last lesson, plus D5 to a27, a resistor from e27 to f27, the buzzer's plus leg in j27 and its other leg in the right minus line
  • The buzzer has a polarity. The + leg is longer, and there is also a + mark on the top of the case. A buzzer put in the wrong way round stays silent.
  • The 68 Ω resistor lowers the current through the pin and also makes the buzzer quieter.
  • Pin A0 stays unconnected. We’ll find out why in Exercise 2.

Exercise 1: A sound that doesn’t wait

We met the buzzer on the pedestrian traffic light. An active buzzer buzzes by itself, always with the same tone, and tone() switches it on and off very quickly, so we hear different sounds (there’s more about this in the section The active buzzer). The first program gives a short beep with every press, and the LED is on while we hold the button. We read the button with the Bounce2 library, like the Start button at the end of the last lesson.

// C++
#include <Bounce2.h>                   // the library for debouncing buttons

const int button = 2;
const int ledPin = 8;
const int buzzer = 5;

Bounce2::Button player;                // the player's button

void setup() {
  player.attach(button, INPUT_PULLUP); // the button is on D2, with the built-in pull-up resistor
  player.interval(10);                 // a change counts when the button has been still for 10 ms
  player.setPressedState(LOW);         // pressed is LOW
  pinMode(ledPin, OUTPUT);
  pinMode(buzzer, OUTPUT);
}

void loop() {
  player.update();                     // read and debounce the button

  if (player.pressed()) {
    tone(buzzer, 800, 60);             // a short beep with every press
  }
  digitalWrite(ledPin, player.isPressed()); // the LED is on while the button is pressed
}

Upload the program and press the button a few times, briefly and for longer.

What’s new in the program?

  • Bounce2::Button player; – The button is an object, like the Start button in Exercise 5 of the last lesson. It is called player, because it is the player’s button.
  • player.update(); – The library reads and debounces the button. It must be in every pass through loop(), once, before any questions about the button.
  • player.pressed() and player.isPressed() – The first gives true only in the pass in which the button has just been pressed, and the second while it is pressed.
  • tone(buzzer, 800, 60); – As on the traffic light: a sound with a frequency of 800 Hz lasts 60 ms. tone() only starts the sound, and the program goes on at once. After 60 ms the Arduino’s chip switches the sound off by itself while the program does something else. That’s why the LED follows the button even while the buzzer is buzzing.

A fanfare with delay()

Now let every press play a short fanfare of three sounds. In loop(), replace the line with tone() with these lines:

// C++
    tone(buzzer, 400, 100);
    delay(150);
    tone(buzzer, 600, 100);
    delay(150);
    tone(buzzer, 800, 300);
    delay(300);

Upload the program and try it:

  1. Press the button and hold it. When does the LED light up?
  2. Press the button five times, as fast as you can. How many fanfares did you hear?
Why is the LED late, and why do some presses get lost?
During the three delay() calls, the program just waits for 600 ms. It doesn’t look at the button and doesn’t switch the LED, so the LED lights up only when the fanfare is over. A press that starts and ends during the fanfare isn’t seen by the program at all.

Experiment: delete all three delay() calls and upload the program again. What do you hear now?

Why do we hear only the last sound without delay()?
tone() doesn’t wait for the sound to end. All three calls run one after another, within one millisecond, and each new sound replaces the previous one at once. The Arduino plays only one sound at a time, so only the last one, at 800 Hz, is left.

To play the fanfare without delay(), the program would have to remember which sound comes next and when to start it. In other words, it would have to remember where it has got to. That’s exactly what a state machine does.

Exercise 2: The game as a state machine

The reaction game is always in one of four states. Each state has its own name, and the game moves from one state to another when something happens: when the player presses the button, or when some time has passed.

StateWhat happensWhen it changesTo state
WAITINGthe game waits for a playerpressGET_READY
GET_READYthe LED is off, the game waits for a random 1–4 sthe random time is upGO
press: too early!RESULT
GOthe LED is on, we measure the timepress: print the timeRESULT
RESULTa 2 s pause2 s have passedWAITING

We can draw the same table as a state diagram: each state is a rectangle, and the arrows are the changes between states. Next to each arrow is what causes the change. We’ll draw a diagram like this later for the rules of the whole pinball machine.

State diagram of the reaction game: from WAITING a press leads to GET_READY. From GET_READY the random time leads to GO, and a press that's too early leads to RESULT. From GO a press leads to RESULT, and from RESULT, after 2 s, back to WAITING
Plan before you program:
repeat forever:
check whether the button has just been pressed
if the state is WAITING:
if it was pressed: choose a random wait, go to GET_READY
if the state is GET_READY:
if it was pressed: too early! long low sound, go to RESULT
otherwise, if the wait is over: switch on the LED, short sound, go to GO
if the state is GO:
if it was pressed: switch off the LED, print the time, go to RESULT
if the state is RESULT:
if 2 s have passed: go to WAITING

The program is longer than the ones so far, but each part of it is short. We read the button with the Bounce2 library, as in Exercise 1.

// C++
#include <Bounce2.h>                      // the library for debouncing buttons

const int button = 2;
const int ledPin = 8;
const int buzzer = 5;
const unsigned long PAUSE_TIME = 2000;    // how long the RESULT state lasts

// The game's states
const int WAITING = 0;                    // the game waits for a player
const int GET_READY = 1;                  // a random wait before the signal
const int GO = 2;                         // the LED is on: press as fast as you can!
const int RESULT = 3;                     // a short pause after the result

Bounce2::Button player;                   // the player's button
int state = WAITING;                      // which state the game is in now
unsigned long stateStart = 0;             // when the current state began
unsigned long waitTime = 0;               // how long GET_READY lasts

void setup() {
  player.attach(button, INPUT_PULLUP);
  player.interval(10);
  player.setPressedState(LOW);
  pinMode(ledPin, OUTPUT);
  pinMode(buzzer, OUTPUT);
  Serial.begin(9600);
  randomSeed(analogRead(A0));             // A0 is floating: a different start every time
  Serial.println("Press the button to start.");
}

void loop() {
  player.update();                                  // we read the button once, at the start
  unsigned long inState = millis() - stateStart;    // how long the game has been in the current state

  switch (state) {
    case WAITING:
      if (player.pressed()) {
        waitTime = random(1000, 4001);    // from 1 to 4 s
        Serial.println("Get ready...");
        changeState(GET_READY);
      }
      break;

    case GET_READY:
      if (player.pressed()) {           // a press before the signal
        tone(buzzer, 150, 400);           // long low sound
        Serial.println("Too early! Wait for the LED to light up.");
        changeState(RESULT);
      } else if (inState >= waitTime) {   // time for the signal
        digitalWrite(ledPin, HIGH);
        tone(buzzer, 800, 60);
        changeState(GO);
      }
      break;

    case GO:
      if (player.pressed()) {
        digitalWrite(ledPin, LOW);
        Serial.print("Your time: ");
        Serial.print(inState);            // from the signal to the press
        Serial.println(" ms");
        changeState(RESULT);
      }
      break;

    case RESULT:
      if (inState >= PAUSE_TIME) {
        Serial.println("Press the button for a new game.");
        changeState(WAITING);
      }
      break;
  }
}

// Moving to a new state: we also remember when it began.
void changeState(int newState) {
  state = newState;
  stateStart = millis();
}

Upload the program, open the Serial Monitor (9600 baud) and play a few rounds. Once, try pressing too early on purpose.

What’s new in the program?

  • const int WAITING = 0; – The states are ordinary numbers, 0, 1, 2 and 3, but with names. state == GO is easier to read than state == 2, and if we misspell a name, compiling gives an error.
  • int state = WAITING; – A variable that remembers which state the game is in. At the start, the game waits for a player.
  • switch (state) { case WAITING: ... break; } – switch looks at the value of the variable state and jumps to the case with the same value. Only the code of that state runs, up to the word break, which ends the switch. We could write the same thing with a chain of if and else if, but this way it’s easy to see what each state does.
  • Don’t forget break. Without it, the program carries on running the code of the next state too, as if it weren’t there.
  • void changeState(int newState) – Every change of state goes through this function. Along with the new state, it remembers the moment the state began, stateStart.
  • unsigned long inState = millis() - stateStart; – How many milliseconds the game has already been in the current state. In the GET_READY state, we compare it with the random wait, in the RESULT state with the 2 s pause, and in the GO state it is exactly the time from the signal to the press: your reaction time.
  • random(1000, 4001) – A random number from 1000 to 4000. The upper limit, 4001, never comes out, so we write one more.
  • randomSeed(analogRead(A0)); – random() isn’t really random. After every start it gives the same numbers in the same order, as if it read them from a long table. randomSeed() chooses where in that table we start. Pin A0 is floating, so analogRead(A0) reads something different every time. The floating pin from the last lesson is useful to us here.
  • player.update(); at the top of loop() – We read the button once, at the start of every pass, and the states only ask player.pressed(). update() is outside the switch, so the library keeps track of the button in every state, including the RESULT state, where we don’t care about presses.
  • The time measured is a little longer than your reaction. Bounce2 reports a press only once the button has been still for 10 ms, as we saw in the last lesson. So every time is about 10 to 15 ms longer. Every player gets the same extra time, so it doesn’t change who is fastest.
  • There isn’t a single delay() in loop(). In the GET_READY state, the program watches the time and the button at the same time. That’s why it can notice a press that comes too early.
Why don't we write the random wait as delay(waitTime)?
During delay() the program doesn’t look at the button. It wouldn’t notice a press that comes too early, so the player couldn’t make a mistake. They could press the button before the signal and hold it, and the program would measure a time of almost 0 ms.

How fast do you react?

Most people react to light in about 200 to 250 ms, which is a quarter of a second. We react to a sound a little faster. In that time, the eye sees the light, the brain decides it's time to press, and the nerves move the muscles of the finger. Tiredness and not paying attention make that time longer, and practice shortens it only a little.

Our game gives a short sound as well as the LED. Try out what your time is when you only watch the LED, and what it is when you close your eyes and only listen to the buzzer.

Exercise 3: Attract mode and the best time

The game works, but while it waits for a player, nothing shows that it’s ready. We’ll add what every pinball machine has:

  • while the game waits, the LED blinks every 500 ms, like the yellow light of a traffic light with a fault;
  • a short sound when the game starts;
  • the best time since the Arduino was switched on, with a special sound when someone beats it.

The state diagram stays the same. We don’t add new states or new changes between them, only what happens inside the WAITING and GO states.

The whole program

// C++
#include <Bounce2.h>                      // the library for debouncing buttons

const int button = 2;
const int ledPin = 8;
const int buzzer = 5;
const unsigned long BLINK_TIME = 500;     // while the game waits, the LED changes every 500 ms
const unsigned long PAUSE_TIME = 2000;    // how long the RESULT state lasts

// The game's states
const int WAITING = 0;                    // the game waits for a player: the LED blinks
const int GET_READY = 1;                  // a random wait before the signal
const int GO = 2;                         // the LED is on: press as fast as you can!
const int RESULT = 3;                     // a short pause after the result

Bounce2::Button player;                   // the player's button
int state = WAITING;                      // which state the game is in now
unsigned long stateStart = 0;             // when the current state began
unsigned long waitTime = 0;               // how long GET_READY lasts
unsigned long bestTime = 0;               // the best time; 0 means there isn't one yet

bool ledOn = false;                       // for blinking while the game waits
unsigned long lastBlink = 0;              // when the LED last changed

void setup() {
  player.attach(button, INPUT_PULLUP);
  player.interval(10);
  player.setPressedState(LOW);
  pinMode(ledPin, OUTPUT);
  pinMode(buzzer, OUTPUT);
  Serial.begin(9600);
  randomSeed(analogRead(A0));             // A0 is floating: a different start every time
  Serial.println("Press the button to start.");
}

void loop() {
  player.update();                                  // we read the button once, at the start
  unsigned long inState = millis() - stateStart;    // how long the game has been in the current state

  switch (state) {
    case WAITING:
      if (millis() - lastBlink >= BLINK_TIME) {     // the LED blinks without delay()
        lastBlink = millis();
        ledOn = !ledOn;
        digitalWrite(ledPin, ledOn);
      }
      if (player.pressed()) {
        ledOn = false;
        digitalWrite(ledPin, LOW);
        tone(buzzer, 400, 50);            // short sound: the game starts
        waitTime = random(1000, 4001);    // from 1 to 4 s
        Serial.println("Get ready...");
        changeState(GET_READY);
      }
      break;

    case GET_READY:
      if (player.pressed()) {           // a press before the signal
        tone(buzzer, 150, 400);           // long low sound
        Serial.println("Too early! Wait for the LED to light up.");
        changeState(RESULT);
      } else if (inState >= waitTime) {   // time for the signal
        digitalWrite(ledPin, HIGH);
        tone(buzzer, 800, 60);
        changeState(GO);
      }
      break;

    case GO:
      if (player.pressed()) {
        digitalWrite(ledPin, LOW);
        Serial.print("Your time: ");
        Serial.print(inState);            // from the signal to the press
        Serial.println(" ms");
        if (bestTime == 0 || inState < bestTime) {
          bestTime = inState;
          tone(buzzer, 1000, 300);        // long high sound: a new best time
          Serial.println("New best time!");
        } else {
          tone(buzzer, 600, 100);
        }
        changeState(RESULT);
      }
      break;

    case RESULT:
      if (inState >= PAUSE_TIME) {
        Serial.print("Best time: ");
        Serial.print(bestTime);
        Serial.println(" ms. Press the button for a new game.");
        changeState(WAITING);
      }
      break;
  }
}

// Moving to a new state: we also remember when it began.
void changeState(int newState) {
  state = newState;
  stateStart = millis();
}

Upload the program and play a few rounds. Let the person next to you play too: who has the best time?

What’s new in the program?

  • Blinking happens only in the WAITING state. We used the same pattern with millis() to blink the yellow light in the lesson Two traffic lights at a crossroads. The code in case WAITING: runs only while the game is waiting, so the LED doesn’t blink in the other states, without us having to forbid it anywhere. This is a big advantage of a state machine: each state has its own code.
  • ledOn = false; and digitalWrite(ledPin, LOW); – When the game starts, we switch the LED off. Otherwise it could stay on, because the last blink may have been just the on half.
  • unsigned long bestTime = 0; – The best time. The value 0 means there isn’t a best time yet, because a reaction time of 0 ms isn’t possible.
  • if (bestTime == 0 || inState < bestTime) – || means or. The condition is true if there isn’t a best time yet or if the new time is better than the best time. In the last lesson we met &&, which means and.
  • Every important event has its own sound: the start of the game, the signal, a press that’s too early, an ordinary time and a new best time. All the sounds are started with tone() and don’t stop the program.
  • The best time is forgotten when we switch the Arduino off. On the pinball machine we’ll later save the high scores in a memory that keeps them even when the Arduino is switched off.

If something doesn’t work

  • The buzzer is silent ⇒ Is the longer leg, marked +, in j27, and the other one in the right – line? Is the wire from D5 in a27, and the resistor from e27 to f27?
  • The LED doesn’t light up or doesn’t react to the button ⇒ The LED and the button are wired as in the last lesson. Look at the section If something doesn’t work there.
  • The Serial Monitor shows nothing or shows strange characters ⇒ Choose 9600 baud.
  • The random wait is the same after every start ⇒ Is something connected to pin A0? Is the line with randomSeed() in setup()?
  • The game skips states or does something that isn’t in the diagram ⇒ Is there a break; at the end of every case?
  • The game doesn’t react to the button ⇒ Is player.update(); at the start of loop()? Without it, the library never reads the button.
  • The error Bounce2.h: No such file or directory ⇒ The Bounce2 library isn’t installed. Install it as in the last lesson.
  • The program won’t upload ⇒ Check that the board Arduino Uno and the right port are selected in the Arduino IDE.

Extra challenges

  1. Fallen asleep? If the player doesn’t press the button within 3 s in the GO state, let the program print Too slow! and go to the RESULT state. Which arrow do you then need to add to the state diagram?
  2. Two players. Add a second button on D4, wired as in extra challenge 1 of the last lesson. In the GO state, the player who presses their button first wins. Whoever presses too early loses. Tip: the second button gets its own object, for example player2, and at the start of loop() you call update() for both.
  3. A fanfare without delay(). Let the game play the three-sound fanfare from Exercise 1 at the start, but without delay(). Tip: store the frequencies in an array, int notes[] = {400, 600, 800};. An array is a variable with several values, and we get to each one by its position: notes[0] is 400, notes[2] is 800. The program has to remember which note comes next and when to start it. That’s a small state machine inside the game.
  4. Average. After every five rounds, let the program print the average time of those five rounds. A press that’s too early doesn’t count as a round.

What did we learn?

  • A state machine is a program that is always in one of several states. In each state it does something different, and it moves to another state when something happens.
  • We draw states and the changes between them as a state diagram: the rectangles are states, and the arrows are changes.
  • States are numbers with names (const int WAITING = 0;), and the variable state remembers the current state.
  • switch and case run only the code of the state the program is in. Each case ends with break.
  • If we remember the time at every change of state, millis() - stateStart tells us how long the program has been in that state.
  • With the Bounce2 library we read the button once, with update() at the start of every pass, and the states ask pressed().
  • tone(pin, frequency, duration) only starts the sound and doesn’t wait for it to end. The Arduino plays one sound at a time: a new one replaces the previous one.
  • delay() stops the whole program. During that time the Arduino doesn’t see the button.
  • random(from, to) gives a random number, and the upper limit never comes out. randomSeed(analogRead(A0)) with a floating pin gives different numbers after every start.
  • || means or, and && means and.

Quick quiz

1. What is a state machine?
A program that is always in one of several states. In each state it does something different, and it moves from state to state when something happens, for example when the player presses the button or when some time has passed.
2. Why does the LED in Exercise 1 follow the button even while the buzzer is buzzing?
tone(buzzer, 800, 60) only starts the sound, and the program goes on at once. After 60 ms the Arduino’s chip switches the sound off by itself, and meanwhile the program keeps reading the button and switching the LED.
3. What happens if we forget break at the end of a case?
The program doesn’t leave the switch, but carries on running the code of the next state too. The game then does things that aren’t in the state diagram, for example it skips a state.
4. What does inState mean in the GO state?
How many milliseconds have passed since the LED lit up, that is, since the game went into the GO state. When the player presses the button, that’s their reaction time.
5. Can random(1000, 4001) give 4001?
No. The upper limit never comes out, so the largest number is 4000. That’s why we write 4001 when we want a wait of up to 4000 ms.
6. Why must pin A0 stay unconnected in this game?
randomSeed(analogRead(A0)) needs a random value. A floating pin reads something different every time, so after every start the game waits for different times. If the pin were connected, it would always read the same value, and after every start the game would wait for the same times in the same order.
7. How does the game notice a press that comes too early?
In the GET_READY state, the program checks both the time and the button in every pass through loop(). If the button is pressed before the wait is over, the press came too early, so the game goes to the RESULT state.
8. Why doesn't the LED blink in the GO state, even though the blinking code is in the program?
The blinking code is in case WAITING:, so it runs only while the game is in the WAITING state. In the GO state, only the code from case GO: runs.