Escape rooms are puzzles you play with your hands: find the clue, crack the code, open the next lock. This capstone builds one in a box. Three locks stand between the players and whatever is inside. Lock 1 is a safecracker dial: turn the knob and listen, because the buzzer clicks faster the closer you get to the secret number. Lock 2 is a riddle on the LCD, answered by tapping the button. Lock 3 needs the right key card, which the game master can hide anywhere in the room. When all three lights are green, the servo pulls back the latch and the LCD shows your escape time.
What you'll learn
- How to build a game from separate stages with a state machine
- How to give feedback with sound that changes speed
- How to save a key card in EEPROM so the box remembers it after a power cut
- How to fit an LCD, an RFID reader and a servo on one UNO without pin clashes
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- 1602 LCD display, 10 kΩ potentiometer and 1 resistor, 220 Ω (UNO R3 Starter Kit)
- A second 10 kΩ potentiometer for the dial (UNO R3 Starter Kit)
- RC522 RFID module with card and key fob (UNO R3 Starter Kit)
- Servo motor SG90 with horns and screws (UNO R3 Starter Kit)
- 3 green LEDs and 3 resistors, 220 Ω (UNO R3 Starter Kit)
- 1 push button (UNO R3 Starter Kit)
- Passive buzzer (UNO R3 Starter Kit)
- The printed parts from the 3D printing box below (puzzle box and lid), or the cardboard version described there
- Breadboard and jumper wires (UNO R3 Starter Kit)
Wiring

| From | To |
|---|---|
| LCD VSS, RW and K | GND |
| LCD VDD | 5V |
| LCD V0 | Middle pin of the contrast potentiometer |
| LCD RS, E | UNO pins 7, 6 |
| LCD D4, D5, D6, D7 | UNO pins 5, 4, 3, 2 |
| LCD A (backlight +) | 220 Ω resistor, then 5V |
| RFID SDA, RST | UNO pins 10, 9 |
| RFID SCK, MOSI, MISO | UNO pins 13, 11, 12 |
| RFID 3.3V, GND | 3.3V, GND (never 5V) |
| Dial potentiometer: middle pin | UNO pin A0 (outer pins to 5V and GND) |
| Push button | UNO pin A1 and GND |
| Passive buzzer +, − | UNO pin A2, GND |
| Lock LEDs 1, 2, 3 (long legs) | 220 Ω resistor each, then UNO pins A3, A4, A5 |
| Servo signal, +, − | UNO pin 8, 5V, GND |
The LCD moved to pins 7 to 2 because the RFID reader needs pins 11, 12 and 13 for its SPI connection. The analog pins A1 to A5 work as ordinary digital pins here.
The sketch
In the Arduino IDE, open Library Manager and install MFRC522 (by GithubCommunity). LiquidCrystal, Servo and EEPROM come with the Arduino IDE.
#include <SPI.h>
#include <MFRC522.h>
#include <LiquidCrystal.h>
#include <Servo.h>
#include <EEPROM.h>
LiquidCrystal lcd(7, 6, 5, 4, 3, 2);
MFRC522 rfid(10, 9); // SDA, RST
Servo latch;
const int DIAL_PIN = A0, BUTTON_PIN = A1, BUZZER_PIN = A2, SERVO_PIN = 8;
const int LOCK_LEDS[3] = {A3, A4, A5};
const int LOCKED_ANGLE = 90, OPEN_ANGLE = 0;
const char* const RIDDLES[5][2] = { // two LCD lines each; the answer is a number from 1 to 9
{"Legs on a", "spider? Tap it!"},
{"Sides on a", "triangle?"},
{"Days in a week?", "Tap the answer"},
{"Wheels on two", "bikes? Tap it!"},
{"Lights on a", "traffic light?"}};
const int ANSWERS[5] = {8, 3, 7, 4, 3};
int stage = 0; // 0 = waiting to start, 1-3 = locks, 4 = escaped
int secret, riddle;
byte key[4]; // UID of the saved key card
unsigned long startedAt, lastClick = 0, onTargetSince = 0;
int taps = 0;
unsigned long lastTap = 0;
bool lastButton = HIGH;
void lcdShow(const char* line1, const char* line2) {
lcd.clear();
lcd.print(line1);
lcd.setCursor(0, 1);
lcd.print(line2);
}
bool buttonPressed() { // true once per press
bool now = digitalRead(BUTTON_PIN);
bool pressed = (lastButton == HIGH && now == LOW);
lastButton = now;
if (pressed) delay(30); // ignore contact bounce
return pressed;
}
void success() {
tone(BUZZER_PIN, 988, 120);
delay(150);
tone(BUZZER_PIN, 1319, 250);
delay(300);
}
void fail() {
tone(BUZZER_PIN, 180, 400);
delay(450);
}
void nextStage() {
if (stage >= 1) digitalWrite(LOCK_LEDS[stage - 1], HIGH);
stage++;
if (stage == 1) {
lcdShow("Lock 1: the dial", "Listen closely..");
} else if (stage == 2) {
riddle = random(5);
taps = 0;
lcdShow(RIDDLES[riddle][0], RIDDLES[riddle][1]);
} else if (stage == 3) {
lcdShow("Lock 3: show me", "the key card");
} else {
unsigned long s = (millis() - startedAt) / 1000;
char line[17];
snprintf(line, sizeof(line), "Time %02lu:%02lu", s / 60, s % 60);
lcdShow("You ESCAPED!", line);
latch.write(OPEN_ANGLE);
int tune[4] = {523, 659, 784, 1047};
for (int i = 0; i < 4; i++) { tone(BUZZER_PIN, tune[i], 180); delay(220); }
}
}
void resetGame() {
latch.write(LOCKED_ANGLE);
for (int i = 0; i < 3; i++) digitalWrite(LOCK_LEDS[i], LOW);
stage = 0;
lcdShow("Puzzle box", "Press to start");
}
void saveKeyCard() { // game master: hold the button while switching on
lcdShow("Game master:", "scan the key");
while (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) delay(50);
for (int i = 0; i < 4; i++) EEPROM.update(i, rfid.uid.uidByte[i]);
rfid.PICC_HaltA();
lcdShow("Key card saved!", "Let go of button");
success();
while (digitalRead(BUTTON_PIN) == LOW) delay(10);
}
void setup() {
Serial.begin(9600);
lcd.begin(16, 2);
SPI.begin();
rfid.PCD_Init();
pinMode(BUTTON_PIN, INPUT_PULLUP);
for (int i = 0; i < 3; i++) pinMode(LOCK_LEDS[i], OUTPUT);
latch.attach(SERVO_PIN);
if (digitalRead(BUTTON_PIN) == LOW) saveKeyCard();
for (int i = 0; i < 4; i++) key[i] = EEPROM.read(i);
resetGame();
}
void loop() {
if (stage == 0) { // waiting for players
if (buttonPressed()) {
randomSeed(micros()); // the moment of the press makes it random
secret = random(5, 96);
Serial.print("Secret dial number: ");
Serial.println(secret);
startedAt = millis();
nextStage();
}
} else if (stage == 1) { // the safecracker dial
int dial = map(analogRead(DIAL_PIN), 0, 1023, 0, 99);
int distance = abs(dial - secret);
unsigned long gap = map(min(distance, 50), 0, 50, 60, 900); // closer = faster clicks
if (millis() - lastClick > gap) {
lastClick = millis();
tone(BUZZER_PIN, 2200, 8);
}
if (distance <= 1) {
if (onTargetSince == 0) onTargetSince = millis();
if (millis() - onTargetSince > 1500) { // hold it still for 1.5 s
onTargetSince = 0;
success();
nextStage();
}
} else {
onTargetSince = 0;
}
} else if (stage == 2) { // the riddle: tap the answer, then wait
if (buttonPressed()) {
taps++;
lastTap = millis();
tone(BUZZER_PIN, 1500, 30);
lcd.setCursor(15, 1);
lcd.print(taps % 10);
}
if (taps > 0 && millis() - lastTap > 1500) {
if (taps == ANSWERS[riddle]) {
success();
nextStage();
} else {
fail();
taps = 0;
lcdShow(RIDDLES[riddle][0], RIDDLES[riddle][1]);
}
}
} else if (stage == 3) { // the key card
if (rfid.PICC_IsNewCardPresent() && rfid.PICC_ReadCardSerial()) {
bool match = true;
for (int i = 0; i < 4; i++) if (rfid.uid.uidByte[i] != key[i]) match = false;
rfid.PICC_HaltA();
if (match) {
success();
nextStage();
} else {
lcdShow("Wrong key!", "Keep looking...");
fail();
delay(1000);
lcdShow("Lock 3: show me", "the key card");
}
}
} else if (buttonPressed()) { // escaped: close the lid and press to lock again
resetGame();
}
}
Play it
- Save the key card first. Hold the button down while you plug in the USB cable. The LCD says "Game master: scan the key". Hold the card (or the key fob) against the target, then let go of the button. The box remembers it, even when it is switched off.
- Put a prize inside, close the lid and press the button. The servo locks the latch and the clock starts.
- Lock 1: turn the dial slowly. The clicks get faster as you get closer. When they are almost a buzz, hold the dial still for a second and a half. The first light turns green.
- Lock 2: read the riddle and tap the answer on the button, one tap for each, then wait. A wrong answer buzzes and you can try again.
- Lock 3: find the key card the game master hid in the room and hold it against the target. The latch opens and the LCD shows your time. Close the lid and press the button to lock it for the next team.
The Serial Monitor (9600 baud) shows the secret dial number, which is handy for the game master and for testing.
How it works
The whole game is a state machine: the variable stage says which lock the players are on, and loop() only runs the code for that stage. nextStage() lights the LED for the lock that was just solved, moves on and sets up the screen for the next one. Adding a fourth lock means adding one more else if and one more case in nextStage().
The dial uses sound as a clue. map() turns the distance to the secret number into a waiting time between clicks: 60 ms when you are right on it, 900 ms when you are far away. Players hear the clicks speed up long before they see anything, just like a safecracker in a film.
The key card is stored in EEPROM, a small memory inside the UNO that keeps its contents without power. EEPROM.update() only writes a byte when it has changed, which saves the memory from wearing out. Every RFID card has its own UID, a serial number written in at the factory, and the sketch simply compares the four bytes of the card it sees with the four it saved.
Pins were the tricky part. The RFID reader talks over SPI, which on the UNO always uses pins 11, 12 and 13, so the LCD moved to pins 2 to 7, and the button, buzzer and LEDs use the analog pins as digital pins.
Try this next
- Add a countdown: the players have 10 minutes, and the LCD shows the time left.
- Give hints: hold the button for 3 seconds to see a clue on the LCD, but each hint adds 30 seconds to the final time.
- Replace the riddle with a Morse-code lock, using the tap timing from the Morse code lesson.
- Save the best escape time in EEPROM and show it at the start.
Troubleshooting
- The LCD shows only blocks or nothing: turn the contrast potentiometer slowly from one end to the other.
- The key card is never accepted: save it again (hold the button while plugging in), and check that the RFID reader is on 3.3V, not 5V.
- The card isn't read through the box: the wall in front of the reader must be thin (about 1.5 mm) and the reader flat against it. Metal near the reader also blocks it.
- The dial clicks but never unlocks: you have to hold it still within one number of the secret for 1.5 seconds. Check the secret number in the Serial Monitor.
- The board restarts when the servo moves: servos draw bursts of current. Use a good USB port or a powered hub, and check that the servo's red wire is on 5V.
- A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.