Built-in project · Arduino Mega 2560 R3

Escape-room puzzle box on an Arduino Mega

Five puzzles in ten minutes: a keypad code, a magnet key, the right RFID card, a tilt sequence and three flashes of light. Solve them all and the lock swings open. The whole build, an Arduino Mega and 19 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Advanced Runs in your browser. Free, and no account needed.

Build it as the Escape Box Kit: the parts in numbered bags and a booklet that checks every step. Free.

Escape-room puzzle box · Arduino Mega 2560 R3live0.000 s 0.00x
Read the clue on the display, then type the year on the keypad and press #
The circuit itself, running here on the simulator. Press what can be pressed; click anything else to open it in the editor.

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Mega 2560 R3
// Escape box: five puzzles between you and what is locked inside, on a Mega.
//
// Ten minutes on the clock. Solve the puzzles in order and each one lights a
// green lamp; solve all five and the servo swings the lock open. Stuck? The
// hint button shows a clue for the puzzle you are on, and costs 30 seconds.
//
//   1  Keypad    a 4 x 4 keypad: rows on 12, 11, 10, 9, columns on 8, 7, 6, 5
//   2  Magnet    a reed switch on 4: a magnet held close is the "key"
//   3  Card      an MFRC522 reader on the SPI pins (50 MISO, 51 MOSI,
//                52 SCK) with its chip select on 49
//   4  Tilt      an MPU-6050 on the I2C pins (SDA 20, SCL 21): tip the box
//   5  Light     a photoresistor module on A0
//
//   Lamps        five green LEDs on 29, 31, 33, 35 and 37
//   Hint         a pushbutton on 39
//   Display      LCD2004 on I2C, address 0x27, sharing the bus with the MPU
//   Lock         a servo on 2: 0 degrees locked, 90 open
//   Sound        piezo on 3
//
// The answers, if you would rather not work them out: the year of the first
// Moon landing, the magnet, the card whose UID ends in A7, left-right-forward,
// and three flashes of light.

#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <Keypad.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Servo.h>
#include "mokxi_i2c.h"
#include "mokxi_mpu6050.h"

const int REED = 4;
const int PIEZO = 3;
const int LOCK_PIN = 2;
const int HINT_BUTTON = 39;
const int RFID_SS = 49;
const int LIGHT = A0;
const int LAMP[5] = {29, 31, 33, 35, 37};

const char KEYS[4][4] = {
    {'1', '2', '3', 'A'},
    {'4', '5', '6', 'B'},
    {'7', '8', '9', 'C'},
    {'*', '0', '#', 'D'},
};
byte rowPins[4] = {12, 11, 10, 9};
byte colPins[4] = {8, 7, 6, 5};
Keypad keypad = Keypad(makeKeymap(KEYS), rowPins, colPins, 4, 4);

LiquidCrystal_I2C lcd(0x27, 20, 4);
MFRC522 reader(RFID_SS, UNUSED_PIN);
SoftI2c imuBus(21, 20);
Mpu6050 imu(imuBus);
Servo lock;

const unsigned long GAME_MS = 600000UL;  // ten minutes
const unsigned long HINT_COST_MS = 30000UL;

const char *const TITLE[5] = {"1/5 THE KEYPAD", "2/5 THE KEY", "3/5 THE CARD", "4/5 THE TILT",
                              "5/5 THE LIGHT"};
const char *const CLUE_A[5] = {"FIRST MOON LANDING", "SOME LOCKS OPEN", "ONLY THE CARD THAT",
                               "TIP THE BOX LEFT,", "WAKE THE SENSOR"};
const char *const CLUE_B[5] = {"TYPE THE YEAR, #", "WITH AN UNSEEN PULL", "ENDS IN A7 PASSES",
                               "RIGHT, THEN FORWARD", "WITH 3 FLASHES"};
const char *const HINT[5] = {"APOLLO 11, JULY 19..", "BRING A MAGNET CLOSE", "TAP EACH OF THE 4",
                             "LEVEL IT EACH TIME", "DARK, BRIGHT, x3"};

enum Phase { PLAYING, ESCAPED, TIME_UP };
Phase phase = PLAYING;
int puzzle = 0;
unsigned long started = 0;
unsigned long penalty = 0;
unsigned long hintUntil = 0;
unsigned long messageUntil = 0;
long finalMs = 0;
char message[21] = "";

// Puzzle state.
char code[5] = "";
unsigned long magnetSince = 0;
int tiltStep = 0;
bool tiltLevel = true;
int flashes = 0;
bool lightWasBright = false;

// ---- the display, a row of changes at a time ----------------------------

char wanted[4][21];
char onGlass[4][21];
int nextRow = 0;

void showLine(int row, const char *text) {
  int i = 0;
  for (; i < 20 && text[i]; i++) wanted[row][i] = text[i];
  for (; i < 20; i++) wanted[row][i] = ' ';
  wanted[row][20] = 0;
}

void serviceLcd() {
  for (int tries = 0; tries < 4; tries++) {
    int row = nextRow;
    nextRow = (nextRow + 1) % 4;
    if (strcmp(wanted[row], onGlass[row]) == 0) continue;
    // One setCursor for each run of changed characters, not one a character.
    int col = 0;
    while (col < 20) {
      if (wanted[row][col] == onGlass[row][col]) {
        col++;
        continue;
      }
      lcd.setCursor(col, row);
      while (col < 20 && wanted[row][col] != onGlass[row][col]) {
        lcd.write(wanted[row][col]);
        onGlass[row][col] = wanted[row][col];
        col++;
      }
    }
    return;
  }
}

void flash(const char *text) {
  strncpy(message, text, 20);
  message[20] = 0;
  messageUntil = millis() + 1500;
}

// ---- small helpers ----------------------------------------------------

bool hintWas = false;
unsigned long hintChanged = 0;

bool hintPressed() {
  bool down = digitalRead(HINT_BUTTON) == LOW;
  if (down != hintWas && millis() - hintChanged > 20) {
    hintWas = down;
    hintChanged = millis();
    return down;
  }
  return false;
}

long remainingMs() {
  long left = (long)GAME_MS - (long)(millis() - started) - (long)penalty;
  return left > 0 ? left : 0;
}

// "mm:ss" into `out`, which has room for six characters.
void clockText(long ms, char *out) {
  long s = (ms + 999) / 1000;
  int m = (int)(s / 60);
  int r = (int)(s % 60);
  out[0] = '0' + m / 10;
  out[1] = '0' + m % 10;
  out[2] = ':';
  out[3] = '0' + r / 10;
  out[4] = '0' + r % 10;
  out[5] = 0;
}

void solved() {
  digitalWrite(LAMP[puzzle], HIGH);
  Serial.print("SOLVED ");
  Serial.println(TITLE[puzzle]);
  tone(PIEZO, 1320, 120);
  puzzle++;
  hintUntil = 0;
  if (puzzle == 5) {
    phase = ESCAPED;
    lock.write(90);
    Serial.println("LOCK OPEN");
    finalMs = (long)(millis() - started) + (long)penalty;
    char t[6];
    clockText(finalMs, t);
    Serial.print("ESCAPED IN ");
    Serial.println(t);
    Serial.flush();
    // A little fanfare.
    const int notes[4] = {523, 659, 784, 1047};
    for (int i = 0; i < 4; i++) {
      tone(PIEZO, notes[i], 140);
      delay(160);
    }
  }
}

void resetGame() {
  phase = PLAYING;
  puzzle = 0;
  started = millis();
  penalty = 0;
  hintUntil = 0;
  code[0] = 0;
  magnetSince = 0;
  tiltStep = 0;
  tiltLevel = true;
  flashes = 0;
  lightWasBright = false;
  for (int i = 0; i < 5; i++) digitalWrite(LAMP[i], LOW);
  lock.write(0);
  Serial.println("ESCAPE BOX: TEN MINUTES, GO");
}

// ---- the five puzzles ---------------------------------------------------

void runKeypad() {
  char key = keypad.getKey();
  if (!key) return;
  tone(PIEZO, 1800, 30);
  if (key == '*') {
    code[0] = 0;
  } else if (key == '#') {
    Serial.print("CODE ");
    Serial.println(code);
    if (strcmp(code, "1969") == 0) {
      solved();
    } else {
      flash("WRONG CODE");
      tone(PIEZO, 220, 250);
    }
    code[0] = 0;
  } else if (strlen(code) < 4) {
    int n = strlen(code);
    code[n] = key;
    code[n + 1] = 0;
  }
}

// The reed closes with a magnet within about 15 mm. Hold it there two seconds.
void runMagnet() {
  if (digitalRead(REED) == LOW) {
    if (magnetSince == 0) {
      magnetSince = millis();
      Serial.println("MAGNET NEAR");
    }
    if (millis() - magnetSince > 2000) solved();
  } else {
    magnetSince = 0;
  }
}

void runCard() {
  if (!reader.PICC_IsNewCardPresent() || !reader.PICC_ReadCardSerial()) return;
  byte last = reader.uid.uidByte[reader.uid.size - 1];
  Serial.print("CARD ...");
  Serial.println(last, HEX);
  reader.PICC_HaltA();
  if (reader.uid.size == 4 && last == 0xA7) {
    solved();
  } else {
    flash("ACCESS DENIED");
    tone(PIEZO, 220, 250);
  }
}

// Left, right, forward, each from level. A wrong tip starts the sequence again.
void runTilt() {
  Mpu6050Reading r;
  if (!imu.read(r)) return;
  long ax = imu.milliG(r.ax);
  long ay = imu.milliG(r.ay);
  long az = imu.milliG(r.az);
  int roll = Mpu6050::tiltDegrees(ay, az);
  int pitch = Mpu6050::tiltDegrees(-ax, az);
  int dir = 0;  // 1 left, 2 right, 3 forward
  if (roll < -25) dir = 1;
  else if (roll > 25) dir = 2;
  else if (pitch > 25) dir = 3;
  if (dir == 0 && roll > -10 && roll < 10 && pitch > -10 && pitch < 10) {
    tiltLevel = true;
    return;
  }
  if (dir == 0 || !tiltLevel) return;
  tiltLevel = false;
  if (dir == tiltStep + 1) {
    tiltStep++;
    Serial.print("TILT ");
    Serial.println(tiltStep);
    tone(PIEZO, 1000 + 200 * tiltStep, 80);
    if (tiltStep == 3) solved();
  } else {
    tiltStep = 0;
    flash("START AGAIN");
    tone(PIEZO, 220, 200);
  }
}

// The module reads lower in more light. Count dark-to-bright changes, with a
// gap between the two levels so a flicker at the edge is not counted twice.
void runLight() {
  int level = analogRead(LIGHT);
  if (!lightWasBright && level < 300) {
    lightWasBright = true;
    flashes++;
    Serial.print("FLASH ");
    Serial.println(flashes);
    tone(PIEZO, 1500, 60);
    if (flashes == 3) solved();
  } else if (lightWasBright && level > 600) {
    lightWasBright = false;
  }
}

void setup() {
  Serial.begin(9600);
  pinMode(REED, INPUT_PULLUP);
  pinMode(HINT_BUTTON, INPUT_PULLUP);
  for (int i = 0; i < 5; i++) pinMode(LAMP[i], OUTPUT);
  lcd.init();
  lcd.backlight();
  for (int r = 0; r < 4; r++) {
    memset(onGlass[r], ' ', 20);
    onGlass[r][20] = 0;
    showLine(r, "");
  }
  SPI.begin();
  reader.PCD_Init();
  imuBus.begin();
  imu.begin();
  lock.attach(LOCK_PIN);
  resetGame();
}

void loop() {
  if (phase == PLAYING) {
    // The light puzzle is primed from the start, so arriving at it in a
    // bright room does not count as a flash.
    if (puzzle == 4) runLight();
    else if (puzzle < 4) lightWasBright = analogRead(LIGHT) < 300;
    if (puzzle == 0) runKeypad();
    else if (puzzle == 1) runMagnet();
    else if (puzzle == 2) runCard();
    else if (puzzle == 3) runTilt();

    if (phase == PLAYING && hintPressed()) {
      penalty += HINT_COST_MS;
      hintUntil = millis() + 4000;
      Serial.print("HINT: ");
      Serial.println(HINT[puzzle]);
    }
    if (phase == PLAYING && remainingMs() == 0) {
      phase = TIME_UP;
      Serial.println("TIME IS UP");
      tone(PIEZO, 220, 800);
    }
  } else if (hintPressed()) {
    resetGame();
  }

  char row[21];
  char t[6];
  if (phase == PLAYING) {
    clockText(remainingMs(), t);
    strcpy(row, "ESCAPE BOX     ");
    strcat(row, t);
    showLine(0, row);
    showLine(1, TITLE[puzzle]);
    if (millis() < hintUntil) {
      showLine(2, "HINT (-30 SECONDS):");
      showLine(3, HINT[puzzle]);
    } else {
      showLine(2, CLUE_A[puzzle]);
      if (millis() < messageUntil) {
        showLine(3, message);
      } else if (puzzle == 0) {
        strcpy(row, "CODE: ");
        strcat(row, code);
        showLine(3, row);
      } else {
        showLine(3, CLUE_B[puzzle]);
      }
    }
  } else if (phase == ESCAPED) {
    clockText(finalMs, t);
    showLine(0, "THE BOX IS OPEN!");
    strcpy(row, "ESCAPED IN ");
    strcat(row, t);
    showLine(1, row);
    showLine(2, "");
    showLine(3, "HINT BUTTON: AGAIN");
  } else {
    showLine(0, "TIME IS UP");
    showLine(1, "THE BOX STAYS SHUT");
    showLine(2, "");
    showLine(3, "HINT BUTTON: AGAIN");
    bool on = (millis() / 300) % 2 == 0;
    for (int i = 0; i < 5; i++) digitalWrite(LAMP[i], on ? HIGH : LOW);
  }
  serviceLcd();
  Serial.flush();
  delay(10);
}

How it is wired

39 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.

  • Arduino Mega 2560 R3 pin SCL; Character LCD, 20x4, with I2C backpack pin SCL
  • Arduino Mega 2560 R3 pin SDA; Character LCD, 20x4, with I2C backpack pin SDA
  • Ground: Arduino Mega 2560 R3 pin GND; Reed switch pin 1; Piezo speaker pin 2; Accelerometer and gyroscope, MPU6050 pin GND; Hobby servo pin GND; LED, green (1) pin C; LED, green (2) pin C; LED, green (3) pin C; LED, green (4) pin C; LED, green (5) pin C; Pushbutton pin 2a; Pushbutton pin 2b
  • Arduino Mega 2560 R3 pin 12; Keypad, 4x4 pin R1
  • Arduino Mega 2560 R3 pin 11; Keypad, 4x4 pin R2
  • Arduino Mega 2560 R3 pin 10; Keypad, 4x4 pin R3
  • Arduino Mega 2560 R3 pin 9; Keypad, 4x4 pin R4
  • Arduino Mega 2560 R3 pin 8; Keypad, 4x4 pin C1
  • Arduino Mega 2560 R3 pin 7; Keypad, 4x4 pin C2
  • Arduino Mega 2560 R3 pin 6; Keypad, 4x4 pin C3
  • Arduino Mega 2560 R3 pin 5; Keypad, 4x4 pin C4
  • Arduino Mega 2560 R3 pin 4; Reed switch pin 2
  • Arduino Mega 2560 R3 pin 3; Piezo speaker pin 1
  • Arduino Mega 2560 R3 pin 2; Hobby servo pin PWM
  • Arduino Mega 2560 R3 pin 20; Accelerometer and gyroscope, MPU6050 pin SDA
  • Arduino Mega 2560 R3 pin 21; Accelerometer and gyroscope, MPU6050 pin SCL
  • Arduino Mega 2560 R3 pin IOREF; Photoresistor sensor module pin VCC
  • Arduino Mega 2560 R3 pin 3V3; RFID reader, MFRC522 pin RST; RFID reader, MFRC522 pin 3.3V
  • Arduino Mega 2560 R3 pin 5V; Character LCD, 20x4, with I2C backpack pin VCC
  • Ground: Arduino Mega 2560 R3 pin GND; Photoresistor sensor module pin GND
  • Ground: Arduino Mega 2560 R3 pin GND; Character LCD, 20x4, with I2C backpack pin GND
  • Arduino Mega 2560 R3 pin A0; Photoresistor sensor module pin AO
  • Arduino Mega 2560 R3 pin 29; Resistor, 220 Ω (1) pin 1
  • Arduino Mega 2560 R3 pin 31; Resistor, 220 Ω (2) pin 1
  • Arduino Mega 2560 R3 pin 33; Resistor, 220 Ω (3) pin 1
  • Arduino Mega 2560 R3 pin 35; Resistor, 220 Ω (4) pin 1
  • Arduino Mega 2560 R3 pin 37; Resistor, 220 Ω (5) pin 1
  • Arduino Mega 2560 R3 pin 39; Pushbutton pin 1a; Pushbutton pin 1b
  • Arduino Mega 2560 R3 pin 49; RFID reader, MFRC522 pin SDA
  • Arduino Mega 2560 R3 pin 50; RFID reader, MFRC522 pin MISO
  • Arduino Mega 2560 R3 pin 51; RFID reader, MFRC522 pin MOSI
  • Arduino Mega 2560 R3 pin 52; RFID reader, MFRC522 pin SCK
  • Arduino Mega 2560 R3 pin 5V; Accelerometer and gyroscope, MPU6050 pin VCC; Hobby servo pin VCC
  • Ground: Arduino Mega 2560 R3 pin GND; RFID reader, MFRC522 pin GND
  • Resistor, 220 Ω (1) pin 2; LED, green (1) pin A
  • Resistor, 220 Ω (2) pin 2; LED, green (2) pin A
  • Resistor, 220 Ω (3) pin 2; LED, green (3) pin A
  • Resistor, 220 Ω (4) pin 2; LED, green (4) pin A
  • Resistor, 220 Ω (5) pin 2; LED, green (5) pin A

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.