The Diamond Heist

Hackathon-project — Arduino escape room box

Verhaal

Probeer je in te breken in een ultiem bewaakt museum om een zeldzame diamant te bemachtigen. Dit doe je door een serie krachtige beveiligingssystemen uit te schakelen zonder een alarm te laten afgaan. Door "inside-information" van een poetshulp weten jullie dat je een hoeveelheid tijd hebt om alle alarmen uit te schakelen voor het systeem dit niet langer als een fout detecteert en de politie verwittigt.

Lukt het om alles uit te schakelen binnen de tijd, dan gaan jullie er schatrijk vandoor. Lukt het niet, dan kunnen jullie een leven achter de tralies beginnen.

Omschrijving & challenges

De game bevat 3 challenges. Bij elke opgeloste challenge komt een stuk van een raadsel vrij. Samen vormen deze stukken een code die je in de puzzelbox invoert om de sleutel en zo de toegang tot de diamant te bemachtigen.

Een moeilijkheidsgraad kan ingesteld worden via een afstandsbediening.

0. Hoofdsysteem — aftelklok & alarm

Status: gebouwd en werkend in de doos.

De centrale logica van de doos: een aftelklok op een 7-segment display (aangestuurd via een 74HC595 shift register) toont de resterende tijd. Via de IR-afstandsbediening kan de moeilijkheidsgraad ingesteld worden. Wanneer de tijd op is voordat alle challenges opgelost zijn, gaat het alarm (buzzer) af.

Foto

Foto van het hoofdsysteem (aftelklok en alarm)

Blokschema

Blokschema van het hoofdsysteem (aftelklok en alarm) Blokschema van het hoofdsysteem (aftelklok en alarm)

Code

Toon/verberg code
// hoofdsysteem.ino
#include <IRremote.h>
#include <Servo.h>

const int pinIR = 38;
const int ledPinsRG[] = { 36, 37 };
const int buzzerPin = 39;
const int keyPin = 10;
const int dodgerStartPin = 22;
const int dodgerDiffPin = 23;

Servo keyServo;
Servo diamondServo;

// PIANO
const int triggerPin = 27;
const int echoPin = 26;
float distance = 0.0;
float deadSpace = 10;    // in cm
float maxDistance = 40;  // in cm

long confirmTime = 1000;  // in ms
long zoneStartTimer = 0;
int currentZone = -1;
int previousZone = -1;

int pianoMelodieIndex = 0;
int Melodie[4] = { 0, 2, 1, 3 };            // secret melodie
const int ledPins[4] = { 30, 32, 31, 33 };  // zone leds
const int Notes[4] = { 400, 600, 800, 1000 };

//timer stuff
const int latchPin = 44;
const int clockPin = 43;
const int dataPin = 45;
const int demuxPins[] = { 46, 47 };
volatile uint8_t displayDigits[4] = { 0, 0, 0, 0 };
volatile uint8_t activeDigit = 0;
unsigned long lastDisplayUpdate = 0;
byte numbers[11] = {
  0B00111111,  // 0
  0B00000110,  // 1
  0B01011011,  // 2
  0B01001111,  // 3
  0B01100110,  // 4
  0B01101101,  // 5
  0B01111101,  // 6
  0B00000111,  // 7
  0B01111111,  // 8
  0B01101111,  // 9
  0B00000000   // off
};

// time stuff
const int minutes = 10;
const int seconds = 0;
int totalTime = minutes * 60 + seconds;
int secondWait = 1000;  // 1 sec delay
unsigned long lastSecond = 0;

// IR signal codes
const uint16_t codeEasy = 22;
const uint16_t codeHard = 25;
const uint16_t codeOK = 64;
uint16_t code = codeEasy;

int currentNumber = 0;
bool isOutOfTime = false;
bool gameStarted, gameOver, gameWon, gameWonNotif, gameNotif, isHard = false;
bool pianoComplete = false;
bool waitingConfirm = false;

// https://tgjohnst.github.io/arduino-tone-composer/
int endNoteLength = 8;
int endMusic[8] = { 784, 740, 622, 440, 415, 659, 831, 1047 };
int endMusicDelay[8] = { 100, 100, 100, 100, 100, 100, 100, 300 };

void setup() {
  Serial.begin(115200);
  setLight(code);

  keyServo.attach(7);
  diamondServo.attach(5);

  keyServo.write(100);
  diamondServo.write(5);

  pinMode(dodgerStartPin, OUTPUT);
  pinMode(dodgerDiffPin, OUTPUT);
  digitalWrite(dodgerStartPin, LOW);
  digitalWrite(dodgerDiffPin, LOW);
  pinMode(keyPin, INPUT_PULLUP);
  digitalWrite(keyPin, HIGH);

  pinMode(buzzerPin, OUTPUT);
  pinMode(echoPin, INPUT);
  pinMode(triggerPin, OUTPUT);
  digitalWrite(triggerPin, LOW);

  for (int led : ledPins) {
    pinMode(led, OUTPUT);
  }

  for (int p : ledPinsRG)
    pinMode(p, OUTPUT);

  for (int p : demuxPins)
    pinMode(p, OUTPUT);

  pinMode(latchPin, OUTPUT);
  pinMode(clockPin, OUTPUT);
  pinMode(dataPin, OUTPUT);

  zoneLED(0);
  tone(buzzerPin, Notes[0]);
  delay(200);
  zoneLED(1);
  tone(buzzerPin, Notes[1]);
  delay(200);
  zoneLED(2);
  tone(buzzerPin, Notes[2]);
  delay(200);
  zoneLED(3);
  tone(buzzerPin, Notes[3]);
  delay(200);
  zoneLED(-1);
  noTone(buzzerPin);

  IrReceiver.begin(pinIR, DISABLE_LED_FEEDBACK);
}

void loop() {
  if (gameWon) {
    if (!gameWonNotif) winMusic();
    gameWonNotif = true;
  }

  if (totalTime <= 0 && !gameOver) {
    alarm();
    gameOver = true;
    return;
  }

  updateDisplay();
  updateTime();
  int IRcode = getIRcode();

  // start game setup
  if (gameNotif) {
    for (int led : ledPinsRG) {
      digitalWrite(led, LOW);
    }

    gameNotif = false;
  }

  if (gameStarted) {
    if (!pianoComplete)
      pianoGame();

    if (IRcode == 74) {
      keyServo.write(10);
    }

    if (IRcode == 66) {
      keyServo.write(100);
    }

    if (digitalRead(keyPin) == LOW) {
      diamondServo.write(80);
      gameWon = true;
    } else {
      diamondServo.write(5);
    }
  } else {
    handleIRcode(IRcode);
  }

  // updating the time every second
  if (!gameWon && gameStarted && millis() - lastSecond >= secondWait) {
    if (totalTime > 0) totalTime--;
    lastSecond = millis();
  }
}

// receiver stuff to receive commands
int getIRcode() {
  int signalCode = -1;
  if (IrReceiver.decode()) {
    signalCode = IrReceiver.decodedIRData.command;
    Serial.println(signalCode);
    IrReceiver.resume();
  }
  return signalCode;
}

// switch case for handeling different IR codes
void handleIRcode(int code) {
  switch (code) {
    case codeOK:
      gameStarted = true;
      gameNotif = true;
      break;
    case codeEasy:
      isHard = false;
      setLight(code);
      break;
    case codeHard:
      isHard = true;
      setLight(code);
      break;
  }
}

// sets the RGB led color based on the IR signal
void setLight(int code) {
  switch (code) {
    case codeEasy:
      digitalWrite(ledPinsRG[0], 0);
      digitalWrite(ledPinsRG[1], 1);
      break;
    case codeHard:
      digitalWrite(ledPinsRG[1], 0);
      digitalWrite(ledPinsRG[0], 1);
      break;
  }
}

// oh no watch out cops coming to shoot you
void alarm() {
  static uint32_t oldtime = millis();
  int count = 0;
  int amount = 10;
  int frequency = 500;

  while (count < amount) {
    if ((millis() - oldtime) > 500) {
      tone(buzzerPin, frequency);
      frequency = frequency == 500 ? 700 : 500;
      oldtime = millis();
      count++;
    }
  }
  noTone(buzzerPin);
}

// music when the diamond is freed of greed
void winMusic() {
  for (int i = 0; i < endNoteLength; i++) {
    tone(buzzerPin, endMusic[i]);
    delay(endMusicDelay[i] + 50);
  }
  delay(100);
  noTone(buzzerPin);
}

unsigned long lastDistanceRead = 0;
const unsigned long distanceInterval = 100;  // ms
int currentDistance = -1;

// PIANO
// this is the full paino game
void pianoGame() {
  if (millis() - lastDistanceRead >= distanceInterval) {
    lastDistanceRead = millis();
    currentDistance = measureDistance();
  }
  distance = currentDistance;

  if (distance < deadSpace || distance > maxDistance + deadSpace) {
    currentZone = -1;
  } else if (distance <= (maxDistance * 1 / 4) + deadSpace) {
    currentZone = 0;
  } else if (distance <= (maxDistance * 2 / 4) + deadSpace) {
    currentZone = 1;
  } else if (distance <= (maxDistance * 3 / 4) + deadSpace) {
    currentZone = 2;
  } else if (distance <= maxDistance + deadSpace) {
    currentZone = 3;
  }

  zoneLED(currentZone);
  checkZone(currentZone);
}

// measures distance from the HC-SR04 sensor
int measureDistance() {

  digitalWrite(triggerPin, LOW);
  delayMicroseconds(2);
  digitalWrite(triggerPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(triggerPin, LOW);

  // 5 ms timeout is plenty for ~40 cm
  float timing = pulseIn(echoPin, HIGH, 8000);
  return (timing == 0) ? -1 : timing / 58;
}

void zoneLED(int zone) {
  for (int LED : ledPins) {
    digitalWrite(LED, LOW);
  }

  if (zone != -1) {
    digitalWrite(ledPins[zone], HIGH);
  }
}

// checks the zone for the piano game
//
void checkZone(int zone) {
  // resetting if invalid zone
  if (zone == -1) {
    for (int led : ledPinsRG) digitalWrite(led, LOW);
    previousZone = -1;
    waitingConfirm = false;
    return;
  }

  // check for zone change
  if (zone != previousZone) {
    for (int led : ledPinsRG) digitalWrite(led, LOW);
    zoneStartTimer = millis();
    waitingConfirm = true;
    previousZone = zone;
  }

  // millis based delay for confirming choice
  if (waitingConfirm && (millis() - zoneStartTimer) >= confirmTime) {
    waitingConfirm = false;

    if (Melodie[pianoMelodieIndex] == zone) {
      digitalWrite(ledPinsRG[1], HIGH);
      pianoMelodieIndex++;

      IrReceiver.stopTimer();
      tone(buzzerPin, Notes[zone], 200);
      delay(200);
      noTone(buzzerPin);
      IrReceiver.restartTimer();

      // finish this foking game
      if (pianoMelodieIndex >= 4) {
        pianoComplete = true;
        digitalWrite(dodgerStartPin, HIGH);
        digitalWrite(dodgerDiffPin, isHard);
        for (int led : ledPinsRG) digitalWrite(led, LOW);
        for (int led : ledPins) digitalWrite(led, LOW);
      }
    } else {
      digitalWrite(ledPinsRG[0], HIGH);
      pianoMelodieIndex = 0;

      IrReceiver.stopTimer();
      tone(buzzerPin, 100, 200);
      delay(200);
      noTone(buzzerPin);
      IrReceiver.restartTimer();

      if (isHard) {
        totalTime -= 30;
      }
    }
  }
}


// DIPSLAY
// updates the clock display
void updateDisplay() {
  if (micros() - lastDisplayUpdate < 2000)
    return;

  lastDisplayUpdate = micros();

  updateShiftRegister(displayDigits[activeDigit]);
  selectDigit(activeDigit);

  activeDigit++;
  if (activeDigit >= 4)
    activeDigit = 0;
}

// updates the 74HC595 shift register for all segments of a digit
void updateShiftRegister(uint8_t value) {
  digitalWrite(latchPin, LOW);
  shiftOut(dataPin, clockPin, MSBFIRST, numbers[value]);
  digitalWrite(latchPin, HIGH);
}

// selects digit with the help of a demultiplexer
// 74LS156 demultiplexer with open collectors
void selectDigit(uint8_t digit) {
  digitalWrite(demuxPins[0], digit & 0x01);
  digitalWrite(demuxPins[1], (digit >> 1) & 0x01);
}

// gets the right value for each digit of the clock display
// from totalTime
void updateTime() {
  int totalMinutes = totalTime / 60;
  int totalSeconds = totalTime % 60;

  displayDigits[0] = totalMinutes / 10;
  displayDigits[1] = totalMinutes % 10;
  displayDigits[2] = totalSeconds / 10;
  displayDigits[3] = totalSeconds % 10;
}

1. Air piano

Status: gebouwd en werkend in de doos.

Een afstandssensor meet de afstand tussen zichzelf en een tussenstuk en verdeelt dit in een aantal gebieden. Op basis van de afstand wordt een toon gegeven. De spelers moeten de juiste volgorde aan tonen ingeven om verder te mogen naar de volgende challenge.

Foto

Foto van de air piano-opstelling

Blokschema

Blokschema van de air piano

Code

Toon/verberg code
// air_piano.ino
// ============================================================
//  AIR PIANO - Fase systeem
//  Volgorde: Zone 0 → Zone 2 → Zone 1 → Zone 3
//  HC-SR04: Trig=Pin10  Echo=Pin9
//  Buzzer: Pin 8
//  LEDs: Pin 2, 3, 4, 5
// ============================================================

#define TRIG_PIN     10
#define ECHO_PIN     9
#define BUZZER_PIN   8

const int LED_PINS[4] = {2, 3, 4, 5};  // <-- AANGEPAST van {1,2,3,4}

const int FASE_VOLGORDE[4] = {0, 2, 1, 3};
const int ZONE_MIDDEN[4]   = {5, 15, 25, 35};
const int ZONE_MIN[4]      = {0, 10, 20, 30};
const int ZONE_MAX[4]      = {10, 20, 30, 40};

int  huidigeFase = 0;
bool gewonnen    = false;
long stilSinds   = 0;
bool alBevestigd = false;
int  vorigeZone  = -1;

const int STIL_TIJD = 3000;

void setup() {
  for (int i = 0; i < 4; i++) {
    pinMode(LED_PINS[i], OUTPUT);
    digitalWrite(LED_PINS[i], LOW);
    delay(10);
  }

  pinMode(TRIG_PIN,   OUTPUT);
  pinMode(ECHO_PIN,   INPUT);
  pinMode(BUZZER_PIN, OUTPUT);

  Serial.begin(9600);
  Serial.println("Air Piano gestart!");
  toonVolgendeZone();
  speelStartMelodietje();
}

void loop() {
  if (gewonnen) return;

  int afstand  = meetAfstand();
  int zone     = afstandNaarZone(afstand);
  int doelZone = FASE_VOLGORDE[huidigeFase];

  if (zone == doelZone) {
    int afwijking = abs(afstand - ZONE_MIDDEN[doelZone]);
    int freq      = map(afwijking, 0, 5, 800, 200);
    tone(BUZZER_PIN, freq, 80);
  } else {
    noTone(BUZZER_PIN);
  }

  if (zone != vorigeZone) {
    vorigeZone  = zone;
    stilSinds   = millis();
    alBevestigd = false;

    if (zone == doelZone) {
      Serial.print("Juiste zone! Houd 3 sec stil... (fase ");
      Serial.print(huidigeFase + 1);
      Serial.println(")");
    } else if (zone >= 0) {
      Serial.print("Verkeerde zone. Ga naar ");
      Serial.print(ZONE_MIN[doelZone]);
      Serial.print("-");
      Serial.print(ZONE_MAX[doelZone]);
      Serial.println(" cm");
    }

  } else if (zone == doelZone && !alBevestigd) {
    long stilTijd = millis() - stilSinds;

    int aanmoedigingsToon = map(stilTijd, 0, STIL_TIJD, 300, 900);
    tone(BUZZER_PIN, aanmoedigingsToon, 80);

    if (stilTijd % 500 < 80) {
      Serial.print("Nog ");
      Serial.print((STIL_TIJD - stilTijd) / 1000.0, 1);
      Serial.println(" sec...");
    }

    if (stilTijd >= STIL_TIJD) {
      alBevestigd = true;
      faseSlaagde();
    }
  }

  delay(80);
}

void faseSlaagde() {
  noTone(BUZZER_PIN);

  Serial.print("*** FASE ");
  Serial.print(huidigeFase + 1);
  Serial.println(" VOLTOOID! ***");

  speelFaseMelodie();

  // LED aan NA de melodie
  digitalWrite(LED_PINS[huidigeFase], HIGH);

  huidigeFase++;

  if (huidigeFase >= 4) {
    Serial.println("=== ALLE FASES VOLTOOID! ===");
    speelWinMelodietje();
    alleLedsKnipperen();
    gewonnen = true;
  } else {
    toonVolgendeZone();
    vorigeZone  = -1;
    alBevestigd = false;
  }
}

void toonVolgendeZone() {
  int doelZone = FASE_VOLGORDE[huidigeFase];
  Serial.print("Fase ");
  Serial.print(huidigeFase + 1);
  Serial.print(" -> Doe hand naar ");
  Serial.print(ZONE_MIN[doelZone]);
  Serial.print("-");
  Serial.print(ZONE_MAX[doelZone]);
  Serial.println(" cm");
}

int meetAfstand() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  long duur = pulseIn(ECHO_PIN, HIGH, 30000);
  if (duur == 0) return -1;
  return duur / 58;
}

int afstandNaarZone(int cm) {
  if (cm <= 0 || cm > 40) return -1;
  if (cm <= 10) return 0;
  if (cm <= 20) return 1;
  if (cm <= 30) return 2;
  return 3;
}

void speelFaseMelodie() {
  int noten[] = {523, 659, 784, 1047};
  int duren[] = {150, 150, 150, 400};
  for (int i = 0; i < 4; i++) {
    tone(BUZZER_PIN, noten[i], duren[i]);
    delay(duren[i] + 50);
  }
  noTone(BUZZER_PIN);
}

void speelStartMelodietje() {
  int noten[] = {262, 330, 392, 523};
  int duren[] = {150, 150, 150, 300};
  for (int i = 0; i < 4; i++) {
    tone(BUZZER_PIN, noten[i], duren[i]);
    delay(duren[i] + 50);
  }
  noTone(BUZZER_PIN);
}

void speelWinMelodietje() {
  int noten[] = {523, 523, 523, 698, 523, 698, 784};
  int duren[] = {150, 150, 150, 300, 150, 300, 500};
  for (int i = 0; i < 7; i++) {
    tone(BUZZER_PIN, noten[i], duren[i]);
    delay(duren[i] + 50);
  }
  noTone(BUZZER_PIN);
}

void alleLedsKnipperen() {
  for (int k = 0; k < 6; k++) {
    for (int i = 0; i < 4; i++) digitalWrite(LED_PINS[i], HIGH);
    delay(200);
    for (int i = 0; i < 4; i++) digitalWrite(LED_PINS[i], LOW);
    delay(200);
  }
  for (int i = 0; i < 4; i++) digitalWrite(LED_PINS[i], HIGH);
}

2. Dodger game

Status: gebouwd en werkend in de doos.

De hoofdbewaker beveiligde zijn computer met een dodger game: de speler moet op tijd uitwijken voor obstakels die op het scherm verschijnen. Een tweede speler aan de andere kant moet de bijhorende kleurcode op tijd indrukken om de eerste speler te helpen verder te geraken. Een minimumscore moet behaald worden om door te mogen naar de volgende challenge.

Foto

Foto van de dodger game-opstelling

Code

Toon/verberg code
// dodger_game.ino
// #include <Wire.h>
#include <U8g2lib.h>

U8G2_SH1107_128X128_2_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);

#define SCREEN_W 128
#define SCREEN_H 128
#define BUTTON_PIN 2
#define LOST_PIN 3
#define DIFFICULTY_PIN 4

int playerX = 20;
float playerY = SCREEN_H / 2;
float playerSpeed = 0;
float GRAVITY = 0.8;
float JUMP = -5.0;
const int PLAYER_SIZE = 5;

int objX = SCREEN_W;
int objY = 0;
const int OBJ_W = 8;
const int OBJ_H = 8;
int objSpeed = 3;

int score = 0;
bool gameOver = false;
bool gameStarted = false;
unsigned long lastUpdate = 0;
const int FRAME_MS = 35;
bool lastBtn = HIGH;

void setDifficulty() {
  bool hard = digitalRead(DIFFICULTY_PIN);
  if (hard == HIGH) {
    objSpeed = 5;
    GRAVITY = 1.0;
    JUMP = -6.0;
  } else {
    objSpeed = 3;
    GRAVITY = 0.8;
    JUMP = -5.0;
  }
}

void resetGame() {
  playerY = SCREEN_H / 2;
  playerSpeed = 0;
  objX = SCREEN_W;
  objY = random(6, SCREEN_H - OBJ_H - 6);
  score = 0;
  gameOver = false;
  gameStarted = false;
  digitalWrite(LOST_PIN, LOW);
  setDifficulty();
}

void drawPlayer() {
  u8g2.drawBox(playerX, (int)playerY - PLAYER_SIZE / 2, PLAYER_SIZE, PLAYER_SIZE);
}

void drawObject() {
  u8g2.drawBox(objX, objY, OBJ_W, OBJ_H);
}

bool checkCollision() {
  return (playerX + PLAYER_SIZE > objX &&
          playerX < objX + OBJ_W &&
          (int)playerY - PLAYER_SIZE / 2 < objY + OBJ_H &&
          (int)playerY + PLAYER_SIZE / 2 > objY);
}

void showStartScreen() {
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_helvB08_tr);
    u8g2.drawStr(25, 40, "DODGE GAME");
    u8g2.drawLine(15, 46, 113, 46);
    u8g2.setFont(u8g2_font_profont11_tr);
    u8g2.drawStr(8, 62, "Ontwijkt de blokken!");
    u8g2.drawStr(8, 76, "Knop = spring omhoog");
    u8g2.drawStr(8, 90, "Rand raken = verlies");
    u8g2.setFont(u8g2_font_helvB08_tr);
    u8g2.drawStr(18, 115, "Druk om te starten");
  } while (u8g2.nextPage());
}

void showGameOver() {
  u8g2.firstPage();
  do {
    u8g2.setFont(u8g2_font_helvB08_tr);
    u8g2.drawStr(20, 45, "GAME OVER");
    u8g2.drawLine(15, 51, 113, 51);
    u8g2.drawStr(25, 72, "Score:");
    u8g2.setCursor(72, 72);
    u8g2.print(score);
    u8g2.setFont(u8g2_font_profont11_tr);
    u8g2.drawStr(18, 95, "Druk om opnieuw");
    u8g2.drawStr(28, 108, "te spelen!");
  } while (u8g2.nextPage());
}

void setup() {
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  pinMode(LOST_PIN, OUTPUT);
  pinMode(DIFFICULTY_PIN, INPUT);
  randomSeed(analogRead(A0));
  u8g2.begin();
  resetGame();
}

void loop() {
  bool btn = digitalRead(BUTTON_PIN);

  if (!gameStarted && !gameOver) {
    showStartScreen();
    if (btn == LOW && lastBtn == HIGH) {
      gameStarted = true;
    }
    lastBtn = btn;
    return;
  }

  if (gameOver) {
    digitalWrite(LOST_PIN, HIGH);
    showGameOver();
    if (btn == LOW && lastBtn == HIGH) {
      resetGame();
    }
    lastBtn = btn;
    return;
  }

  unsigned long now = millis();
  if (now - lastUpdate < FRAME_MS) {
    lastBtn = btn;
    return;
  }
  lastUpdate = now;

  playerSpeed += GRAVITY;

  if (btn == LOW && lastBtn == HIGH) {
    playerSpeed = JUMP;
  }

  playerY += playerSpeed;

  if (playerY < PLAYER_SIZE / 2 || playerY > SCREEN_H - PLAYER_SIZE / 2) {
    gameOver = true;
    lastBtn = btn;
    return;
  }

  objX -= objSpeed;

  if (objX + OBJ_W < 0) {
    objX = SCREEN_W;
    objY = random(6, SCREEN_H - OBJ_H - 6);
    score++;
    if (score % 3 == 0) objSpeed++;
  }

  if (checkCollision()) {
    gameOver = true;
    lastBtn = btn;
    return;
  }

  u8g2.firstPage();
  do {
    drawPlayer();
    drawObject();
    u8g2.setFont(u8g2_font_helvB08_tr);
    u8g2.setCursor(90, 12);
    u8g2.print(score);
    u8g2.drawHLine(0, SCREEN_H - 1, SCREEN_W);
  } while (u8g2.nextPage());

  lastBtn = btn;
}

3. Cryptex

Status: gebouwd en werkend in de doos.

Een aparte puzzelbox, zoals een cryptex-puzzel. Deze geeft een sleutel vrij zodra de juiste code wordt ingesteld. Deze sleutel leidt tot de diamant.

Foto

Foto van de cryptex-puzzelbox

Code

Toon/verberg code
// cryptex.ino
//#include <IRremote.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int IRsendPin = 12;  // D6 pin
bool alarmActive = true;

// rotary encoder pins GPIO REMEMBER
const int rotaryCLK = 14;
const int rotaryDATA = 13;
const int rotarySWITCH = 2;

// rotary encoder state stuff
int prevState, lastButtonPush;
int quarterSteps = 0;

// display stuff
const int codeLength = 4;  // adjust to change length code
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
int idx = 0;

String code = "";
String SECRET = "SAFE"; // this is the secret code that the player needs to lay out
byte upArrow[8] = {
  B00000,
  B00100,
  B01110,
  B11111,
  B00000,
  B00000,
  B00000,
  B00000
};

void setup() {
  Wire.begin(4, 5);  // SDA, SCL
  IrSender.begin(IRsendPin);
  Serial.begin(115200);

  pinMode(rotaryCLK, INPUT_PULLUP);
  pinMode(rotaryDATA, INPUT_PULLUP);
  pinMode(rotarySWITCH, INPUT_PULLUP);

  //init display + charValues
  for (int i = 0; i < codeLength; i++) {
    code += "_";
  }

  lcd.init();
  lcd.backlight();
  lcd.createChar(0, upArrow);
  printOnScreen("Good Luck", "");

  delay(1000);

  printOnScreen(code, "");
  updateArrow(codeLength, idx);

  prevState = (digitalRead(rotaryCLK) << 1) | digitalRead(rotaryDATA);
  lastButtonPush = digitalRead(rotarySWITCH);
}

void loop() {
  if (code.equals(SECRET)) {
    endTask();
    return;
  }

  // checking the state of the rotary encoder
  uint8_t clkState = (digitalRead(rotaryCLK) << 1) | digitalRead(rotaryDATA);
  int8_t movement = getMovement(prevState, clkState);

  if (movement != 0) {
    quarterSteps += movement;
    int increment = 0;

    // One full detent clockwise
    if (quarterSteps >= 4) {
      quarterSteps = 0;
      increment = 1;
    }
    // One full detent counter-clockwise
    else if (quarterSteps <= -4) {
      quarterSteps = 0;
      increment = -1;
    }

    if (increment != 0) {
      code = updateChar(code, idx, increment);
    }
  }

  // check if button is pushed and then selects next character
  if (digitalRead(rotarySWITCH) == LOW && lastButtonPush == HIGH) {
    idx = idx >= codeLength - 1 ? 0 : idx + 1;
    updateArrow(codeLength, idx);
    Serial.println("button");
    delay(300);
  }

  prevState = clkState;
  lastButtonPush = digitalRead(rotarySWITCH);
}

// https://en.wikipedia.org/wiki/Rotary_encoder -> how does a rotary encoder work
// https://www.marginallyclever.com/2011/03/using-a-rotary-encoder-with-arduino/ -> encoder states
int8_t getMovement(uint8_t prev, uint8_t curr) {
  static const int8_t table[16] = {
    0, -1, 1, 0,
    1, 0, 0, -1,
    -1, 0, 0, 1,
    0, 1, -1, 0
  };

  return table[(prev << 2) | curr];
}

// ends the game and sends out a signal to open the box with the key
void endTask() {
  if (alarmActive) {
    printOnScreen("Alarm system", "deactivated!");
    delay(3000);
    printOnScreen("Transmitter", "Active!");
  }
  IrSender.sendNEC(0x00, 74, 0);
  delay(200);
  alarmActive = false;
}

// updates character from a string based on an integer increment
// 1) find character at current index in text
// 2) take next character out of list of all characters based on index + increment
// 3) place new character in place in text
// 4) print only character again on lcd to avoid flicker and avoid use of too much resources
String updateChar(String text, int textIndex, int increment) {
  char codeChar = text.charAt(textIndex);
  int currentCharIndex = characters.indexOf(codeChar);
  int newIndex = currentCharIndex + increment;
  newIndex = newIndex < 0 ? characters.length() - 1 : newIndex;
  newIndex = newIndex >= characters.length() ? 0 : newIndex;
  char newChar = characters.charAt(newIndex);
  Serial.println(newChar);

  lcd.setCursor(generateCursorX(codeLength) + textIndex, 0);
  lcd.print(newChar);

  text.setCharAt(idx, newChar);
  return text;
}

// the little arrow that shows which character of the text is selected
// moves the arrow incrementally
void updateArrow(int textLength, int index) {
  int posX = generateCursorX(textLength) + index;
  int prevPosX = index == 0 ? posX + textLength - 1 : posX - 1;
  lcd.setCursor(prevPosX, 1);
  lcd.print(" ");
  lcd.setCursor(posX, 1);
  lcd.write(byte(0));
}

// fucntion to print on screen without having to clear the whole screen
// this avoids flickering
void printOnScreen(String row1, String row2) {
  String clearStr = "";
  for (int i = 0; i < 16; i++) clearStr += " ";

  lcd.setCursor(0, 0);
  lcd.print(clearStr);
  lcd.setCursor(0, 1);
  lcd.print(clearStr);

  int row1CursorX = generateCursorX(row1.length());
  int row2CursorX = generateCursorX(row2.length());

  lcd.setCursor(row1CursorX, 0);
  lcd.print(row1);
  lcd.setCursor(row2CursorX, 1);
  lcd.print(row2);
}

// easy method to get right column to start printing new text
int generateCursorX(int stringLength) {
  return (16 - stringLength) / 2;
}

4. NFC reset

Status: uitgewerkt, maar uiteindelijk uit het finale product gehaald.

Met een NFC-kaart of -tag kan het volledige systeem gereset worden naar de begintoestand, zodat een nieuwe poging gestart kan worden zonder de Arduino opnieuw te moeten programmeren of de stroom te onderbreken.

Foto

Foto van de NFC reset-opstelling

Code

Toon/verberg code (NFCReset.h)
#ifndef NFCRESET_H
#define NFCRESET_H

#include <SPI.h>
#include <MFRC522.h>

class NFCReset {
  private:
    MFRC522 rfid;

    static const int MAX_CARDS = 2;
    static const int MIN_CARDS = 1;

    byte authorizedUIDs[MAX_CARDS][7];
    byte uidSizes[MAX_CARDS];
    int  numUIDs;

    void (*resetCallback)();

    bool matchesUID(byte* uid, byte size, byte* stored, byte storedSize) {
      if (size != storedSize) return false;
      for (byte i = 0; i < size; i++) {
        if (uid[i] != stored[i]) return false;
      }
      return true;
    }

  public:
    NFCReset(byte ssPin, byte rstPin)
      : rfid(ssPin, rstPin), numUIDs(0), resetCallback(nullptr) {}

    void begin() {
      SPI.begin();
      rfid.PCD_Init();
    }

    void onReset(void (*callback)()) {
      resetCallback = callback;
    }

    bool addUID(byte b0, byte b1, byte b2, byte b3) {
      if (numUIDs >= MAX_CARDS) {
        Serial.println("[NFC] Fout: maximaal 2 kaarten toegestaan.");
        return false;
      }
      authorizedUIDs[numUIDs][0] = b0;
      authorizedUIDs[numUIDs][1] = b1;
      authorizedUIDs[numUIDs][2] = b2;
      authorizedUIDs[numUIDs][3] = b3;
      uidSizes[numUIDs] = 4;
      numUIDs++;
      return true;
    }

    bool isReady() {
      if (numUIDs < MIN_CARDS) {
        Serial.println("[NFC] Fout: voeg minstens 1 kaart toe via addUID().");
        return false;
      }
      if (resetCallback == nullptr) {
        Serial.println("[NFC] Fout: geen reset functie gekoppeld via onReset().");
        return false;
      }
      return true;
    }

    int count() {
      return numUIDs;
    }

    void update() {
      if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial())
        return;

      bool authorized = false;
      for (int i = 0; i < numUIDs; i++) {
        if (matchesUID(rfid.uid.uidByte, rfid.uid.size,
                       authorizedUIDs[i], uidSizes[i])) {
          authorized = true;
          break;
        }
      }

      rfid.PICC_HaltA();
      rfid.PCD_StopCrypto1();

      if (authorized && resetCallback != nullptr) {
        Serial.println("[NFC] Kaart herkend — reset!");
        resetCallback();
      } else if (!authorized) {
        Serial.println("[NFC] Onbekende kaart.");
      }
    }
};

#endif

Niet weerhouden, maar wel uitgewerkt

Door tijdsgebrek hebben we onderstaande onderdelen uiteindelijk niet in het eindresultaat opgenomen. Ze zijn echter wel volledig uitgewerkt en gesimuleerd in Tinkercad, hieronder telkens de schematische weergave en de 3D-weergave uit de simulatie.

Dino game

Status: geprobeerd te bouwen, maar werkt niet. Enkel de Tinkercad-simulatie werkt.

De oorspronkelijke challenge gebaseerd op het Dino-spel van Google Chrome, met kleurcodes op de cactussen die door een tweede speler op tijd ingedrukt moesten worden. Vervangen door de Dodger game hierboven.

Open de Tinkercad-simulatie van de Dino game

Tinkercad — schematische weergave

Tinkercad schematische weergave van de Dino game

Tinkercad — 3D-weergave

Tinkercad 3D-weergave van de Dino game

Lasers en spiegels (hard mode-uitbreiding)

Status: niet gebouwd, enkel uitgewerkt en gesimuleerd in Tinkercad.

Een laser die via spiegels (gedraaid met draaiknoppen) een receiver moet bereiken, met meer draaiknoppen dan spiegels zodat de juiste combinatie gevonden moet worden.

Open de Tinkercad-simulatie van lasers en spiegels

Tinkercad — schematische weergave

Tinkercad schematische weergave van lasers en spiegels

Tinkercad — 3D-weergave

Tinkercad 3D-weergave van lasers en spiegels

Onderdelen

Code & bronvermelding

Hieronder vermelden we enkel de externe bibliotheken en codebronnen die we doorheen het project hergebruikt of als basis gebruikt hebben.

OnderdeelBibliotheek / bronLink
Dodger gameGebaseerd op oled-flappy-bird-arduinogithub.com/adidax/oled-flappy-bird-arduino

Datasheets

Datasheets van de gebruikte chips/modules, ter download:

Logica van het project

Onderstaand schema (flowchart) legt uit hoe het spel verloopt: van het instellen van de moeilijkheidsgraad, over de drie challenges, tot het invoeren van de code in de cryptex en het vrijgeven van de diamant, of het afgaan van het alarm bij het overschrijden van de tijdslimiet.

Flowchart van de spellogica

Foto's

Foto's van het geheel en van de afzonderlijke onderdelen van de doos.

Team