Davis Burnside

Arduino 3-player reflex game

I've been looking forward to making an Arduino post for a while, but I couldn't settle on an idea that would be simple enough for a tutorial. But a few days ago I was playing keep-away with my roommate's dog and I realized... everybody loves a reflex game- even dogs.
The Arduino is the perfect hardware platform for a reflex game. Its clock speed is fast enough to handle split-second logic decisions, and the software / hardware is extremely simple to make. (Input is handled by buttons and a vibration sensor, output is handled with LEDs and console statements).

Here is a video of it in action. When the blue LED is on, the Arduino listens for a vibration, which is triggered by dropping an object on the table. When the vibration sensor is tripped, the blue LED turns off and the yellow LED turns on. The first player to press their button wins, and their corresponding red LED activates. If a button is pressed before the vibration sensor is triggered / yellow light is on, that player is disqualified for the round. Press the Reset button (out of camera view) and everybody is ready to play again.

I was really dreading having to draw-out the circuit diagrams for this post, but I found a really cool website that not only lets you drag-and-drop all the physical components, but you can run code on a virtualized arduino too! You can view / modify the entire virtualized end result here.

For all the Arduino posts I make, I will assume you have wires, a breadboard to make the connections, and the device itself. I am using the Uno R3, which is only about $15. The rest of the hardware needed for this post comes with most Arduino starter kits, which are also inexpensive.
I will also assume you have the Arduino's IDE, which you can download for free from the Arduino website.

How does it work?

Step 1: The Piezo

Vibration is detected with a piezoelectric sensor. When the sensor is disturbed, it generates a small voltage between its two terminals. The Arduino can sense this voltage if the wires and software are configured correctly.

If you are following along, wire up the Arduino and breadboard like this. Note that all Black wires are connected to ground.

The Piezo has 2 terminals. One is connected to ground, the other is connected to port A0. The A0-A5 ports are Analog ports, which means they can detect any voltage between 0 and 5 volts. Digital ports can only detect voltage-high (5V) or voltage-low (0V).
A 1 million Ohm resistor allows the Piezo's value to be read clearly. (Why 1 million Ohms? Fricked if I know. I had to look up someone else's guide to wire the Piezo correctly)

To get a good understanding how it will read the Piezo, run this mini program on your Arduino.

int piezoPin = A0;
double voltageThreshold = 0.05;

void setup() {

  pinMode(piezoPin, INPUT);
  Serial.begin(9600);
}

void loop() {

    // Read the value of the Piezo
    int piezoADC = analogRead(piezoPin);
    float piezoV = piezoADC / 1023.0 * 5.0;

    // If the value exceeds the threshold, output to the console
    if (piezoV >= voltageThreshold) {

      String statement = "Hit detected with voltage:  " + String(piezoV);
      Serial.println(statement);
    }

    delay(50);
}
	    

If you tap the Piezo with your finger, the Arduino should read a voltage of around 0.2 V or so. Since this exceeds the voltageThreshold value, the IDE's console should display a message. Hitting it harder will momentarily read a higher voltage, up to 5V.

Step 2: Game Logic

The Piezo is an example of physical logic: Something that operates through real-world interaction. But almost all of the work is done by the finished product is through virtual logic- the code running inside the Arduino. The Arduino's incredible usefulness comes from its ability to seamlessly mix physical and virtual logic: To allow hardware to control software and vice versa.

Make sure the Arduino is powered off, then add the following components to the circuit. Don't remove the Piezo and wires from earlier.

To test your wiring, delete the previous code and run this instead. When you press the button, the LEDs should alternate between on and off. If you hold the button pressed, the LEDs will continue to oscillate- (10 times a second, because of the delay call at line 31).

int LED1_pin = 12;
int LED2_pin = 11;
int button_pin = 10;

boolean LED1isLit = true;

void setup() {

  pinMode(LED1_pin, OUTPUT);
  pinMode(LED2_pin, OUTPUT);
  pinMode(button_pin, INPUT_PULLUP);
}

void loop() {

  if (digitalRead(button_pin) == LOW) {

    LED1isLit = !LED1isLit;
  }

  if (LED1isLit) {

    digitalWrite(LED1_pin, HIGH);
    digitalWrite(LED2_pin, LOW);
  }
  else {
    digitalWrite(LED1_pin, LOW);
    digitalWrite(LED2_pin, HIGH);
  }

  delay(100);
}
	    

The max current through an Arduino port is 40 milliamps, but the recommended threshold is around 20 milliamps. When a digital port outputs an electric current with the digitalWrite command, it does so at 5 Volts.
5 Volts = 0.02 Amps * 250 Ohms, so a resistor of around 250 Ohms is needed between the port and ground. A more-common 220 Ohm resistor (Stripe pattern: Red Red Brown) will do the trick just fine.

Why doesn't the button need a resistor, you might ask? Look at line 11, how the pin mode is set to INPUT_PULLUP. According to the Arduino website , "There are 20K pullup resistors built into the Atmega chip that can be accessed from software. These built-in pullup resistors are accessed by setting the pinMode() as INPUT_PULLUP. This effectively inverts the behavior of the INPUT mode, where HIGH means the sensor is off, and LOW means the sensor is on."

Unlike ports 11 & 12 that feed a current to the LEDs, pin 10 is reading a voltage. When the button connected to pin 10 is pressed, pin 10 has a direct line to ground. Since the port is at 5V, ground is 0V, and the INTERNAL_PULLUP property gives around 20 KiloOhms of resistance, the current flowing out of pin 10 is tiny (less than a milliamp), and therefore in the safe range, even with no external resistor. With the LEDs, it is a different story. There is no OUTPUT_PULLUP pin mode, so an external resistor must be used to keep your board from getting toasty

Here is a good representation of an Internal pullup resistor from MakeBright.com .

When the switch is open (button unpressed), the pin reads as HIGH. When the switch is closed (button pressed), the current flows from an internal 5V source, through the internal resistor, and out the pint to Ground. This make the port read LOW.

Step 3: Player Interaction

The Player Interaction wiring uses a similar setup of buttons and LEDs as Game Control. Three buttons each connect an Arduino pin to ground. Three LEDS connect pins to a 220 Ohm resistor, then to ground. To keep the breadboard from getting cluttered, I wired the buttons to the north-facing ground strip.

I did not make test code for this section- if you wired part 2 correctly, you can probably do the same for this part. Instead, here is the code for the final product.


int player1ButtonPin = 5;
int player2ButtonPin = 6;
int player3ButtonPin = 7;
int resetButtonPin = 11;
int piezoPin = A0;

int player1LedPin = 2;
int player2LedPin = 3;
int player3LedPin = 4;
int gameStatusLedPin1 = 12;
int gameStatusLedPin2 = 13;

int winner = 0;
boolean player1disq = false;
boolean player2disq = false;
boolean player3disq = false;
boolean gameIsPlaying = true;
boolean ballHasHitTable = false;
double voltageThreshold = 0.05;

//============================================================================

void setup() {

  // Because there are no resistors used with the buttons, I use the port's internal pullup resistor with INPUT_PULLUP
  pinMode(player1ButtonPin, INPUT_PULLUP);
  pinMode(player2ButtonPin, INPUT_PULLUP);
  pinMode(player3ButtonPin, INPUT_PULLUP);
  pinMode(resetButtonPin, INPUT_PULLUP);
  pinMode(piezoPin, INPUT);

  pinMode(gameStatusLedPin1, OUTPUT);
  pinMode(gameStatusLedPin2, OUTPUT);
  pinMode(player1LedPin, OUTPUT);
  pinMode(player2LedPin, OUTPUT);
  pinMode(player3LedPin, OUTPUT);

  Serial.begin(9600);
}

//============================================================================

void loop() {

  if (gameIsPlaying) {

    // Read the value of the Piezo
    int piezoADC = analogRead(piezoPin);
    float piezoV = piezoADC / 1023.0 * 5.0;

    // If the value exceeds the threshold, the ball has hit the table
    if (!ballHasHitTable && piezoV >= voltageThreshold) {

      ballHasHitTable = true;
      String voltage = "Hit detected with voltage:  " + String(piezoV);
      Serial.println(voltage);
    }

    // check if any player's buttons have been pressed yet
    int player1Read = digitalRead(player1ButtonPin);
    int player2Read = digitalRead(player2ButtonPin);
    int player3Read = digitalRead(player3ButtonPin);

    // Determine if any players have won or been disqualified
    if (ballHasHitTable) {
      checkForWinner( player1Read, player2Read, player3Read);
    }
    else {
      checkForEarlyButtonPresses( player1Read, player2Read, player3Read);
    }
  }

  // Display the winning lights if there is a winner
  showWinnerLEDifNeeded();

  // Check if the reset button has been pressed
  int resetButtonRead = digitalRead(resetButtonPin);
  if (resetButtonRead == LOW) {

    resetGame();
  }

  showGameStatusLEDs();

  delay(50);
}

//============================================================================

void showGameStatusLEDs() {

  if (gameIsPlaying) {

    if (ballHasHitTable) {

      digitalWrite(gameStatusLedPin1, HIGH);
      digitalWrite(gameStatusLedPin2, LOW);
    }
    else {
      digitalWrite(gameStatusLedPin1, LOW);
      digitalWrite(gameStatusLedPin2, HIGH);
    }
  }
  else {
    digitalWrite(gameStatusLedPin1, LOW);
    digitalWrite(gameStatusLedPin2, LOW);
  }
}

//============================================================================

void showWinnerLEDifNeeded() {

  if (winner == 1) {
    digitalWrite(player1LedPin, HIGH);
  }
  else if (winner == 2) {
    digitalWrite(player2LedPin, HIGH);
  }
  else if (winner == 3) {
    digitalWrite(player3LedPin, HIGH);
  }
  else {
    digitalWrite(player1LedPin, LOW);
    digitalWrite(player2LedPin, LOW);
    digitalWrite(player3LedPin, LOW);
  }
}

//============================================================================

void checkForEarlyButtonPresses(int player1Read, int player2Read, int player3Read) {

  if (player1Read == LOW && !player1disq) {

    player1disq = true;
    Serial.println("player 1 too early");
  }
  if (player2Read == LOW && !player2disq) {

    player2disq = true;
    Serial.println("player 2 too early");
  }
  if (player3Read == LOW && !player3disq) {

    player3disq = true;
    Serial.println("player 3 too early");
  }
}

//============================================================================

void checkForWinner(int player1Read, int player2Read, int player3Read) {

  if (player1Read == LOW && !player1disq && winner == 0) {

    winner = 1;
    gameIsPlaying = false;
    Serial.println("player 1 wins");
  }
  else if (player2Read == LOW && !player2disq && winner == 0) {

    winner = 2;
    gameIsPlaying = false;
    Serial.println("player 2 wins");
  }
  else if (player3Read == LOW && !player3disq && winner == 0) {

    winner = 3;
    gameIsPlaying = false;
    Serial.println("player 3 wins");
  }
}

//============================================================================

void resetGame() {

  winner = 0;
  gameIsPlaying = true;
  ballHasHitTable = false;
  player1disq = false;
  player2disq = false;
  player3disq = false;

  Serial.println("reset game");
}

	    

Let's break down the code. Lines 2-12 assign names to each pin used, while lines 14-20 declare the variables needed to keep track of game state. Since this game relies on very simple logic, simple booleans can be used a lot here.

The setup method has 2 jobs: To configure all the hardware pins, and to open a Serial connection. The loop method is a bit lengthier, but it subdivides it's tasks to 5 simple helper methods

checkForWinner: called every loop run, but only after the piezo has detected the ball drop. It checks which player, if any, have pressed their button.

checkForEarlyButtonPresses: also called every loop run, but only if the piezo has not yet detected a drop. If this method detects that a player's button is pressed, that player's "disqualified" flag is set to true.

showWinnerLEDifNeeded: called every loop run

showGameStatusLEDs: called every loop run

resetGame: Only called when the reset button is pressed.

Here is the wiring diagram of the finished product. You didn't think my wires really looked that pretty, did you?

by Davis Burnside
Posted April 11 2018