A whack-a-mole game on an Arduino
Intermediate
Or see every lesson.
- 455parts on the bench
- 43boards running now
- 1.00xreal time, on every board
In whack-a-mole a light pops up and you hit the button under it before it goes out. It is a great party game and a great project, because it is the first one where the Arduino has to do several things at once: time the round, time each mole, read four buttons, beep, and update a display, all without waiting on any one of them.
The circuit above is an Arduino Uno with four LED moles, a button under each, a passive buzzer and a TM1637 four-digit display. Press any button to start a thirty-second round. The display shows the seconds left on the left and your score on the right. Each hit scores a point and makes the next mole a little quicker; a wrong button costs a point. When time runs out the display shows your best score of the session.
What you need
- An Arduino Uno
- Four LEDs and four 220 ohm resistors
- Four pushbuttons
- A passive buzzer (piezo)
- A TM1637 four-digit display
- A breadboard and jumper wires
Wiring
| Part and pin | Goes to | Note |
|---|---|---|
| Mole LEDs 1 to 4, through 220 ohm | Pins 2, 3, 4, 5 | Cathodes to GND |
| Buttons 1 to 4 | Pins 6, 7, 8, 9 | Other leg to GND, INPUT_PULLUP; button 1 under mole 1 |
| Piezo | Pin 10 | Other leg to GND |
| TM1637 CLK | A0 | Analog pins work as digital pins |
| TM1637 DIO | A1 | |
| TM1637 VCC and GND | 5 V and GND |
Doing five things at once
The sketch never calls a long delay(). Instead loop() runs every 5 ms and asks a few questions about the clock. Is the round over? Is the lit mole past its time? Is it time for the next mole? It keeps the answers in a handful of variables: when the round started, when the current mole goes back down, and when the next one pops up.
That is the pattern behind every game, robot and gadget that has to watch more than one thing. Compare it with delay(900) to keep a mole up: during those 900 ms the buttons would not be read at all, and every hit would be missed.
The buttons are read for new presses only. Each button’s state from the last pass is kept in wasDown[], and a press counts when the pin is down now and was up last time. Holding a button scores once, not every 5 ms.
Getting harder as you score
A mole starts out up for 900 ms. Every hit takes 30 ms off, until it would drop below 380 ms. Do the sum and the 18th hit is the last to shorten it, to 360 ms, which is fast for most people. A wrong button costs a point but does not slow the game back down.
Which mole comes next is random, and a mole never pops up under the button you just pressed. The random number generator is seeded from micros() at the first press, which is a person’s reaction time and something nothing can predict. Without that, every session would play the same moles in the same order.
Each sound is a short tone(): 1047 Hz to start, 1568 Hz for a hit, a low 147 Hz for a miss and 392 Hz at the end. Because tone() with a length runs on a timer, the beep plays while the game carries on.
if (hit >= 0) {
if (hit == mole) {
score++;
digitalWrite(LAMP[mole], LOW);
mole = -1;
nextMoleAt = now + random(150, 500);
if (upMs > 380) upMs -= 30;
tone(PIEZO_PIN, 1568, 60);
} else {
score--;
tone(PIEZO_PIN, 147, 120);
}
}
if (mole >= 0 && (int32_t)(now - moleUntil) >= 0) {
digitalWrite(LAMP[mole], LOW); // too slow: it ducks
mole = -1;
nextMoleAt = now + random(150, 500);
}
if (mole < 0 && (int32_t)(now - nextMoleAt) >= 0) {
int8_t pick;
do { pick = random(MOLES); } while (pick == hit);
mole = pick;
moleUntil = now + upMs;
digitalWrite(LAMP[mole], HIGH);
}The score display
The TM1637 takes two wires and draws four digits with a colon. showNumberDecEx(seconds * 100 + score, 0b01000000, true) puts the seconds on the left, the score on the right, the colon between them and leading zeros on. Between rounds, two dashes and the best score show instead.
The comparisons with (int32_t)(now - moleUntil) >= 0 look odd but matter: they keep working when millis() rolls over after about 49 days, which a plain now >= moleUntil does not.
Common mistakes
Using delay() to time the moles. The buttons go deaf while it waits. Use millis() as the sketch does.
Counting a held button as many hits. Score on the change from up to down, not on "is down".
An active buzzer instead of a passive one. An active buzzer has its own oscillator and plays one pitch whatever tone() asks for. The four different sounds need a passive piezo.
A blank display. Check CLK and DIO are not swapped and that the display has 5 V and GND.
Moles that never move. If randomSeed() is called in setup() with a fixed number, or not at all, the order repeats every time. Seed from the first press.
Questions
How do I make a whack-a-mole game with Arduino?
Wire four LEDs and four buttons, light a random LED, record when it lit with millis(), and score a point if its button is pressed before a time limit. Shorten the limit after each hit and end the round after thirty seconds.
Can I use arcade buttons with built-in lights?
Yes. Wire the button switch like a pushbutton and the lamp like the LED, through the right resistor for its voltage. Mokxi has lit arcade buttons in its parts list.
How do I keep a high score after power off?
Save it with EEPROM.put() when a round beats it and read it with EEPROM.get() in setup(). The Uno’s EEPROM is good for about 100,000 writes per cell, so write only on a new best.
Can this run on an Arduino Nano?
Yes. The Nano has the same chip and the same pin numbers, so the sketch runs unchanged.
Keep going
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.