Affiliate links help support this site at no extra cost to you.
Welcome to Day 25.
We have built radars, robots, and weather stations.
But if you look closely at your code, it’s probably… simple.
It runs top to bottom. It uses delay(1000).
It waits.
Real robots don’t wait.
Does your car’s airbag wait for the radio to finish changing the station? No.
To build professional-grade systems, you need to change how you think.
You need to stop writing “Linear Code” and start building State Machines.
Today, we build the ultimate test of logic: The Simon Says Game.
It requires memory (Arrays), Timing (Millis), and complex Logic (States).
The Trap: Why delay() is Evil
Imagine a chef.
He puts pasta in boiling water (10 mins) and stands there staring at it.
He does nothing else.
That is delay(600000).
It blocks the CPU. You cannot read buttons. You cannot blink LEDs. You are paralyzed.
The Solution: millis()
The chef looks at the clock. “Started at 1:00. It is now 1:05. Not ready yet.”
He chops onions while checking the clock periodically.
This is Non-Blocking Code.
Technically, millis() returns the number of milliseconds since the board turned on.
It is an unsigned long number (It gets really big).
We check: if (currentMillis - previousMillis >= interval).
If true? Do the thing.
If false? Continue looping. Do other things.
The Hardware: Building the Rig
This is our most complex circuit yet. It requires careful wiring.
Bill of Materials:
1x Piezo Buzzer: Passive (2-pin). For sound effects.
Breadboard & Jumpers: Lots of wires.
The Wiring Map:
We need 4 pairs of Inputs and Outputs.
LEDs:
Red Anode -> Pin 8 (via 220Ω Resistor).
Green Anode -> Pin 9 (via 220Ω Resistor).
Blue Anode -> Pin 10 (via 220Ω Resistor).
Yellow Anode -> Pin 11 (via 220Ω Resistor).
All Cathodes -> GND rail.
Buttons:
One leg -> Pins 2, 3, 4, 5.
Other leg -> GND (Common Ground).
Note: We will use INPUT_PULLUP mode in code.
Buzzer: Pin 12 to GND.
Start Button (Optional): Pin 6 to GND.
Wiring Tip: Use internal pull-ups for buttons to save resistors. Wire buttons to connect to GND.
The Architecture: Finite State Machines (FSM)
Don’t write one giant loop. Break your system into States.
For our game, we have 4 clear distinct modes:
IDLE: Waiting for start button.
PLAYBACK: Showing the sequence to the user.
INPUT: Waiting for user to press buttons.
GAME_OVER: You failed. Sad noise.
We use a “Switch Case” to jump between these tracks.
The Hardware: Building the Rig
We need 4 pairs of Inputs and Outputs.
LEDs: Red (Pin 8), Green (9), Blue (10), Yellow (11).
Buttons: Pins 2, 3, 4, 5.
Buzzer: Pin 12.
Start Button: Pin 6.
Wiring Tip: Use internal pull-ups for buttons to save resistors. Wire buttons to connect to GND.
The Code: Professional Structure
We are leaving “Spaghetti Code” behind.
We will use Enums to name our states.
Step 1: Defining the States
// Define the Statesenum GameState { IDLE, PLAYBACK, INPUT, GAME_OVER};GameState currentState = IDLE;// Hardware Definitionsconst int leds[] = {8, 9, 10, 11};const int buttons[] = {2, 3, 4, 5};const int buzzer = 12;// High Score Memoryint sequence[100]; // Max 100 turnsint turn = 0; // Current turn countint step = 0; // Player's step in current turnvoid setup() { Serial.begin(9600); // Initialize Pins using Loops (Professional Style) for (int i = 0; i < 4; i++) { pinMode(leds[i], OUTPUT); pinMode(buttons[i], INPUT_PULLUP); // Enable internal resistors } pinMode(buzzer, OUTPUT); pinMode(6, INPUT_PULLUP); // Start Button // True Randomness randomSeed(analogRead(A0)); Serial.println("System Ready. Press Start.");}
Step 2: The Main Loop (The “Brain”)
This is the beauty of FSM. The loop is clean.
It just asks: “What state am I in?” and delegates the work.
void loop() { switch (currentState) { case IDLE: // Pulse LEDs comfortably // Check for Start Button if (digitalRead(6) == LOW) { startGame(); } break; case PLAYBACK: playSequence(); currentState = INPUT; // Done playing, your turn! break; case INPUT: readPlayerInput(); break; case GAME_OVER: playLosingSound(); currentState = IDLE; break; }}
Deep Dive: Debouncing Buttons
When you press a metal button, it doesn’t just “click”.
Microscopically, it bounces thousands of times in 1 millisecond.
The Arduino is fast enough to count 50 clicks for one press.
This crashes games.
The Fix:
Hardware Debouncing: Add a capacitor (100nF) across the button legs. It acts like a shock absorber, smoothing out the voltage spikes.
Software Debouncing: Time. We tell the Arduino to ignore any change that happens faster than 50ms.
Read Button.
If changed? Wait 50ms.
Read Again.
Still pressed? Okay, it’s real.
// Simple Software Debounce Snippetif (digitalRead(btn) == LOW) { delay(50); // Wait for bounce to settle if (digitalRead(btn) == LOW) { // Valid Press! // Wait for release so we don't register 10 presses for one hold while(digitalRead(btn) == LOW); }}
Note: The while loop technically blocks code, but for a game like Simon where specific input is required, it is acceptable. In a flight controller, you would avoid this.
Implementing the Game Logic
This is where arrays shine.
We generate a random number (0-3) and store it in sequence[].
Each turn, we loop through the array.
The Playback Function
void playSequence() { delay(500); for (int i = 0; i < turn; i++) { int colorID = sequence[i]; digitalWrite(leds[colorID], HIGH); tone(buzzer, 200 + (colorID * 100)); // Different pitch for each color delay(300); digitalWrite(leds[colorID], LOW); noTone(buzzer); delay(100); }}
The Input Function (The Hard Part)
We need to wait for the user to press something.
If they press the WRONG button -> GAME_OVER.
If they press the RIGHT button -> Check next step.
If they finish the sequence -> Add new color, go back to PLAYBACK.
void readPlayerInput() { // Check all 4 buttons for (int i = 0; i < 4; i++) { if (digitalRead(buttons[i]) == LOW) { // Debounce here... // Did they act correctly? if (i == sequence[step]) { // Correct! step++; flashLED(i); // Feedback } else { // Wrong! currentState = GAME_OVER; return; } } } // Did they finish the turn? if (step >= turn) { turn++; // Level Up step = 0; // Reset steps sequence[turn-1] = random(0, 4); // Add new color currentState = PLAYBACK; }}// Helper Function to Flash LEDvoid flashLED(int id) { digitalWrite(leds[id], HIGH); tone(buzzer, 300 + (id * 200)); delay(200); digitalWrite(leds[id], LOW); noTone(buzzer); delay(100);}
Challenge: The “Evil” Mode
Once you have the basic game working, you can add “Modes”.
Speed Up: As turn increases, decrease the delay time in playSequence. Make it faster and faster!
int delayTime = 500 - (turn * 20); // Faster every turnif (delayTime < 100) delayTime = 100; // Cap limit
Reverse Mode: The player has to enter the sequence backwards.
Distraction Mode: Randomly flash a “dummy” LED during the player’s turn to confuse them (requires Non-Blocking code!).
Advanced: Adding “Millis” Timers
In the code above, I used delay(300) for simplicity.
To impress an interviewer, you would remove that.
You would have a variable lastFlashTime and check:
if (millis() - lastFlashTime > 300)
This allows you to animate a background “score counter” on an LCD while the game is playing.
Troubleshooting
Randomness isn’t Random:
Arduino’s random() is pseudo-random. It starts the same way every time.
Fix:randomSeed(analogRead(A0)); in setup. A0 (floating pin) provides noise entropy.
Buttons triggering twice:
Your debounce logic is too fast. Increase wait to 80ms.
Verify your button wiring. If floating (no pull-up), it acts as an antenna picking up static.
Low LED brightness:
Are you using 220Ω resistors? If you used 10kΩ by mistake, they will be very dim.
Buzzer clicking/weak:
Ensure you are using tone() for a passive buzzer. If using an active buzzer (one that beeps when given 5V), tone() won’t change the pitch well.
Game crashes after 100 turns:
We defined sequence[100]. If you go to turn 101, you write to memory that doesn’t belong to you (Buffer Overflow).
Fix: Add if (turn >= 99) winGame();
The Engineer’s Glossary (Day 25)
FSM (Finite State Machine): A computation model with a limited number of defined states and transitions (rules) for switching between them.
Blocking Code: Code that pauses the processor (e.g., delay()), preventing other tasks.
Non-Blocking Code: Code that checks conditions (e.g., millis()) and continues running, allowing multitasking.
Debounce: The process of filtering out mechanical noise from a switch to get a clean signal.
Enum: A user-defined data type consisting of named constants, making code readable (IDLE vs 0).
Conclusion
You have just built a State Machine.
Idle -> Playback -> Input -> Win/Loss.
This architecture is scalable.
You can add a “High Score” state. A “Cheat Mode” state.
You just draw a new circle on the chart and add a case in the switch.
This is how ATMs work. This is how vending machines work.
This is how code becomes reliable.
Next Up:
We have mastered logic. Now we need to talk to the outside world.
Not just blinking lights, but meaningful data.
Tomorrow, we learn Serial Communication properly.
Converting data types, parsing strings, and talking to Python.
See you on Day 26.
Your Arduino code is stuck in 1990. Learn how to use FreeRTOS to run multiple tasks simultaneously, ensuring your ESP8266 never freezes again. The ultimate guide to real-time operating systems.