arduino-juego-simon-dice

How to Create a Simon Says Game with Arduino

  • 9 min

The game Simon Says is a memory exercise based on repeating sequences of lights and sounds that grow with each round.

At first glance, it may seem like a simple project (turning on lights and reading buttons). However, under the hood, this project is the perfect excuse to introduce fundamental concepts in computer science such as dynamic Array management, generation of pseudo-random numbers, and, above all, designing Finite State Machines (FSM).

The goal is to create a device that generates a random sequence of lights and sounds, which the player must repeat. If they get it right, the sequence grows; if they fail, the game resets.

This project is ideal for consolidating the reading of digital inputs, the use of arrays, and flow control logic. At the end, we’ll see how to evolve it to use millis() without blocking the program.

The Logic by Phases

Before connecting a single wire, we need to think about the logic. A common mistake when starting out is trying to write all the code inside a giant loop full of if and delay. That is unsustainable.

The correct way to tackle a game like Simon is through a Finite State Machine. Our system will go through different clearly defined phases.

We can define the following states:

IDLE (Waiting): The game is idle, waiting for someone to press a button to start.

SHOWING (Displaying): Arduino plays the current sequence of lights and sounds. Here, the user only observes.

INPUT (Playing): Arduino waits for the user to press the sequence. Here is where we must read buttons and compare in real-time.

GAMEOVER (Loser): The user failed. We play an error sound, reset variables, and return to IDLE.

LEVEL_UP (Level completed): The user succeeded. We add one more step to the sequence and return to SHOWING.

This structure allows us to have organized and modular code. If we want to change the “Game Over” animation, we only touch that part of the code without breaking the button reading.

Components

  • [1x|blue] Arduino Uno | The brain of the project
  • [4x|green] LEDs of different colors | Indicate the sequence (Red, Green, Blue, Yellow)
  • [4x|yellow] 220Ω or 330Ω Resistors | Limit LED current
  • [4x|orange] Pushbuttons | Player input
  • [1x|purple] Passive Buzzer | Generates musical notes {passive}
  • [Various|gray] Breadboard and wires | Connections

For this setup, we will need very basic components. The interesting part here is the quantity, as we will manage 4 identical channels (one per color).

For the buttons, we will use Arduino’s internal Pull-up resistors. This saves us from placing 4 additional physical resistors on the breadboard, greatly simplifying the setup.

Connection Diagram

The setup is repetitive, which is good for practicing wiring order.

Color LEDs

We connect each LED with its series resistor. The resistor can go on the anode or the cathode, as it’s electrically in series either way.

[ { “from”: “Green LED + resistor”, “to”: “Pin 2”, “color”: “green”, “note”: “Anode to pin, cathode to GND” }, { “from”: “Yellow LED + resistor”, “to”: “Pin 3”, “color”: “yellow” }, { “from”: “Blue LED + resistor”, “to”: “Pin 4”, “color”: “blue” }, { “from”: “Red LED + resistor”, “to”: “Pin 5”, “color”: “red” }, { “from”: “LED Cathodes”, “to”: “Arduino GND”, “color”: “black” } ]

Pushbuttons

We will use INPUT_PULLUP, so each button is connected between the input pin and GND. The logic is inverted: LOW means pressed.

[ { “from”: “Button 1”, “to”: “Pin 6”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Button 2”, “to”: “Pin 7”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Button 3”, “to”: “Pin 8”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Button 4”, “to”: “Pin 9”, “color”: “orange”, “note”: “INPUT_PULLUP, other side to GND” }, { “from”: “Other side of buttons”, “to”: “Arduino GND”, “color”: “black” } ]

Buzzer

The buzzer will provide the sound feedback for each color.

[ { “from”: “Buzzer (+)”, “to”: “Pin 10”, “color”: “purple”, “note”: “Signal for tone()” }, { “from”: “Buzzer (-)”, “to”: “Arduino GND”, “color”: “black” } ]

It is recommended to use consecutive pins to facilitate the use of for loops in the initial setup.

The Code

The interesting part begins when we use arrays to map our hardware. This makes the code scalable. If tomorrow you want a 6-color Simon, you only change the array sizes.

Definitions and Global Variables

First, we define the pins and musical notes. Notice how we use constant arrays to associate LED i with button i and its tone i.

// --- Hardware Mapping ---
const int MAX_LEVEL = 100; // Maximum game level
const int NUM_COLORS = 4;

// Pins (Green, Yellow, Blue, Red)
const int ledPins[] = {2, 3, 4, 5};
const int buttonPins[] = {6, 7, 8, 9};
const int buzzerPin = 10;

// Frequencies for each color (Do, Mi, Sol, High Do)
const int gameTones[] = {261, 329, 392, 523};

// --- Game State Variables ---
int gameSequence[MAX_LEVEL]; // Array to store the sequence
int currentLevel = 0;        // Current level
Copied!

Setup (setup)

In the setup, we initialize the pins. The most important thing here is the randomSeed. If we don’t do this, the game will always generate the same sequence every time we turn it on, which would be very boring.

void setup() {
  Serial.begin(9600);
  
  for (int i = 0; i < NUM_COLORS; i++) {
    pinMode(ledPins[i], OUTPUT);
    pinMode(buttonPins[i], INPUT_PULLUP); // Important: Internal Pull-up
  }
  pinMode(buzzerPin, OUTPUT);
  
  // Random seed using noise from an unconnected analog pin
  randomSeed(analogRead(A0));
}
Copied!

The Main Loop and Logic

Here we implement the game logic. Instead of using a non-blocking state machine with switch, we simplify it into sequential functions within loop() to make it more readable.

The flow is:

  1. If we start at level 0 -> Generate initial sequence.
  2. Show sequence (showSequence).
  3. Read player attempt (readSequence).
  4. If successful -> Increase level and wait a bit.
  5. If failed -> Game Over and restart.
void loop() {
  if (currentLevel == 0) {
    // Game start (we could put an animation here)
    delay(1000);
    generateNextStep(); 
  }

  showSequence(); // Phase 1: Arduino shows
  
  if (readSequence()) { // Phase 2: Player repeats
    // If it returns true, the player successfully matched the whole sequence
    delay(1000); // Pause between levels
    if (currentLevel < MAX_LEVEL) {
      generateNextStep();
    } else {
      victory();
    }
  } else {
    // If it returns false, the player failed
    gameOver();
  }
}
Copied!

Auxiliary Functions

This is where we “do the heavy lifting”. We need functions to turn on lights with sound and to manage the logic.

1. Add a step to the sequence:

void generateNextStep() {
  if (currentLevel >= MAX_LEVEL) return;

  // Add a random color (0 to 3) to the end of the current sequence
  gameSequence[currentLevel] = random(0, NUM_COLORS);
  currentLevel++;
}
Copied!

The boundary check prevents writing outside the gameSequence array when exceeding level 100.

2. Show the sequence: We iterate through the array up to the current level, turning on the LED and playing the corresponding buzzer tone.

void showSequence() {
  for (int i = 0; i < currentLevel; i++) {
    int colorIndex = gameSequence[i];
    flashLED(colorIndex);
    delay(250); // Pause between lights
  }
}

void flashLED(int colorIndex) {
  digitalWrite(ledPins[colorIndex], HIGH);
  tone(buzzerPin, gameTones[colorIndex]);
  delay(300); // Duration of light/sound
  digitalWrite(ledPins[colorIndex], LOW);
  noTone(buzzerPin);
}
Copied!

3. Read player input (The critical part): This function must wait for the user to press a button, identify which one it is, turn on its light (visual feedback), and check if it matches the stored sequence.

bool readSequence() {
  for (int i = 0; i < currentLevel; i++) {
    int expectedButton = gameSequence[i]; // The color they should press
    int actualButton = -1;                // The one they actually press
    
    // Wait for a button press
    while (actualButton == -1) {
      for (int j = 0; j < NUM_COLORS; j++) {
        if (digitalRead(buttonPins[j]) == LOW) { // LOW because it's INPUT_PULLUP
          actualButton = j;
          flashLED(j); // Visual/sound feedback when pressed
          
          // Simple debounce: wait for release
          while (digitalRead(buttonPins[j]) == LOW) { delay(10); }
        }
      }
    }
    
    // Check if they got it right
    if (actualButton != expectedButton) {
      return false; // Failed
    }
  }
  return true; // Completed the sequence correctly
}
Copied!

4. Game Over: A small dramatic sound and light show to indicate the player has lost, and we reset the level to 0.

void gameOver() {
  // Low and long sound
  tone(buzzerPin, 150); 
  
  // Blink all LEDs
  for(int i=0; i<3; i++){
     for(int j=0; j<NUM_COLORS; j++) digitalWrite(ledPins[j], HIGH);
     delay(200);
     for(int j=0; j<NUM_COLORS; j++) digitalWrite(ledPins[j], LOW);
     delay(200);
  }
  noTone(buzzerPin);
  
  currentLevel = 0; // Restart game
  delay(1000);
}

void victory() {
  for (int i = 0; i < NUM_COLORS; i++) {
    flashLED(i);
  }
  currentLevel = 0;
  delay(1000);
}
Copied!

Possible Improvements

This code is a solid foundation, but it still allows for improvements. Here are some ideas to keep experimenting:

  • Increase speed: Make the delay inside showSequence smaller as currentLevel increases.
  • Low power mode: Put the Arduino to sleep if nothing is pressed for 30 seconds.
  • Save record: Use the EEPROM memory to save the high score even if the Arduino is turned off.