Build a BLE Simon Says Game

Wire an ESP32 memory game that sends scores to a browser via Bluetooth.

Introduction

30 Second Summary

Remember Simon Says? The classic game where you watch a sequence of lights and sounds, then try to repeat it from memory. Now imagine building one from real electronic components on your desk, then watching your score appear wirelessly on a web page in your browser.

In this project, you will build a physical Simon Says memory game on an ESP32 microcontroller. You will wire LEDs, buttons, and a buzzer, program the game logic, broadcast scores over Bluetooth Low Energy, and create a browser-based scoreboard that updates in real-time using the Web Bluetooth API.

What You'll Build

Four colored LEDs flash a pattern on your breadboard, you press buttons to repeat it, and the moment you slip up your score pops onto a leaderboard in Chrome without a single cable connecting the game to the screen.

By the end of this project, you'll have:

  • A physical Simon Says game with 4 colored LEDs, 4 buttons, and a buzzer that plays unique tones for each color and a game-over sound when you fail.
  • A complete game engine running on an ESP32 that generates random sequences, checks your button presses, advances levels on success, and ends the round on failure.
  • A live browser scoreboard powered by Web Bluetooth that receives scores wirelessly via Bluetooth Low Energy and ranks them in a leaderboard that updates the moment you lose a round.
  • Secret Mission: Add player name entry on the web page and multiplayer turn-taking so friends can compete on the same leaderboard.

Are there any prerequisites?

You need basic web development skills (HTML, CSS, and JavaScript). No prior ESP32 or Arduino experience is required.

This project requires purchasing hardware components (an ESP32 board, LEDs, buttons, a buzzer, a breadboard, and jumper wires). Step 1 covers exactly what to buy and how to wire everything together.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You're going to build a physical Simon Says memory game on an ESP32 that wirelessly sends your scores to a browser leaderboard over Bluetooth Low Energy. A standalone game can only show your score on the device itself. Adding a web interface means anyone nearby can watch scores update live, compare results, and turn a solo toy into a multiplayer experience.

Set Up Arduino IDE and Wire the Circuit

Before any code can run on your ESP32, you need two things: a working development environment on your PC and a physical circuit on a breadboard.

In this step, you will install the Arduino IDE, add the ESP32 board package, wire your LEDs, buttons, and buzzer, then upload a test sketch to confirm everything works end to end.

In this step, get ready to:
  • Install Arduino IDE and add the ESP32 board package.
  • Wire 4 LEDs, 4 buttons, and a buzzer to the ESP32 on a breadboard.
  • Upload a modified Blink sketch and confirm one LED blinks.
Install Arduino IDE and add the ESP32 board package

The Arduino IDE is a code editor that compiles and uploads programs to microcontroller boards like the ESP32.

  • Press the Windows key to open your search bar.
  • Type Arduino IDE and check what appears.

✔️ I see Arduino IDE 2.x

You already have a compatible version installed. Open it and skip ahead to adding the ESP32 board package below.

ⓧ I see Arduino IDE 1.x (the old version)

The legacy 1.x IDE does not support the ESP32 board package reliably. You need version 2.x.

  • Open your browser and go to https://www.arduino.cc/en/software.
  • Under DOWNLOAD OPTIONS, click Windows Win 10 and newer, 64 bits.
  • A donation page appears. Click JUST DOWNLOAD to proceed without donating.
  • Run the downloaded .exe file. The installer will ask whether to install for the current user or all users. Choose either option and follow the prompts.
  • When the installer finishes, leave Run Arduino IDE ticked and click Finish.

ⓧ Nothing comes up

Arduino IDE is not installed yet. Let's get it.

  • Open your browser and go to https://www.arduino.cc/en/software.
  • Under DOWNLOAD OPTIONS, click Windows Win 10 and newer, 64 bits.
  • A donation page appears. Click JUST DOWNLOAD to proceed without donating.
  • Run the downloaded .exe file. The installer will ask whether to install for the current user or all users. Choose either option and follow the prompts.
  • When the installer finishes, leave Run Arduino IDE ticked and click Finish.

You should now see the Arduino IDE open with a blank sketch.

What is Arduino IDE?

Arduino IDE is a free code editor designed for microcontrollers. You write sketches (programs) in C++, and the IDE compiles them and uploads the binary to your board over USB.

Version 2.3.10 is the latest release at the time of writing.

Now you need to tell the IDE how to compile code for the ESP32. Add the board package URL by following these steps:

  • In Arduino IDE, click File in the menu bar, then click Preferences.
  • Find the Additional boards manager URLs field near the bottom of the window.
  • Paste the following URL into that field:
https://dl.espressif.com/dl/package_esp32_index.json
  • Click OK to close Preferences.

What does this URL do?

This URL points to a package index file maintained by Espressif (the company that makes the ESP32). It tells the Arduino IDE where to download the ESP32 compiler toolchain and board definitions.

  • Open the Boards Manager by clicking Tools in the menu bar, then Board, then Boards Manager.
  • In the search field, type ESP32.
  • Find the entry labelled esp32 by Espressif Systems and click Install.
  • Wait for the installation to finish. This downloads around 1-2 GB of toolchains and may take several minutes.

Once the install completes, you will see a green INSTALLED label next to the esp32 entry.

Installation taking forever or failing?

The ESP32 package is large. If the download hangs, check your internet connection and try again. If you see a disk space error, free up at least 2 GB on your C: drive.

help me troubleshoot the ESP32 board package installation in Arduino IDE

Now connect your ESP32 DevKit to your PC with a USB cable. Plug the micro-USB end into the ESP32 board and the USB-A end into your PC.

  • In Arduino IDE, click Tools, then Board, then scroll to the esp32 section.
  • Select ESP32 Dev Module.
  • Click Tools, then Port. You should see a COM port listed (for example COM3 or COM4).
  • Select that COM port.

No COM port showing up?

Some ESP32 DevKits use a CH340 USB chip that needs a driver on Windows. Download the CH340 driver from the manufacturer's website, install it, then unplug and replug the ESP32.

help me fix my ESP32 not showing a COM port in Arduino IDE on Windows

Wire the LEDs, buttons, and buzzer on the breadboard

With the IDE ready, it is time to build the physical circuit. You will connect 4 LEDs, 4 push buttons, and a passive buzzer to specific GPIO pins on the ESP32.

What are GPIO pins?

GPIO stands for General Purpose Input/Output. These are the numbered pins on the ESP32 that you can control with code. You can set a pin HIGH (3.3V) to turn on an LED, or read a pin to detect whether a button is pressed.

Place your ESP32 DevKit on the breadboard so it straddles the center gap. The USB port should face outward for easy cable access.

Wire the 4 LEDs

Each LED needs a current-limiting resistor (220-330 ohm) in series to prevent it from burning out.

  • Connect the long leg (anode/positive) of LED 1 to GPIO 13 through a 220-330 ohm resistor.
  • Connect the short leg (cathode/negative) of LED 1 to the breadboard's GND rail.
  • Repeat for LED 2 on GPIO 12, LED 3 on GPIO 14, and LED 4 on GPIO 27.
  • Connect the breadboard's GND rail to a GND pin on the ESP32 using a jumper wire.

Use different colored LEDs

If you have red, green, blue, and yellow LEDs, assign one color per pin. This makes it much easier to tell which LED matches which button during gameplay.

Wire the 4 push buttons

The ESP32 has internal pull-up resistors, so you do not need external resistors for the buttons. Each button connects between its GPIO pin and GND.

  • Place button 1 on the breadboard. Connect one leg to GPIO 26 and the opposite leg to the GND rail.
  • Place button 2 with one leg on GPIO 25 and the other on GND.
  • Place button 3 with one leg on GPIO 33 and the other on GND.
  • Place button 4 with one leg on GPIO 32 and the other on GND.

Why no external pull-up resistors?

The code will use INPUT_PULLUP mode, which activates a resistor inside the ESP32. This keeps the pin HIGH when the button is not pressed, and reads LOW when you press the button (connecting the pin to GND).

Wire the buzzer
  • Connect the positive leg of the passive buzzer to GPIO 15.
  • Connect the negative leg to the GND rail.

Double-check that all GND connections route back to a GND pin on the ESP32. Your circuit is complete.

Upload the Blink test sketch

Time to prove the toolchain and circuit both work. You will upload a simple sketch that blinks the LED on GPIO 13 on and off every second.

  • In Arduino IDE, click File, then New Sketch.
  • Delete any existing code in the editor, then paste the following:
// Blink test sketch
const int ledPins[] = {13, 12, 14, 27};
void setup() {
  pinMode(ledPins[0], OUTPUT);
}
void loop() {
  digitalWrite(ledPins[0], HIGH);
  delay(1000);
  digitalWrite(ledPins[0], LOW);
  delay(1000);
}

What does this sketch do?

The ledPins array stores all 4 LED pin numbers. For now, the sketch only uses the first pin (GPIO 13). setup() sets that pin as an output.

Inside loop(), the pin goes HIGH (LED on) for one second, then LOW (LED off) for one second, repeating forever.

  • Save the sketch by pressing Ctrl+S. Name it simon_says_ble when prompted.
  • Confirm that ESP32 Dev Module is still selected under Tools > Board, and your COM port is selected under Tools > Port.

Before you upload, what do you think will happen on the breadboard if the code compiles and uploads successfully?

  • Click the Upload button (the right-pointing arrow in the toolbar). Wait for the console at the bottom to show Done uploading.

Look at your breadboard. The LED connected to GPIO 13 should now blink on and off every second.

LED not blinking?

Check that the LED's long leg connects (through the resistor) to GPIO 13, and the short leg connects to GND. Try reversing the LED if the polarity is wrong.

If the upload itself fails with a timeout, hold the BOOT button on the ESP32 while clicking Upload, then release it once you see "Connecting..." in the console.

help me fix my ESP32 Blink upload or LED not working

✔️ Awesome, I've got everything!

Great. Make sure you have saved simon_says_ble.ino and your LED is still blinking.

ⓧ I'd like to double check the full code

// Blink test sketch
const int ledPins[] = {13, 12, 14, 27};
void setup() {
  pinMode(ledPins[0], OUTPUT);
}
void loop() {
  digitalWrite(ledPins[0], HIGH);
  delay(1000);
  digitalWrite(ledPins[0], LOW);
  delay(1000);
}

Your development environment is set up and your circuit is alive. Next up, you will write code that makes every button light its matching LED and play a unique tone.

Make the Buttons and LEDs Respond

Your LED blinked, your upload pipeline works, and your circuit is wired. The toolchain from code to hardware is proven.

Before building any game logic, you need to confirm that each button correctly maps to its matching LED and plays a distinct tone. This I/O foundation is what the Simon Says game will rely on for every round.

In this step, get ready to:
  • Map each button press to its matching LED output.
  • Add distinct buzzer tones for each of the 4 buttons.
  • Verify all 4 button-LED-tone combinations work correctly.
Set up pin definitions and initialization

The Blink sketch was a one-off test that only knew about a single LED. Your new sketch needs to track all 4 LEDs, all 4 buttons, and the buzzer pin, then configure each one correctly at startup.

  • In Arduino IDE, select all the code in your current Blink sketch and delete it.
  • Replace it with the following pin definitions and setup() function:
#include <Arduino.h>

// Pin assignments for LEDs, buttons, and buzzer
const int ledPins[] = {13, 12, 14, 27};
const int buttonPins[] = {26, 25, 33, 32};
const int BUZZER_PIN = 15;
const int gameTones[] = {262, 330, 392, 523};

void setup() {
  // Configure all LED and button pins
  for (int i = 0; i < 4; i++) {
    pinMode(ledPins[i], OUTPUT);
    pinMode(buttonPins[i], INPUT_PULLUP);
    digitalWrite(ledPins[i], LOW);
  }
  pinMode(BUZZER_PIN, OUTPUT);
}

What does this code do?

  • ledPins[] and buttonPins[] store the GPIO numbers for your 4 LEDs and 4 buttons. Index 0 maps button 0 to LED 0, index 1 maps button 1 to LED 1, and so on.
  • gameTones[] holds the frequency in Hz for each button's buzzer sound (262, 330, 392, 523). These match the classic Simon game tones.
  • INPUT_PULLUP activates the ESP32's built-in pull-up resistor on each button pin. This means a pressed button reads LOW and an unpressed button reads HIGH.
  • digitalWrite(ledPins[i], LOW) ensures all LEDs start turned off.
Add the button-reading loop with LED and buzzer response

The setup() configured all pins, but the ESP32 does nothing in its main loop yet. You need a loop() function that continuously checks every button and responds with the matching LED and tone.

  • Add this loop() function below your setup() function:
void loop() {
  // Check each button and respond with matching LED + tone
  for (int i = 0; i < 4; i++) {
    if (digitalRead(buttonPins[i]) == LOW) {
      digitalWrite(ledPins[i], HIGH);
      tone(BUZZER_PIN, gameTones[i]);
    } else {
      digitalWrite(ledPins[i], LOW);
      noTone(BUZZER_PIN);
    }
  }
}

What does this code do?

  • The for loop runs through all 4 buttons on every cycle. The ESP32 executes this hundreds of times per second, making responses feel instant.
  • digitalRead(buttonPins[i]) == LOW detects a press. Because of INPUT_PULLUP, pressed means LOW.
  • tone(BUZZER_PIN, gameTones[i]) drives the buzzer at the frequency matching that button's color. noTone() silences it immediately on release.
  • Click the Upload button (right-arrow icon) in the Arduino IDE toolbar to compile and upload your sketch to the ESP32.
  • Wait for the console at the bottom to show "Done uploading." The previously blinking LED should now be off.

Seeing a compile error?

  • Check that you deleted ALL of the old Blink sketch code before pasting. Leftover lines cause duplicate definition errors.
  • Make sure loop() appears below setup(). Both functions are required for any Arduino sketch to compile.
  • Confirm the board is still set to ESP32 Dev Module under Tools > Board and the correct COM port is selected.

Help me fix my Arduino compile error for the button-LED sketch.

Test all 4 button-LED-tone combinations

The sketch is running. Before you move on, you need to prove that every single channel works. A broken wire or wrong pin would silently fail later in the game.

  • Before pressing anything, predict: when you hold button 1, which LED should light up, and what pitch should you hear?
  • Press and hold each of the 4 buttons one at a time.

Each button lights its matching LED and plays a distinct tone through the buzzer. Releasing the button turns the LED off and silences the buzzer. Button 1 plays the lowest pitch (262 Hz) and button 4 plays the highest (523 Hz).

What is INPUT_PULLUP?

INPUT_PULLUP enables a resistor inside the ESP32 that holds the pin at HIGH voltage when the button is not pressed. Pressing the button connects the pin to ground, pulling it LOW. This is why the code checks for LOW to detect a press.

One button doesn't light its LED?

  • Check that the button's jumper wire goes to the correct GPIO pin. Button 1 should connect to GPIO 26, button 2 to GPIO 25, button 3 to GPIO 33, and button 4 to GPIO 32.
  • Check that the matching LED is wired to the correct pin. LED 1 should connect to GPIO 13, LED 2 to GPIO 12, LED 3 to GPIO 14, and LED 4 to GPIO 27.
  • Make sure the button legs bridge the center channel of the breadboard. If both legs are on the same side, the button is always connected.
  • If the buzzer stays on constantly, check that one of your button wires is not loose and floating LOW.

Help me debug my ESP32 button and LED wiring.

✔️ Awesome, I've got everything!

Great. Double check you've saved your sketch file in Arduino IDE.

ⓧ I'd like to double check the full code

Here is the complete simon_says_ble.ino at this point. Compare yours against it:

#include <Arduino.h>

const int ledPins[] = {13, 12, 14, 27};
const int buttonPins[] = {26, 25, 33, 32};
const int BUZZER_PIN = 15;
const int gameTones[] = {262, 330, 392, 523};

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(ledPins[i], OUTPUT);
    pinMode(buttonPins[i], INPUT_PULLUP);
    digitalWrite(ledPins[i], LOW);
  }
  pinMode(BUZZER_PIN, OUTPUT);
}

void loop() {
  for (int i = 0; i < 4; i++) {
    if (digitalRead(buttonPins[i]) == LOW) {
      digitalWrite(ledPins[i], HIGH);
      tone(BUZZER_PIN, gameTones[i]);
    } else {
      digitalWrite(ledPins[i], LOW);
      noTone(BUZZER_PIN);
    }
  }
}

Every button lights its LED and plays its tone. Your hardware I/O layer is fully confirmed. Next up, you'll turn these button presses into an actual Simon Says game with random sequences, playback, and scoring.

Build the Simon Says Game Logic

Your hardware layer is solid. Every button lights its LED and plays its tone. Now it is time to build the actual game on top of that foundation.

A Simon Says game needs three abilities: generating a random sequence of colors, playing that sequence back to the player with lights and sounds, and checking whether the player repeats it correctly. You will build all three in this step, turning your ESP32 from a button tester into a full memory game.

In this step, get ready to:
  • Add game state variables and random sequence generation to the sketch.
  • Build playback functions that flash LEDs and play tones in sequence.
  • Implement input checking that advances levels on success and triggers game over on failure.
Add game state and sequence generation

The game needs to track a growing sequence of random colors, the current level, and whether a round is in progress. You will replace the existing button-response sketch with a new version that sets up these state variables and initializes the random number generator.

  • In Arduino IDE, select all the code in simon_says_ble.ino (Ctrl+A) and delete it.
  • Replace it with this new game foundation by pasting the following code:
#include <Arduino.h>

// Pin definitions
const int ledPins[] = {13, 12, 14, 27};
const int buttonPins[] = {26, 25, 33, 32};
const int BUZZER_PIN = 15;

// Game tones (Hz) for each color
const int gameTones[] = {262, 330, 392, 523};

// Game state
#define MAX_LEVEL 100
int gameSequence[MAX_LEVEL];
int currentLevel = 0;
bool gameActive = false;

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 4; i++) {
    pinMode(ledPins[i], OUTPUT);
    pinMode(buttonPins[i], INPUT_PULLUP);
    digitalWrite(ledPins[i], LOW);
  }
  pinMode(BUZZER_PIN, OUTPUT);
  randomSeed(analogRead(0));
  Serial.println("Simon Says ready! Press any button to start.");
}

void loop() { }

What does this code do?

The gameSequence array stores up to 100 random LED indices (0-3), one per level. currentLevel tracks how far the player has progressed, and gameActive flags whether a round is in progress.

The randomSeed(analogRead(0)) reads electrical noise from an unconnected analog pin to seed the random number generator. This ensures the game produces a different sequence every time you power on.

  • Click the Upload button (right arrow icon in the top toolbar) to flash this sketch to your ESP32.
  • Open the Serial Monitor by clicking the magnifying glass icon in the top-right toolbar, or press Ctrl+Shift+M.
  • Set the baud rate dropdown to 115200 (top-right corner of the Serial Monitor pane).

You should see Simon Says ready! Press any button to start. printed in the Serial Monitor.

Don't see the message?

  • Check that the baud rate is set to 115200 in the Serial Monitor dropdown. A mismatch produces garbled text.
  • Press the EN (reset) button on your ESP32 board to restart the sketch and re-print the message.
  • Make sure the correct COM port is selected under Tools then Port.

Still stuck? Why isn't my Serial Monitor showing any output from my ESP32?

Add sequence playback

The game needs to show the player which pattern to repeat. You will add two functions: one that flashes a single LED with its matching tone, and another that plays the entire sequence in order.

  • In simon_says_ble.ino, find the line void loop() { } at the bottom of your file.
  • Add the following two functions above that line:
void flashLED(int index) {
  // Light the LED and play its tone
  digitalWrite(ledPins[index], HIGH);
  tone(BUZZER_PIN, gameTones[index]);
  delay(300);
  digitalWrite(ledPins[index], LOW);
  noTone(BUZZER_PIN);
  delay(50);
}

void playSequence() {
  // Play back the full pattern for the player to memorize
  delay(500);
  for (int i = 0; i < currentLevel; i++) {
    flashLED(gameSequence[i]);
    delay(100);
  }
}

What does this code do?

The flashLED() function lights one LED for 300ms while playing its corresponding tone through the buzzer. The 50ms gap at the end separates consecutive flashes so the player can distinguish them.

The playSequence() function loops through every entry in the game sequence up to the current level, calling flashLED() for each one. The initial 500ms pause gives the player a moment to get ready before the pattern starts.

Next you need functions that read button presses and compare them against the stored sequence.

  • Below playSequence() and still above void loop() { }, add the input handling functions:
int waitForButton() {
  // Block until a button is pressed, return its index (0-3)
  while (true) {
    for (int i = 0; i < 4; i++) {
      if (digitalRead(buttonPins[i]) == LOW) {
        delay(50);
        if (digitalRead(buttonPins[i]) == LOW) {
          // Feedback: light LED and play tone
          digitalWrite(ledPins[i], HIGH);
          tone(BUZZER_PIN, gameTones[i]);
          delay(200);
          digitalWrite(ledPins[i], LOW);
          noTone(BUZZER_PIN);
          while (digitalRead(buttonPins[i]) == LOW) { delay(10); }
          delay(50);
          return i;
        }
      }
    }
    delay(10);
  }
}

bool checkPlayerInput() {
  // Compare each player press against the stored sequence
  for (int i = 0; i < currentLevel; i++) {
    int pressed = waitForButton();
    if (pressed != gameSequence[i]) return false;
  }
  return true;
}

What does this code do?

The waitForButton() function loops until a button is pressed. It reads the press twice with a 50ms gap (debouncing) to avoid registering a single press as multiple inputs. When a valid press is detected, it gives immediate LED and tone feedback so the player knows it registered.

The checkPlayerInput() function calls waitForButton() once per sequence entry and compares it against the stored pattern. If any press is wrong, it returns false immediately.

  • Upload the sketch to confirm it compiles without errors.

The upload should succeed. The Serial Monitor still shows the ready message because the empty loop() does nothing yet.

Seeing a compile error?

  • Check that flashLED(), playSequence(), waitForButton(), and checkPlayerInput() are all placed above void loop() { }. Functions must be defined before they are called in Arduino sketches.
  • Verify you have matching curly braces for every function. A missing } at the end of waitForButton() is a common miss.

Help me fix this compile error in my Simon Says Arduino sketch.

Add game over, feedback, and the main game loop

You have the building blocks for playing sequences and reading input. Now you need the game-over behavior, level-up feedback, and the main loop that ties everything together.

  • Below checkPlayerInput() and above void loop() { }, add the game-over and feedback functions:
void gameOver() {
  Serial.print("Game Over! Score: ");
  Serial.println(currentLevel - 1);
  // Flash all LEDs and buzz 3 times
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) digitalWrite(ledPins[j], HIGH);
    tone(BUZZER_PIN, 100);
    delay(200);
    for (int j = 0; j < 4; j++) digitalWrite(ledPins[j], LOW);
    noTone(BUZZER_PIN);
    delay(200);
  }
  currentLevel = 0;
  gameActive = false;
}

void levelUpFeedback() {
  // Quick flash all LEDs with a high tone
  for (int j = 0; j < 4; j++) digitalWrite(ledPins[j], HIGH);
  tone(BUZZER_PIN, 880);
  delay(150);
  for (int j = 0; j < 4; j++) digitalWrite(ledPins[j], LOW);
  noTone(BUZZER_PIN);
}

What does this code do?

The gameOver() function prints the final score to Serial Monitor, then flashes all 4 LEDs with a low 100Hz buzz three times as a dramatic fail signal. It resets the game state so the player can start fresh.

The levelUpFeedback() function gives a quick celebratory flash of all LEDs with a high 880Hz tone when the player successfully repeats a sequence.

  • Below levelUpFeedback() and still above void loop() { }, add the button-start detection function:
bool anyButtonPressed() {
  // Check if any button is pressed (used to start the game)
  for (int i = 0; i < 4; i++) {
    if (digitalRead(buttonPins[i]) == LOW) {
      delay(50);
      if (digitalRead(buttonPins[i]) == LOW) {
        while (digitalRead(buttonPins[i]) == LOW) { delay(10); }
        return true;
      }
    }
  }
  return false;
}

What does this code do?

The anyButtonPressed() function checks all 4 buttons and returns true if any one is pressed. The game uses this to wait for the player to start a new round. It includes debouncing and waits for the button to be released before returning.

  • Upload the sketch to confirm everything compiles correctly with the new functions.

The upload should succeed again. Your helper functions are all in place. The last piece is the main game loop.

  • Find the line void loop() { } at the bottom of your file.
  • Delete it and replace it with the full game loop:
void loop() {
  // Wait for a button press to start the game
  if (!gameActive) {
    if (anyButtonPressed()) {
      gameActive = true;
      currentLevel = 0;
      Serial.println("Game started!");
    }
    return;
  }

  // Add a new random color to the sequence
  gameSequence[currentLevel] = random(0, 4);
  currentLevel++;
  if (currentLevel >= MAX_LEVEL) currentLevel = MAX_LEVEL;

  // Play the sequence, then check the player's input
  playSequence();
  if (checkPlayerInput()) {
    levelUpFeedback();
    Serial.print("Level up! Now at level: ");
    Serial.println(currentLevel);
    delay(300);
  } else {
    gameOver();
  }
}

What does this code do?

The loop() function runs continuously. When no game is active, it waits for any button press to start a new round.

Once active, each iteration adds a random LED index (0-3) to the sequence, plays the full pattern back, then waits for the player to repeat it. A correct repetition triggers levelUpFeedback() and advances the level. A wrong press triggers gameOver() and resets.

Before you upload, think about what you expect to happen when you press a button. Will the ESP32 flash one LED, or will the game immediately show a longer sequence?

  • Upload the sketch to your ESP32.
  • Open the Serial Monitor (Ctrl+Shift+M) at 115200 baud.
  • Press any button on your breadboard to start the game.

The ESP32 flashes one LED. Press the matching button. The Serial Monitor prints Level up! Now at level: 1 and the sequence grows to two LEDs.

  • Deliberately press the wrong button when the sequence plays.

All 4 LEDs flash with a low buzz three times, and the Serial Monitor prints Game Over! Score: X where X is the level you reached.

Game not responding to button presses?

  • If the Serial Monitor shows Simon Says ready! but pressing a button does nothing, check that your button wires are firmly seated in the breadboard. A loose jumper wire breaks the circuit.
  • If the game starts but immediately shows Game Over! Score: 0, the button might be bouncing. Ensure the debounce delay(50) is present in waitForButton().
  • If LEDs flash but no tone plays, verify the buzzer wire connects to GPIO 15 and ground.

My Simon Says game isn't working as expected after uploading. Help me debug.

✔️ Awesome, I've got everything!

Great. Double check you have saved simon_says_ble.ino and that your game runs correctly on the breadboard.

ⓧ I'd like to double check the full code

Scroll up and confirm your simon_says_ble.ino contains these sections in order:

  • Pin definitions, tone array, and game state variables (MAX_LEVEL, gameSequence, currentLevel, gameActive).
  • The setup() function with Serial.begin(115200), pin initialization, randomSeed(analogRead(0)), and the ready message.
  • The flashLED() function.
  • The playSequence() function.
  • The waitForButton() function with debounce logic.
  • The checkPlayerInput() function.
  • The gameOver() function.
  • The levelUpFeedback() function.
  • The anyButtonPressed() function.
  • The loop() function that manages game flow (wait for start, generate sequence, play it, check input, level up or game over).

Your ESP32 is now a fully playable Simon Says memory game. Next up, you will add Bluetooth Low Energy so the game can broadcast scores wirelessly to a web page.

Add Bluetooth Low Energy Score Broadcasting

Your Simon Says game works perfectly as a standalone toy. Press buttons, watch LEDs, hear tones, and see your score in the Serial Monitor.

The problem is that the score disappears the moment you close the Serial Monitor. There is no way for anyone else to see how well you did unless they are staring at your laptop screen. Bluetooth Low Energy (BLE) solves this by broadcasting your score wirelessly so a web page can pick it up from across the room.

In this step, get ready to:
  • Set up a BLE GATT service with a score characteristic that supports notifications.
  • Send the player's score over BLE whenever a game ends.
  • Start BLE advertising so the device appears as "SimonSays" to nearby scanners.
Add BLE includes and declare BLE objects

Before the ESP32 can act as a BLE server, you need to include the BLE libraries and declare the global objects that manage the connection and data.

  • In Arduino IDE, open your simon_says_ble.ino sketch.
  • Find the #include <Arduino.h> line at the very top of your file.
  • Add the following BLE library includes directly below it:
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
  • Now scroll down past your existing game state variables (gameSequence, currentLevel, gameActive).
  • Add the following BLE global variables, UUIDs, and server callbacks class below the game state variables:
// BLE objects
BLEServer *pServer = NULL;
BLECharacteristic *pScoreCharacteristic = NULL;
bool deviceConnected = false;
bool oldDeviceConnected = false;
String playerName = "Player";

// BLE UUIDs
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define SCORE_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

// BLE Server Callbacks
class MyServerCallbacks : public BLEServerCallbacks {
  void onConnect(BLEServer *pServer) {
    deviceConnected = true;
    Serial.println("Device connected");
  }
  void onDisconnect(BLEServer *pServer) {
    deviceConnected = false;
    Serial.println("Device disconnected");
  }
};

What does this code do?

The four #include lines pull in the ESP32's built-in BLE library. This library handles all the low-level Bluetooth radio communication for you.

The global variables track the BLE server (pServer), the characteristic that holds the score (pScoreCharacteristic), and whether a device is currently connected (deviceConnected).

The two UUID strings uniquely identify your BLE service and its score characteristic. Any device that knows these UUIDs can find and subscribe to your game's score data.

The MyServerCallbacks class updates the deviceConnected flag whenever a phone or computer connects or disconnects from the ESP32.

Initialize BLE and start advertising in setup()

With the declarations in place, you need to initialize the BLE radio, create the GATT service, attach the score characteristic, and start advertising so other devices can discover "SimonSays".

  • Find your setup() function.
  • Locate the line pinMode(BUZZER_PIN, OUTPUT); inside setup().
  • Add the following BLE initialization code directly below that line, replacing the existing randomSeed and Serial.println lines at the end of setup():
  // Initialize BLE
  BLEDevice::init("SimonSays");
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Create BLE Service
  BLEService *pService = pServer->createService(SERVICE_UUID);

  // Create Score Characteristic (NOTIFY)
  pScoreCharacteristic = pService->createCharacteristic(
    SCORE_CHAR_UUID,
    BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY
  );
  pScoreCharacteristic->addDescriptor(new BLE2902());

  // Start service and advertising
  pService->start();
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pAdvertising->setScanResponse(true);
  BLEDevice::startAdvertising();
  Serial.println("BLE advertising started - device name: SimonSays");

  // Seed random
  randomSeed(analogRead(0));
  Serial.println("Simon Says ready! Press any button to start.");

What does this code do?

  • BLEDevice::init("SimonSays") starts the Bluetooth radio and names your device "SimonSays" so it is identifiable in Bluetooth scans.
  • createServer() makes the ESP32 a BLE peripheral (server) that other devices can connect to.
  • createService(SERVICE_UUID) defines a GATT service. Think of it as a folder that groups related data together.
  • createCharacteristic() with PROPERTY_NOTIFY creates a score value that automatically pushes updates to connected devices.
  • addDescriptor(new BLE2902()) adds the standard descriptor that lets clients enable or disable notifications.
  • The advertising lines make "SimonSays" visible to nearby Bluetooth scanners.
  • Click the Upload button (right-arrow icon) in the Arduino IDE toolbar to compile and upload to your ESP32.
  • Open the Serial Monitor by clicking Tools in the menu bar, then Serial Monitor.
  • Make sure the baud rate is set to 115200.

You should see two lines print after the ESP32 resets:

BLE advertising started - device name: SimonSays followed by Simon Says ready! Press any button to start.

Not seeing BLE advertising started?

  • Check that your board is still set to ESP32 Dev Module under Tools > Board. A wrong board selection can cause BLE compilation errors.
  • If you see a compilation error mentioning BLEDevice.h, make sure you are using the ESP32 board package (not a generic Arduino board). The BLE libraries come bundled with the ESP32 package.
  • Confirm the Serial Monitor baud rate matches 115200. A mismatch shows garbled text.

Still stuck? Why isn't my ESP32 BLE sketch compiling or printing to Serial Monitor?

Send the score over BLE when the game ends

The ESP32 is now advertising over BLE, but it is not sending any data yet. You need to push the score to connected devices inside the gameOver() function.

  • Find your gameOver() function.
  • Locate the line noTone(BUZZER_PIN); near the end of the game-over flash loop (the second one inside the for loop).
  • Add the following BLE notification code below the closing brace of the for loop, just above currentLevel = 0;:
  // Send score via BLE
  if (deviceConnected) {
    String scoreData = playerName + ":" + String(currentLevel - 1);
    pScoreCharacteristic->setValue(scoreData.c_str());
    pScoreCharacteristic->notify();
    Serial.print("Score sent via BLE: ");
    Serial.println(scoreData);
  }

What does this code do?

When a game ends, this block checks if a BLE client is connected. If so, it builds a string like Player:3 combining the player name and score. It sets that string as the characteristic's value, then calls notify() to push the update to the connected device instantly.

If no device is connected, the if-block is skipped and the game just resets as before.

  • Click Upload to flash the updated sketch.
  • Open the Serial Monitor.
  • Press a button to start a game, then deliberately press the wrong button to trigger a game over.

You should see Game Over! Score: X in the Serial Monitor. You will not see the "Score sent via BLE" message yet because no device is connected. That is expected.

Game not responding after upload?

  • Press the physical reset button on the ESP32 DevKit after uploading. BLE initialization can occasionally hold the startup.
  • If you see a compilation error, make sure your if (deviceConnected) block is inside gameOver() and all braces are balanced.

Still stuck? Why isn't my game over code working after adding BLE notifications?

Handle BLE reconnection in loop()

When a client disconnects, the ESP32 stops advertising by default. You need to restart advertising after a disconnection so the device remains discoverable for the next connection.

  • Find your loop() function.
  • Add the following reconnection logic at the very top of loop(), before the existing if (!gameActive) check:
  // Handle BLE reconnection
  if (!deviceConnected && oldDeviceConnected) {
    delay(500);
    pServer->startAdvertising();
    Serial.println("Restarted advertising");
    oldDeviceConnected = false;
  }
  if (deviceConnected && !oldDeviceConnected) {
    oldDeviceConnected = true;
  }

What does this code do?

The first if-block detects when a device has just disconnected (deviceConnected flipped to false while oldDeviceConnected is still true). It waits briefly, then restarts advertising so the ESP32 becomes discoverable again.

The second if-block updates oldDeviceConnected when a new connection is established. Together they form a simple state machine that keeps BLE ready for the next connection at all times.

  • Click Upload to flash the final version of this step.
  • Open the Serial Monitor and confirm you see BLE advertising started - device name: SimonSays on boot.

Before you test the full flow, what do you think will happen when you play a round and lose? Will you see the "Score sent via BLE" message in the Serial Monitor?

  • Press any button to start a game.
  • Deliberately press the wrong button to trigger game over.

You will see Game Over! Score: X but no "Score sent via BLE" line. That is correct. No device is connected yet, so the if (deviceConnected) check skips the notification. The full browser connection comes in the next step.

Why no BLE score message yet?

The ESP32 is advertising and ready to accept connections, but nobody has connected to it yet. The score notification only fires when deviceConnected is true. In the next step, you will build a web page that connects via Web Bluetooth, and then you will see scores flow wirelessly to the browser.

Seeing compilation errors after adding reconnection code?

  • Make sure the reconnection block is inside loop() and above the if (!gameActive) check.
  • Verify you have matching opening and closing braces for loop(). A missing brace is the most common cause of errors when adding code mid-function.

Still stuck? Help me fix compilation errors in my BLE reconnection code.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file.

ⓧ I'd like to double check the full code

Your simon_says_ble.ino is now quite long. Scroll through your file and confirm these sections exist in order:

  • BLE includes (BLEDevice.h, BLEServer.h, BLEUtils.h, BLE2902.h) right after #include <Arduino.h>.
  • Pin definitions and game state variables (unchanged from before).
  • BLE global objects (pServer, pScoreCharacteristic, deviceConnected, oldDeviceConnected, playerName).
  • UUID defines (SERVICE_UUID and SCORE_CHAR_UUID).
  • MyServerCallbacks class with onConnect and onDisconnect.
  • setup() with BLE initialization, service and characteristic creation, and advertising start.
  • gameOver() with the if (deviceConnected) block that sends the score.
  • loop() starting with the reconnection handling, followed by the existing game logic.

Your ESP32 is now a BLE peripheral broadcasting game scores to anyone who connects. Next up, you will build a web page that connects to "SimonSays" over Web Bluetooth and displays a live leaderboard.

Build the Web Bluetooth Scoreboard

Your ESP32 is broadcasting scores over Bluetooth Low Energy every time a game ends. Right now, those scores only appear in the Serial Monitor over a USB cable.

In this step, you will build a web page that connects wirelessly to the ESP32 using the Web Bluetooth API. The page receives score notifications in real time and displays them in a ranked leaderboard. This is the bridge between your physical hardware and web software.

In this step, get ready to:
  • Create a styled HTML page with a Connect button, status indicator, and leaderboard section.
  • Write JavaScript that uses Web Bluetooth to connect to the SimonSays device.
  • Subscribe to BLE notifications and render incoming scores in a ranked table with a gold highlight for the top scorer.
Create the HTML page structure and styling

The scoreboard page needs a dark-themed layout with a Connect button, a connection status label, and a leaderboard panel. You will write all the HTML and CSS first so you can see the page visually before adding any JavaScript.

Why does Web Bluetooth need localhost?

The Web Bluetooth API only works in a secure context. That means HTTPS or localhost. Opening the HTML file directly (file://) will silently block Bluetooth access. You will use the VS Code Live Server extension to serve your page on localhost so Chrome allows the connection.

  • In VS Code, click the Extensions icon in the left sidebar (or press Ctrl+Shift+X).
  • Search for Live Server and install the extension by Ritwick Dey.
  • Create a new folder on your Desktop called simon-scoreboard.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder and select the simon-scoreboard folder you just created.
  • Create a new file called index.html inside this folder.
  • Paste the following into index.html to set up the page head with all the styling:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Simon Says Scoreboard</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 2rem; }
    h1 { font-size: 2rem; margin-bottom: 1.5rem; color: #00d4ff; }
    .controls { display: flex; gap: 1rem; margin-bottom: 2rem; flex-wrap: wrap; justify-content: center; }
    button { padding: 0.75rem 1.5rem; font-size: 1rem; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; transition: transform 0.1s; }
    button:active { transform: scale(0.95); }
    #connectBtn { background: #00d4ff; color: #1a1a2e; }
    #connectBtn:disabled { background: #555; color: #999; cursor: not-allowed; }
    #status { margin-bottom: 1.5rem; padding: 0.5rem 1rem; border-radius: 4px; font-size: 0.9rem; }
    .connected { background: #0d4d0d; color: #4cff4c; }
    .disconnected { background: #4d0d0d; color: #ff4c4c; }
    .leaderboard { width: 100%; max-width: 500px; background: #16213e; border-radius: 12px; padding: 1.5rem; }
    .leaderboard h2 { text-align: center; margin-bottom: 1rem; color: #ffd700; }
    table { width: 100%; border-collapse: collapse; }
    th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #2a2a4a; }
    th { color: #aaa; font-size: 0.85rem; text-transform: uppercase; }
    tr:first-child td { color: #ffd700; font-weight: 700; }
    .no-scores { text-align: center; color: #666; padding: 2rem; }
  </style>
</head>

What does this code do?

  • The <style> block defines a dark theme (#1a1a2e background) with centered layout using flexbox.
  • The #connectBtn styles create a bright cyan button. When disabled after connecting, it greys out.
  • The .connected and .disconnected classes toggle the status indicator between green and red.
  • The tr:first-child td rule highlights the top-scoring row in gold. This is the "highest score" visual treatment.
  • Directly below the </head> tag you just pasted, add the body structure with an empty script tag:
<body>
  <h1>Simon Says Scoreboard</h1>
  <div class="controls">
    <button id="connectBtn">Connect to Game</button>
  </div>
  <div id="status" class="disconnected">Not connected</div>
  <div class="leaderboard">
    <h2>Leaderboard</h2>
    <div id="scoreList"><p class="no-scores">Connect to the game and play a round to see scores here.</p></div>
  </div>
  <script>
  </script>
</body>
</html>

What does this code do?

  • The connectBtn button is what the learner clicks to trigger the Bluetooth pairing flow.
  • The status div starts with class disconnected (red styling) and switches to connected (green) once Bluetooth links up.
  • The scoreList div is where JavaScript will inject the leaderboard table later.
  • The empty <script> tag is a placeholder for the JavaScript you will add next.
  • Save index.html.
  • Right-click index.html in the VS Code Explorer sidebar and select Open with Live Server.
  • Chrome opens automatically at http://127.0.0.1:5500 (or a similar localhost port). You should see a dark page with the title "Simon Says Scoreboard", a cyan "Connect to Game" button, a red "Not connected" label, and a leaderboard section with placeholder text.

Don't see the page in Chrome?

  • Make sure you opened a folder in VS Code (not just the single file). Live Server needs a workspace folder to serve.
  • If the page opened in a different browser, copy the URL and paste it into Chrome. Web Bluetooth only works in Chrome.
  • Check the bottom-right of VS Code for the Go Live button. If it is missing, restart VS Code after installing the Live Server extension.

Still stuck? Help me get Live Server running in VS Code so I can serve my index.html on localhost.

Add the Web Bluetooth connection logic

The Connect button needs to trigger Chrome's Bluetooth device picker, find the SimonSays device, and establish a GATT connection so the page can read characteristics from the ESP32.

  • In index.html, find the empty <script> tag. Place the following code between <script> and </script>:
    // BLE UUIDs matching the ESP32 sketch
    const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b';
    const SCORE_CHAR_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8';
    const connectBtn = document.getElementById('connectBtn');
    const statusEl = document.getElementById('status');
    const scoreListEl = document.getElementById('scoreList');
    let scores = [];
    let device = null;
    // Connect button triggers the Web Bluetooth device picker
    connectBtn.addEventListener('click', async () => {
      try {
        device = await navigator.bluetooth.requestDevice({ filters: [{ services: [SERVICE_UUID] }], optionalServices: [SERVICE_UUID] });
        device.addEventListener('gattserverdisconnected', onDisconnected);
        const server = await device.gatt.connect();
        const service = await server.getPrimaryService(SERVICE_UUID);
        const scoreChar = await service.getCharacteristic(SCORE_CHAR_UUID);
        await scoreChar.startNotifications();
        scoreChar.addEventListener('characteristicvaluechanged', onScoreReceived);
        statusEl.textContent = 'Connected to ' + device.name;
        statusEl.className = 'connected';
        connectBtn.disabled = true;
        connectBtn.textContent = 'Connected';
      } catch (error) {
        console.error('Connection failed:', error);
        statusEl.textContent = 'Connection failed: ' + error.message;
        statusEl.className = 'disconnected';
      }
    });

What does this code do?

  • The SERVICE_UUID and SCORE_CHAR_UUID constants must match the UUIDs in your ESP32 sketch exactly. They tell the browser which BLE service and characteristic to look for.
  • The requestDevice() call opens Chrome's Bluetooth device picker, filtered to only show devices advertising the Simon Says service.
  • After connecting, startNotifications() tells the ESP32 to push score data to the browser whenever the characteristic value changes.
  • The characteristicvaluechanged event listener calls onScoreReceived each time the ESP32 sends a new score. You will write that function next.
  • Save index.html. Live Server auto-refreshes the browser.
  • Make sure your ESP32 is powered on and running the Simon Says BLE sketch (check that the Serial Monitor shows "BLE advertising started").
  • In Chrome, click the Connect to Game button on your page.

Chrome's Bluetooth device picker appears, listing nearby BLE devices. You should see "SimonSays" in the list. Do NOT select it yet. The page still needs the score handler functions before a connection will work properly.

SimonSays not showing in the device picker?

  • Make sure your ESP32 is plugged in and the Serial Monitor shows "BLE advertising started". If you closed the Serial Monitor, just re-open it to confirm.
  • Check that Windows Bluetooth is turned on in Settings > Bluetooth & devices.
  • If the ESP32 was previously connected to another device, press the reset button on the board to restart advertising.

Still stuck? Help me figure out why SimonSays doesn't appear in Chrome's Bluetooth device picker.

  • Click Cancel in the device picker for now. You will connect for real once the event handlers are in place.
Handle incoming scores and render the leaderboard

The connection logic references two functions that do not exist yet: onScoreReceived and onDisconnected. You also need renderLeaderboard to build the HTML table. These three functions complete the scoreboard.

  • In index.html, add the following code directly after the closing }); of the event listener you just pasted (still inside the <script> tag):
    // Decode incoming BLE notification and add to scores
    function onScoreReceived(event) {
      const decoder = new TextDecoder();
      const value = decoder.decode(event.target.value);
      // Format: "PlayerName:Score"
      const parts = value.split(':');
      const name = parts[0] || 'Unknown';
      const score = parseInt(parts[1]) || 0;
      scores.push({ name, score, time: new Date().toLocaleTimeString() });
      scores.sort((a, b) => b.score - a.score);
      renderLeaderboard();
    }
    // Build the leaderboard HTML table sorted by score
    function renderLeaderboard() {
      if (scores.length === 0) { scoreListEl.innerHTML = '<p class="no-scores">No scores yet. Play a round!</p>'; return; }
      let html = '<table><thead><tr><th>#</th><th>Player</th><th>Score</th><th>Time</th></tr></thead><tbody>';
      scores.forEach((entry, index) => { html += '<tr><td>' + (index + 1) + '</td><td>' + entry.name + '</td><td>' + entry.score + '</td><td>' + entry.time + '</td></tr>'; });
      html += '</tbody></table>';
      scoreListEl.innerHTML = html;
    }
    // Handle Bluetooth disconnection
    function onDisconnected() {
      statusEl.textContent = 'Disconnected';
      statusEl.className = 'disconnected';
      connectBtn.disabled = false;
      connectBtn.textContent = 'Reconnect';
    }

What does this code do?

  • The onScoreReceived function decodes the raw BLE bytes into a string (e.g., "Player:3"), splits on the colon, and pushes the result into the scores array with a timestamp.
  • After each new score, the array is sorted descending so the highest score always sits at rank 1. The tr:first-child td CSS rule then highlights it in gold automatically.
  • The renderLeaderboard function rebuilds the full table HTML every time a score arrives.
  • The onDisconnected function resets the UI so the learner can reconnect by clicking the button again.
  • Save index.html. The browser auto-refreshes.

Before you test the full flow, what do you think will happen when you click Connect, select SimonSays, play a round, and deliberately lose?

  • Click the Connect to Game button in Chrome.
  • In the Bluetooth device picker, select SimonSays and click Pair.
  • The status indicator turns green and reads "Connected to SimonSays". The button text changes to "Connected" and is greyed out.
  • On your ESP32, press any button to start a game.
  • Deliberately press the wrong button to trigger a game over.

Within 1-2 seconds, a leaderboard table appears on the web page showing rank, player name, score, and time. The top entry is highlighted in gold. You just bridged physical hardware to a browser wirelessly.

Score not appearing on the leaderboard?

  • Check Chrome's DevTools console (F12 > Console) for errors. A "GATT operation failed" usually means the ESP32 reset mid-connection. Press the ESP32 reset button and reconnect.
  • Confirm the Serial Monitor shows "Score sent via BLE" after you lose. If it shows "Game Over" but no BLE message, the browser may not have connected successfully.
  • Make sure both SERVICE_UUID values in your JavaScript match the ones in your Arduino sketch character-for-character.

Still stuck? Help me debug why scores from my ESP32 are not showing up on the web page leaderboard.

✔️ Awesome, I've got everything!

Great. Double check you have saved index.html and that the leaderboard updates when you play and lose a round.

ⓧ I'd like to double check the full code

Your index.html file should contain these sections in order. Scroll up and confirm each is present:

  • The <!DOCTYPE html> declaration and <head> with all CSS styles (18 CSS rules inside the <style> block).
  • The <body> with an h1, a .controls div containing connectBtn, a #status div, and a .leaderboard div with scoreList inside.
  • A <script> block containing: UUID constants, DOM references, the connectBtn click handler with requestDevice and startNotifications, then onScoreReceived, renderLeaderboard, and onDisconnected functions.

Your scoreboard is live. The hardware-to-browser bridge is complete, and every game over pushes a score wirelessly to your leaderboard. Next up in the Secret Mission, you will add player names and multiplayer support so each person's scores are tagged on the board.

Secret mission

Add Player Names and Multiplayer

Your leaderboard works, but every score shows the same default name. In this mission, you will add bidirectional BLE communication so players can type their name in the browser and see it appear next to their score. This turns your solo game into a multiplayer showdown.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally with physical hardware, so there are no ongoing costs.

Resources you used:

  • Arduino IDE installed on your PC.
  • ESP32 board package installed inside Arduino IDE.
  • simon_says_ble.ino sketch file.
  • index.html web scoreboard file.
  • Physical breadboard circuit (LEDs, buttons, buzzer wired to ESP32).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep playing the game.

  • Leave the breadboard circuit assembled and the ESP32 plugged in via USB.
  • Keep Arduino IDE installed for future ESP32 projects.
  • Your simon_says_ble.ino and index.html files stay on your Desktop, ready to modify.

Pause - I'll come back to this later

Shut down the hardware to free up your USB port and desk space. Your files and software stay intact for when you return.

  • Unplug the ESP32 from your USB cable. The circuit powers down immediately with no ongoing cost.
  • Close Arduino IDE and any local server you had running for the web page.
  • Leave the breadboard assembled. Components stay wired and ready to use when you plug the ESP32 back in.

Delete - I don't want to use this again

Remove all project files and software. Your hardware components are yours to keep and reuse for other projects.

  • Unplug the ESP32 from USB.
  • Disassemble the breadboard circuit. Pull out the jumper wires, LEDs, resistors, buttons, and buzzer. Store them for future projects.
  • Delete your project files by opening PowerShell and running:
Remove-Item -Path "$HOME\Desktop\simon_says_ble.ino" -Force
Remove-Item -Path "$HOME\Desktop\index.html" -Force
  • Uninstall Arduino IDE by opening Settings, selecting Apps, then Apps & features. Find Arduino IDE in the list and click Uninstall.

Nice Work!

Nice Work!

Nice work! You just built a physical Simon Says memory game that wirelessly sends scores to a browser-based leaderboard.

You've learned how to:

  • Build a complete Simon Says game on an ESP32 with LEDs, buttons, and a buzzer, programming GPIO input/output, random sequence generation, and game state management in C++.
  • Set up a BLE GATT server with a custom service and NOTIFY characteristic that broadcasts scores wirelessly to nearby devices.
  • Create a Web Bluetooth scoreboard that connects to your hardware from Chrome and displays a live leaderboard updating in real-time.
  • Secret Mission: Added bidirectional BLE communication with a writable characteristic for player names, enabling multiplayer turn-taking.

Ready to quiz yourself?