Community project

OLED Whack-A-Mole Game

Photo of OLED Whack-A-Mole Game
Generated with AI

Michael Diaz

Published November 6, 2026

Build a whack-a-mole game on the ESP32 that lights up nine LEDs in random sequence while players race to press buttons before time runs out. The game displays the countdown and remaining time on one OLED screen while a second screen shows the final score, creating an engaging arcade-style experience.

This guide provides a complete wiring diagram, parts list, and firmware to get the game running. Assembly covers connecting the MCP23017 I/O expander for button inputs, wiring nine LED outputs, configuring both SSD1306 displays on separate I2C buses, and testing the finished circuit. Makers will have everything needed to assemble and program a fully functional two-screen game system.

Wiring diagram

Loading diagram…

Gather all the parts

QtyComponent
1

Adafruit MCP23017 breakout 5346

3.3V button expander; address jumpers open, 0x20. Includes bus pullups.

1

SSD1306 OLED

0.96-inch 128 x 64 white monochrome OLED display module driven by an SSD1306 controller over I2C (address 0x3C). It runs from 3 V to 5 V and connects through a 4-pin 2.54 mm header (GND, VCC, SCL, SDA); the self-emitting pixels need no backlight.

1

SSD1306 128x64 OLED on second I2C controller

Four-pin 3.3V SSD1306 module with onboard pullups, address 0x3C on independent Wire1 bus (GPIO32/33), not connected to the game display bus.

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

Push Button

Momentary push button switch

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

LED

red

Standard 3mm/5mm through-hole LED. A current-limiting series resistor is added automatically.

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

330 ohm

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 5 steps

1. Arrange the game

Unplug USB. Arrange nine red LEDs and nine buttons as matching pairs in a 3 by 3 grid, numbered left to right, top to bottom. Place the Start button separately. Use an ESP32 DevKit v1 and the Adafruit MCP23017 breakout 5346, not a bare chip.

2. Connect the button expander

Connect VIN to 3V3, GND to GND, RST to 3V3, SDA to GPIO21 (data) and SCL to GPIO22 (clock). Leave all three address jumpers open for address 0x20. Buttons 1–7 connect to A0–A6; buttons 8 and 9 connect to B0 and B1; Start connects to B2. Each button's other contact goes to GND. Internal pullups keep unpressed inputs high.

  • Do not use A7 or B7 as button inputs. On four-leg switches, use contacts on opposite switching sides, not two permanently joined legs.

3. Wire the nine lights

LEDs 1–9 use GPIO4, 13, 14, 16, 17, 18, 19, 23 and 25 respectively. Each GPIO goes through its own 330 ohm resistor to the LED's long positive leg. Connect every short negative leg to GND.

  • Never connect an LED directly to a GPIO without its resistor.

4. Connect both screens

Use two 128x64 four-pin SSD1306 I2C OLED modules at their default 0x3C address. Both VCC pins go to 3V3 and both GND pins go to GND. Game screen SDA goes to GPIO21 (data) and SCL to GPIO22 (clock). Score screen SDA goes to GPIO32 (data) and SCL to GPIO33 (clock). Separate ESP32 I2C controllers let both screens keep the same address. Use OLED modules with onboard I2C pullups.

  • Check the printed VCC and GND labels; header order varies. Power everything from 3.3V, not 5V.

5. Inspect the finished circuit

Check every ground and resistor, and make sure there are no shorts between 3V3 and GND. Start begins the 3, 2, 1, Go! sequence. During the 30-second round, press the button matching the lit LED for one point. Wrong buttons do not score. Each target lasts up to one second. Holding a button does not repeat points. Start works again after Game Over.

Review all connections

1. Connections between "io" and "ESP32"

FunctionioESP32
powerVIN3V3
groundGNDGND
powerRST3V3
i2cSDAGPIO 21
i2cSCLGPIO 22

2. Connections between "game" and "ESP32"

FunctiongameESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 21
i2cSCLGPIO 22

3. Connections between "score" and "ESP32"

FunctionscoreESP32
powerVCC3V3
groundGNDGND
i2cSDAGPIO 32
i2cSCLGPIO 33

4. Connections between "button1" and "ESP32"

Functionbutton1ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A0EXT

5. Connections between "button2" and "ESP32"

Functionbutton2ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A1EXT

6. Connections between "button3" and "ESP32"

Functionbutton3ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A2EXT

7. Connections between "button4" and "ESP32"

Functionbutton4ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A3EXT

8. Connections between "button5" and "ESP32"

Functionbutton5ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A4EXT

9. Connections between "button6" and "ESP32"

Functionbutton6ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A5EXT

10. Connections between "button7" and "ESP32"

Functionbutton7ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 A6EXT

11. Connections between "button8" and "ESP32"

Functionbutton8ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 B0EXT

12. Connections between "button9" and "ESP32"

Functionbutton9ESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 B1EXT

13. Connections between "start" and "ESP32"

FunctionstartESP32
groundGNDGND
digitalSIGNAL → Adafruit MCP23017 breakout 5346 B2EXT

14. Connections between "r1" and "ESP32"

Functionr1ESP32
digitalP1GPIO 4
digitalP2 → LED ANODEEXT

15. Connections between "led1" and "ESP32"

Functionled1ESP32
groundGNDGND

16. Connections between "r2" and "ESP32"

Functionr2ESP32
digitalP1GPIO 13
digitalP2 → LED ANODEEXT

17. Connections between "led2" and "ESP32"

Functionled2ESP32
groundGNDGND

18. Connections between "r3" and "ESP32"

Functionr3ESP32
digitalP1GPIO 14
digitalP2 → LED ANODEEXT

19. Connections between "led3" and "ESP32"

Functionled3ESP32
groundGNDGND

20. Connections between "r4" and "ESP32"

Functionr4ESP32
digitalP1GPIO 16
digitalP2 → LED ANODEEXT

21. Connections between "led4" and "ESP32"

Functionled4ESP32
groundGNDGND

22. Connections between "r5" and "ESP32"

Functionr5ESP32
digitalP1GPIO 17
digitalP2 → LED ANODEEXT

23. Connections between "led5" and "ESP32"

Functionled5ESP32
groundGNDGND

24. Connections between "r6" and "ESP32"

Functionr6ESP32
digitalP1GPIO 18
digitalP2 → LED ANODEEXT

25. Connections between "led6" and "ESP32"

Functionled6ESP32
groundGNDGND

26. Connections between "r7" and "ESP32"

Functionr7ESP32
digitalP1GPIO 19
digitalP2 → LED ANODEEXT

27. Connections between "led7" and "ESP32"

Functionled7ESP32
groundGNDGND

28. Connections between "r8" and "ESP32"

Functionr8ESP32
digitalP1GPIO 23
digitalP2 → LED ANODEEXT

29. Connections between "led8" and "ESP32"

Functionled8ESP32
groundGNDGND

30. Connections between "r9" and "ESP32"

Functionr9ESP32
digitalP1GPIO 25
digitalP2 → LED ANODEEXT

31. Connections between "led9" and "ESP32"

Functionled9ESP32
groundGNDGND

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MCP23X17.h>
#include <esp_system.h>
TwoWire scoreBus(1);
Adafruit_SSD1306 game(128,64,&Wire,-1), points(128,64,&scoreBus,-1);
Adafruit_MCP23X17 io;
const uint8_t leds[]={4,13,14,16,17,18,19,23,25};
const uint8_t keys[]={0,1,2,3,4,5,6,8,9,10};
struct Button { bool raw=false,down=false; uint32_t changed=0; } buttons[10];
enum State {READY,COUNT,PLAY,OVER};
State state=READY;
uint32_t phaseAt=0,playAt=0,moleAt=0;
int countdown=3,score=0,target=-1,lastSeconds=-1;
bool dirty=true,scoreDirty=true;
void lightsOff(){for(auto p:leds)digitalWrite(p,LOW);target=-1;}
void nextMole(uint32_t now){int old=target;lightsOff();do {target=esp_random()%9;}while(target==old);digitalWrite(leds[target],HIGH);moleAt=now;}
void render(int seconds){
 if(dirty){
  game.clearDisplay();game.setTextColor(SSD1306_WHITE);game.setCursor(0,0);
  if(state==READY){game.setTextSize(3);game.println("Start");game.setTextSize(1);game.println("Press START button");}
  else if(state==COUNT){game.setTextSize(4);if(countdown)game.print(countdown);else game.print("Go!");}
  else if(state==PLAY){game.setTextSize(2);game.println("Time left");game.setTextSize(3);game.print(seconds);game.setTextSize(1);game.println(" seconds");}
  else {game.setTextSize(2);game.println("Game Over");game.setTextSize(1);game.print("Score: ");game.println(score);game.println();game.println("Press START to replay");}
  game.display();dirty=false;
 }
 if(scoreDirty){points.clearDisplay();points.setTextColor(SSD1306_WHITE);points.setCursor(0,0);points.setTextSize(2);points.println("Score");points.setTextSize(3);points.println(score);points.display();scoreDirty=false;}
}
void setup(){
 Serial.begin(115200);
 for(auto p:leds){pinMode(p,OUTPUT);digitalWrite(p,LOW);}
 Wire.begin(21,22);scoreBus.begin(32,33);
 if(!io.begin_I2C(0x20,&Wire)||!game.begin(SSD1306_SWITCHCAPVCC,0x3C,true,false)||!points.begin(SSD1306_SWITCHCAPVCC,0x3C,true,false)){
  Serial.println("Initialization failed: check displays and expander wiring.");while(true)delay(100);
 }
 // A7 and B7 remain outputs: these pins must not be used as inputs.
 io.pinMode(7,OUTPUT);io.digitalWrite(7,LOW);io.pinMode(15,OUTPUT);io.digitalWrite(15,LOW);
 for(auto k:keys)io.pinMode(k,INPUT_PULLUP);
 render(30);
}
void loop(){
 uint32_t now=millis();bool pressed[10]={};uint16_t raw=io.readGPIOAB();
 for(int i=0;i<10;i++){
  bool down=!(raw&(1<<keys[i]));auto &b=buttons[i];
  if(down!=b.raw){b.raw=down;b.changed=now;}
  if(now-b.changed>=25 && b.down!=b.raw){b.down=b.raw;pressed[i]=b.down;}
 }
 if((state==READY||state==OVER)&&pressed[9]){score=0;scoreDirty=true;state=COUNT;countdown=3;phaseAt=now;dirty=true;lightsOff();}
 if(state==COUNT && now-phaseAt>=1000){phaseAt=now;if(countdown>0){countdown--;dirty=true;}else{state=PLAY;playAt=now;lastSeconds=-1;nextMole(now);dirty=true;}}
 int seconds=30;
 if(state==PLAY){
  uint32_t elapsed=now-playAt;
  if(elapsed>=30000){state=OVER;lightsOff();dirty=true;seconds=0;}
  else{
   seconds=(30000-elapsed+999)/1000;
   if(seconds!=lastSeconds){lastSeconds=seconds;dirty=true;}
   if(target>=0 && pressed[target]){score++;scoreDirty=true;nextMole(now);}
   else if(now-moleAt>=1000)nextMole(now);
  }
 }
 render(seconds);delay(2);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik