Final Project – Code Black

For my final project, I created a hybrid digital-physical escape room-style game inspired by timed bomb defusal sequences in video games. The entire experience blends physical interactivity through Arduino with dynamic visuals and logic in p5.js. The result is a tense, fast-paced game where players must race against the clock to complete four distinct challenges and successfully input a final disarm code before the device “explodes.”

The core idea was to simulate a bomb defusal setup using varied mini-games—each one testing a different skill: speed, logic, memory, and pattern recognition.

How the Game Works

The digital game runs in p5.js and connects to a single Arduino board that handles inputs from physical buttons, rotary encoders, and switches. Players are given 80 seconds to complete four escalating stages:

  1. Button Mash Challenge – Tap a physical button repeatedly until a counter hits the target.

  2. Math Riddle Quiz – Use a button to select answers and confirm; one wrong answer ends the game.

  3. Note Match – Listen to a played note and match it using one of four physical options.

  4. Morse Code Challenge – Decode a flashing Morse signal and reproduce it using a button.

After all four stages, the player must recall and enter a 4-digit code, derived from the hidden logic behind each stage. If they enter it correctly, the bomb is defused and a green screen confirms success. Otherwise—boom. Game over.

A countdown timer runs persistently, and sound effects, animations, and images change based on player progress, creating an immersive narrative.

Video Demo:  https://drive.google.com/drive/folders/1xghtShbdS5ApygD3-LrT41DRQsbnQ98U?usp=share_link

Hardware & Physical Build

This game relies heavily on Arduino to provide tactile interaction. Here’s how each component contributes:

  • Button Mash: A simple digital input wired to a button switch.

  • Math Quiz: A designated button allows users to scroll through numeric answers, with a button to lock in their choice.

  • Note Match: A speaker plays a pitch generated from Arduino, and players must select the correct note using four distinct buttons.

  • Morse Code: The p5 screen shows a pattern, which the player must replicate with button presses (dots and dashes).

To enhance the look, I created screen graphics for each stage and embedded them as assets into the p5.js sketch. I also used audio cues (success/failure sounds) to give it more feedback.

Code + Serial Integration

The p5.js sketch acts as the game engine and controller, managing state transitions, timing, visuals, and logic. Arduino handles all the physical input and sends data to p5.js over serial using a consistent message format.

Initially, I experimented with sending raw characters for stage signals and player responses, but ran into reliability issues. Eventually, I switched to using numeric values and simple prefixes which made parsing much more predictable.

There’s a small but critical serial timing issue to manage — making sure Arduino doesn’t flood the buffer, and that p5 reads and trims data consistently. I handled this using readUntil("\n").trim() on the p5 side and line breaks on the Arduino side.

I also implemented a game reset trigger — pressing “R” after the game ends resets both the p5 and Arduino states and lets the player start over without refreshing the browser.

Arduino Code:

// initialize connections
const int BUTTON_PINS[] = {2, 3, 4, 5}; 
const int BUZZER_PIN = 8;
const int CONFIRM_PIN = 7;
const int POT_PIN = A0;
const int RED_LED_PIN_1 = 9;
const int YELLOW_LED_PIN_1 = 10;
const int GREEN_LED_PIN_1 = 12;
const int RED_LED_PIN_2 = 13;
const int YELLOW_LED_PIN_2 = 11;
const int GREEN_LED_PIN_2 = 6;

// initialize all game variables
int currentPressed = -1;
int targetNote = -1;
bool newRound = true;
bool morsePressed = false;
unsigned long morseStart = 0;
int buttonMashCount = 0;
int currentGame = 0;

bool bombDefused = false;
bool bombExploded = false;
bool gameEnded = false;

unsigned long gameStartTime;
const unsigned long GAME_DURATION = 80000; 

bool inCountdown = false;
unsigned long lastBeepTime = 0;
unsigned long beepInterval = 1000;

int blinkState = 0; 
unsigned long lastBlinkTime = 0;
unsigned long blinkInterval = 400;
bool ledOn = false;

void setup() {
  Serial.begin(9600);
  while (!Serial);
  // setup and initialize all physical connections
  for (int i = 0; i < 4; i++) pinMode(BUTTON_PINS[i], INPUT_PULLUP);
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(CONFIRM_PIN, INPUT_PULLUP);
  pinMode(POT_PIN, INPUT);
  pinMode(RED_LED_PIN_1, OUTPUT);
  pinMode(YELLOW_LED_PIN_1, OUTPUT);
  pinMode(GREEN_LED_PIN_1, OUTPUT);
  pinMode(RED_LED_PIN_2, OUTPUT);
  pinMode(YELLOW_LED_PIN_2, OUTPUT);
  pinMode(GREEN_LED_PIN_2, OUTPUT);

  randomSeed(analogRead(A1));
  gameStartTime = millis();
}

void loop() {
  if (Serial.available()) {
    String input = Serial.readStringUntil('\n');
    input.trim();

    if (input == "RESET") {
      resetGame(); // reset game variables when reset command is received
      return;
    }
    // to identify target note sent by arduino
    if (input.startsWith("NOTE:")) {
      targetNote = input.substring(5).toInt();  // parses 6th character which holds numeric value
      newRound = true;
      return;
    }
    // if bomb is defused on p5, it sends input to arduino and bomb is defused here as well
    if (input == "DEFUSED"){
      bombDefused= true; 
      gameEnded = true;
      return;
    }
    // in case user makes a mistake in games, p5 sends exploded to arduino
    if (input == "EXPLODED") {
      bombExploded = true;
      gameEnded = true;
      Serial.println("EXPLOSION_ACK");
      return;
    }
    // to parse game sent to arduino each time a challenge is completed and we move to next one
    currentGame = input.toInt();

    if (currentGame == 0) buttonMashCount = 0;
    if (currentGame == 2) newRound = true;
  }
  // when bomb is defused or explodes
  if (gameEnded) {
    noTone(BUZZER_PIN);
    return;
  }
  // turn of all leds 
  if (bombExploded || bombDefused) {
    digitalWrite(RED_LED_PIN_1, LOW);
    digitalWrite(YELLOW_LED_PIN_1, LOW);
    digitalWrite(GREEN_LED_PIN_1, LOW);
    digitalWrite(RED_LED_PIN_2, LOW);
    digitalWrite(YELLOW_LED_PIN_2, LOW);
    digitalWrite(GREEN_LED_PIN_2, LOW);

    noTone(BUZZER_PIN);
  }

  unsigned long elapsed = millis() - gameStartTime;
  // handles blinking of leds alternatively until 30 seconds are left 
  unsigned long remaining = GAME_DURATION - elapsed;
  if (!gameEnded && !bombDefused) {
  if (remaining > 30000) {
    if (millis() - lastBlinkTime >= 400) {
      lastBlinkTime = millis();
      ledOn = !ledOn;

      digitalWrite(RED_LED_PIN_1, LOW);
      digitalWrite(YELLOW_LED_PIN_1, LOW);
      digitalWrite(GREEN_LED_PIN_1, LOW);
      digitalWrite(RED_LED_PIN_2, LOW);
      digitalWrite(YELLOW_LED_PIN_2, LOW);
      digitalWrite(GREEN_LED_PIN_2, LOW);


      if (ledOn) {
        if (blinkState == 0) {
          digitalWrite(GREEN_LED_PIN_1, HIGH);
          digitalWrite(GREEN_LED_PIN_2, HIGH);}
        else if (blinkState == 1) {
          digitalWrite(YELLOW_LED_PIN_1, HIGH);
          digitalWrite(YELLOW_LED_PIN_2, HIGH);}
        else if (blinkState == 2) {
          digitalWrite(RED_LED_PIN_1, HIGH);
          digitalWrite(RED_LED_PIN_2, HIGH);}

        blinkState = (blinkState + 1) % 3;
      }
    }
  }
  // last 30 seconds yellow starts blibking with beeps
  else if (remaining > 13000) {
    if (millis() - lastBlinkTime >= 500) {
      lastBlinkTime = millis();
      ledOn = !ledOn;

      //ensure other LEDs are off
      digitalWrite(RED_LED_PIN_1, LOW);
      digitalWrite(RED_LED_PIN_2, LOW);
      digitalWrite(GREEN_LED_PIN_1, LOW);
      digitalWrite(GREEN_LED_PIN_2, LOW);

      // Yellow blinking
      digitalWrite(YELLOW_LED_PIN_1, ledOn ? HIGH : LOW);
      digitalWrite(YELLOW_LED_PIN_2, ledOn ? HIGH : LOW);
    }
    // beeps
    if (millis() - lastBeepTime >= 1000) {
      lastBeepTime = millis();
      tone(BUZZER_PIN, 1000, 100);
    }
  }
  // last 10 seconds red is blinking with faster beeps
  else if (remaining > 3000) {
    if (millis() - lastBlinkTime >= 300) {
      lastBlinkTime = millis();
      ledOn = !ledOn;
      digitalWrite(RED_LED_PIN_1, ledOn ? HIGH : LOW);
      digitalWrite(RED_LED_PIN_2, ledOn ? HIGH : LOW);
    }

    if (millis() - lastBeepTime >= 500) {
      lastBeepTime = millis();
      tone(BUZZER_PIN, 1200, 100);
    }
  }
}
  // bomb exploded cause time is up
  if (elapsed >= GAME_DURATION && !bombDefused) {
    bombExploded = true;
    gameEnded = true;
    Serial.println("EXPLODED");
    return;
  }
  // Serial input
  

  switch (currentGame) {
    case 0: handleButtonMash(); break;
    case 1: handleMathQuiz(); break;
    case 2: handleNoteMatch(); break;
    case 3: handleMorseCode(); break;
  }
}
// to handle physicak input for each game
void handleButtonMash() {
  static unsigned long lastPressTime = 0;
  static bool lastButtonState = HIGH;
  bool currentState = digitalRead(CONFIRM_PIN);
  // each press sends 1 to p5, which increments counter for current presses 
  if (lastButtonState == HIGH && currentState == LOW && millis() - lastPressTime > 200) {
    buttonMashCount++;
    lastPressTime = millis();
    Serial.println("1");
  }
  lastButtonState = currentState;
}
// resetGame function defined to reset all game variables and start game again
void resetGame() {
  bombDefused = false;
  bombExploded = false;
  gameEnded = false;
  buttonMashCount = 0;
  currentPressed = -1;
  currentGame = 0;
  newRound = true;
  morsePressed = false;
  targetNote = -1;
  morseStart = 0;
  gameStartTime = millis();
  ledOn = false;
  blinkState = 0;
  lastBlinkTime = 0;
  lastBeepTime = 0;
  
  // turn off all LEDs and buzzer
  digitalWrite(RED_LED_PIN_1, LOW);
  digitalWrite(YELLOW_LED_PIN_1, LOW);
  digitalWrite(GREEN_LED_PIN_1, LOW);
  digitalWrite(RED_LED_PIN_2, LOW);
  digitalWrite(YELLOW_LED_PIN_2, LOW);
  digitalWrite(GREEN_LED_PIN_2, LOW);
  noTone(BUZZER_PIN);
}

void handleMathQuiz() {
  static int selectedNum = 0;
  static int lastButtonState = HIGH;
  static unsigned long lastDebounceTime = 0;
  const unsigned long debounceDelay = 200;

  int currentState = digitalRead(BUTTON_PINS[0]); // increment button on pin 2

  // handle incrementing selected number
  if (lastButtonState == HIGH && currentState == LOW && (millis() - lastDebounceTime > debounceDelay)) {
    selectedNum = (selectedNum + 1) % 10;
    Serial.print("SELECT:");
    Serial.println(selectedNum);
    lastDebounceTime = millis();
  }
  lastButtonState = currentState;

  // handle confirmation
  if (digitalRead(CONFIRM_PIN) == LOW) {
    delay(50); 
    // sends selected number to arduino when confirm button is pressed 
    if (digitalRead(CONFIRM_PIN) == LOW) {
      Serial.print("PRESS:");
      Serial.println(selectedNum);
      delay(300); 
    }
  }
}

void handleNoteMatch() {
  static unsigned long toneEndTime = 0;
  static bool isPlayingTarget = false;

  // handle new round target note
  if (newRound) {
    noTone(BUZZER_PIN);
    delay(5);
    digitalWrite(BUZZER_PIN, LOW);
    // plays target note sent by p5
    tone(BUZZER_PIN, getPitch(targetNote), 500);
    toneEndTime = millis() + 500;
    isPlayingTarget = true;
    newRound = false;
    return;
  }

  // handle tone playing completion
  if (isPlayingTarget && millis() > toneEndTime) {
    noTone(BUZZER_PIN);
    isPlayingTarget = false;
  }

  // playing note corresponding to button presses 
  if (!isPlayingTarget) {
    bool anyPressed = false;
    for (int i = 0; i < 4; i++) {
      if (digitalRead(BUTTON_PINS[i]) == LOW) {
        anyPressed = true;
        if (currentPressed != i) {
          noTone(BUZZER_PIN);
          delay(5);
          currentPressed = i;
          tone(BUZZER_PIN, getPitch(i));
          Serial.println(i);
        }
        break;
      }
    }
    // no note should play when button is not pressed
    if (!anyPressed && currentPressed != -1) {
      noTone(BUZZER_PIN);
      currentPressed = -1;
    }
    // send final answer confirmation when button is pressed
    if (digitalRead(CONFIRM_PIN) == LOW) {
      noTone(BUZZER_PIN);
      Serial.println("CONFIRM");
      delay(300);
      newRound = true;
    }
  }
}

void handleMorseCode() {
  static unsigned long lastDebounceTime = 0;
  const unsigned long debounceDelay = 50; // ms
  
  int btn = digitalRead(BUTTON_PINS[0]);  // button on pin 2 is used for sending data
  
  // Button press detection with debouncing
  if (btn == LOW && !morsePressed && (millis() - lastDebounceTime) > debounceDelay) {
    morseStart = millis();
    morsePressed = true;
    lastDebounceTime = millis();
  }
  
  // Button release detection
  if (btn == HIGH && morsePressed) {
    unsigned long duration = millis() - morseStart;
    morsePressed = false;
    
    // short press sends . and long press sends -
    if (duration >= 20) {
      Serial.println(duration < 500 ? "." : "-");
    }
    lastDebounceTime = millis();
    delay(100); 
  }

  // pressing confirm button sends confirm to p5 which then checks if string formed by user matches morse code proivded
  if (digitalRead(CONFIRM_PIN) == LOW) {
    delay(50); // Debounce
    if (digitalRead(CONFIRM_PIN) == LOW) { 
      Serial.println("CONFIRM");
      while(digitalRead(CONFIRM_PIN) == LOW); 
      delay(300);
    }
  }
}
// 4 notes chosen for note match 
int getPitch(int index) {
  int pitches[] = {262, 294, 330, 349};
  return pitches[index];
}

p5js code:

let port;
let connectBtn;
let startBtn;
let baudrate = 9600;

// initiate all flags required
let showWelcome = true;
let showInstructions = false;
let gameStarted = false;
let currentGame = 0;
let gameCompleted = [false, false, false, false];
let codeDigits = [];
let userCodeInput = "";
let correctCode = "";
let bombDefused = false;
let bombExploded = false;
let stageCompleted = false;
let stageCompleteTime = 0;
let stageDigit = -1;
let imgWelcome, imgInstructions, imgButtonSmash, buttonMashSuccessImg, mathQuizSuccessImg, noteMatchSuccessImg, morseCodeSuccessImg, imgMathRiddle, imgNoteMatch, imgMorseCode1,imgMorseCode2, imgBombDefused, imgBombExploded,imgCodeEntry;
let bombSound;
let playedExplosionSound = false;
let successSound;
let playedSuccessSound = false;

// initiate all game variables
let totalTime = 80;
let startTime;

let pressCount = 0;
let targetPresses = 30;
let challengeActive = false;

let selectedNumber = 0;
let correctAnswer = 5;  
let mathAnswered = false;
let feedback = "";

let currentSelection = -1;
let lockedIn = false;
let noteMessage = "";
let noteAnswerIndex = 0;

let morseCode = "";
let userInput = "";
let roundActive = false;
let showSuccess = false;
let showFailure = false;

function preload() {
  imgWelcome = loadImage("start.png");
  imgInstructions = loadImage("instructions.png");
  imgButtonSmash = loadImage("button_smash.png");
  buttonMashSuccessImg = loadImage("stage1_success.png");
  mathQuizSuccessImg = loadImage("stage2_success.png");
  noteMatchSuccessImg = loadImage("stage3_success.png");
  morseCodeSuccessImg = loadImage("stage4_success.png");
  imgMathRiddle = loadImage("math_riddle.png");
  imgNoteMatch = loadImage("note_match.png");
  imgMorseCode1 = loadImage("morse_code1.png");
  imgMorseCode2 = loadImage("morse_code2.png");
  imgBombDefused = loadImage("defused.png");
  imgBombExploded = loadImage("exploded.png");
  bombSound = loadSound('bomb.mp3');
  successSound = loadSound('success.mp3');
  imgCodeEntry = loadImage("code_entry.png")
}

function setup() {
  createCanvas(600, 600);
  textAlign(CENTER, CENTER);

  port = createSerial();

}

function startGame() {
  startTime = millis(); // Set the start time for the timer
  gameStarted = true; // Set the flag to start the game
  currentGame = 0; // Set the current game to 0
  sendGameSwitch(0); // Send game switch signal to Arduino
  startButtonMashChallenge(); // Start the Button Mash Challenge
}

function draw() {
  background(220);
  
  // displays screen for when bomb is defused along with sound effects
  if (bombDefused) {
      image(imgBombDefused, 0, 0, width, height);
    if (!playedSuccessSound) {
        successSound.play();
        playedSuccessSound = true;
      }
      return;
    }
  // displays screen for when bomb is exploded along with sound effects
  if (bombExploded) {
      image(imgBombExploded, 0, 0, width, height);
      if (!playedExplosionSound) {
        bombSound.play();
        playedExplosionSound = true;
      }
      return;
    }

  // Welcome Screen display
  if (showWelcome) {
    image(imgWelcome, 0, 0, width, height);
    return;
  }

  //Instructions Screen display
  if (showInstructions) {
    image(imgInstructions, 0, 0, width, height);
    return;
  }
  // calculates time to keep track of explosion and so on
  let elapsed = int((millis() - startTime) / 1000);
  let remaining = max(0, totalTime - elapsed);
  
  // if time runs out bomb is exploded
  if (remaining <= 0 && !bombDefused) {
    bombExploded = true;
    return;
  }

  // handle all incoming data by reading and sending to function after trimming
  if (port.opened() && port.available() > 0) {
    let data = port.readUntil("\n").trim();
    if (data.length > 0) {
      handleSerialData(data);
    }
  }

  // toggles success screens for all games 
  if (stageCompleted) {
  switch (currentGame) {
    case 0:
      // Show success screen for Button Mash
      image(buttonMashSuccessImg, 0, 0, width, height);
      break;
    case 1:
      // Show success screen for Math Quiz
      image(mathQuizSuccessImg, 0, 0, width, height);
      break;
    case 2:
      // Show success screen for Note Match
      image(noteMatchSuccessImg, 0, 0, width, height);
      break;
    case 3:
      // Show success screen for Morse Code
      image(morseCodeSuccessImg, 0, 0, width, height);
      break;
  }
    // removes success screen afte 3 seconds and moves onto next game
    if (millis() - stageCompleteTime > 3000) {
      codeDigits.push(stageDigit);
      currentGame++;
      sendGameSwitch(currentGame);
      stageCompleted = false;

      // start the next game
      switch (currentGame) {
        case 1: startMathQuiz(); break;
        case 2: startNoteMatchChallenge(); break;
        case 3: startMorseCodeChallenge(); break;
        case 4: correctCode = "4297"; break; 
      }
    }
    return;
  }

  // display game screens using functions defined 
  switch (currentGame) {
    case 0: drawButtonMashChallenge(); break;
    case 1: drawMathQuiz(); break;
    case 2: drawNoteMatchChallenge(); break;
    case 3: drawMorseCodeChallenge(); break;
    case 4: 
      correctCode = "4297";
      if (userCodeInput === "") startCodeEntry();
      drawCodeEntry();
      break;

  }
  // timer display at top of screen
  if (gameStarted && !bombDefused && !bombExploded) {
    textSize(20);
    fill(0);
    textAlign(CENTER, TOP);
    text("Time Remaining: " + remaining + "s", width / 2, 20);
  }

}

function handleSerialData(data) {
  
  if (bombDefused) {
    return; // no data should be handled if bomb has been defused 
  }
  // stop handing data once bomb explodes
  if (data === "EXPLODED") {
    bombExploded = true;
    if (port.opened()) {
      port.write("EXPLODED\n");
    }
    return;
  }
  switch (currentGame) {
    case 0:
      if (data === "1" && challengeActive) {
        pressCount++;
        //  checks success condition, when user presses button 30 times
        if (pressCount >= targetPresses) {
          challengeActive = false;
          // handle necessary flags for this stage and keep track of time for success screen display
          stageDigit = 4;  
          gameCompleted[0] = true;
          stageCompleted = true;
          stageCompleteTime = millis();
        }
      }
      break;

    case 1:
      if (data.startsWith("SELECT:")) {// parses data for this specific game
        selectedAnswer = int(data.substring(7));  // 8th character gives actual numeric value
      } else if (data.startsWith("PRESS:")) {  // for confirm button press
        let val = int(data.substring(6));  // 7th character gives digit confirmed by user 
        // success condition
        if (val === correctAnswer) {
          feedback = "CORRECT";
          stageDigit = 2;
          gameCompleted[1] = true;
          stageCompleted = true;
          stageCompleteTime = millis();
        } else {
          // in case of wrong answer
          bombExploded = true;
          if (port.opened()) {
            port.write("EXPLODED\n");
          }
        }
      }
      break;
    // handling data for note match game
    case 2:
      // if user presses confirm button, checks answer
      if (!lockedIn) {
        if (data === "CONFIRM") {
          lockedIn = true;
          // if correct answer is selected 
          if (currentSelection === noteAnswerIndex) {
            noteMessage = "Correct!";
            stageDigit = 9;
            gameCompleted[2] = true;
            stageCompleted = true;
            stageCompleteTime = millis();
            // if user makes a mistake, they lose
          } else {
            bombExploded = true;
            if (port.opened()) {
              port.write("EXPLODED\n");
            }
          }
        } else if (!isNaN(int(data))) {
          currentSelection = int(data);  // reading data for option selected 
        }
      }
      break;
    // parsing user input based on arduino feedback to concatenate morse code and compare with original string
    case 3:
      if (data === "." || data === "-") {
        userInput += data;
        // if user confirms answer 
      } else if (data === "CONFIRM") {
        if (userInput === morseCode) {
          showSuccess = true;
          stageDigit = 7;
          gameCompleted[3] = true;
          stageCompleted = true;
          stageCompleteTime = millis();
          roundActive = false;
          // in case of incorrect answer
        } else {
          bombExploded = true;
          if (port.opened()) {
            port.write("EXPLODED\n");
          }
        } // displays morse code for 5 seconds for user to memorize then disappears
        setTimeout(() => {
          showSuccess = false;
          showFailure = false;
          userInput = "";
        }, 5000);
      }
      break;

    case 4:
      // handles code entry
      if (data === "CONFIRM") {
        if (userCodeInput.length !== 4) return; // Ignore if code is incomplete
        if (userCodeInput === "4297") {
          bombDefused = true;
        } else {
          bombExploded = true;
          if (port.opened()) {
            port.write("EXPLODED\n");
          }
        }
      }
      break;
  }
}
// to tell arduino to switch to game being sent
function sendGameSwitch(gameNum) {
  if (port.opened()) {
    port.write(gameNum + "\n");
  }
}

// all game display functions
function startButtonMashChallenge() {
  pressCount = 0;
  challengeActive = true;
}

function drawButtonMashChallenge() {
  image(imgButtonSmash, 0, 0, width, height);
  fill(255);
  textSize(44);
  textAlign(CENTER, CENTER);
  text(pressCount, 300,325); 
}

function startMathQuiz() {
  feedback = "";
  correctAnswer = 5; 
  selectedAnswer = 0;
}


function drawMathQuiz() {
  image(imgMathRiddle, 0, 0, width, height);

  fill(29,148,94);
  rect(width / 2 - 40, 350, 80, 80);
  fill(0);
  textSize(48);
  text(selectedAnswer, width / 2, 370);

}

function startNoteMatchChallenge() {
  lockedIn = false;
  noteMessage = "";
  currentSelection = -1;
  noteAnswerIndex = floor(random(0, 4));
  sendNoteChallenge(noteAnswerIndex);
}

function drawNoteMatchChallenge() {
  image(imgNoteMatch, 0, 0, width, height);
  
  textSize(24);
  textAlign(CENTER, CENTER); 
  fill(0);

  let labels = ["C", "D", "E", "F"];
  let size = 80;
  let spacing = 20;
  let totalWidth = labels.length * size + (labels.length - 1) * spacing;
  let startX = (width - totalWidth) / 2;
  let y = 300;

  for (let i = 0; i < labels.length; i++) {
    let x = startX + i * (size + spacing);
    
    if (i === currentSelection) {
      fill(0, 0, 255);
    } else {
      fill(255);
    }

    rect(x, y, size, size);

    fill(0);
    text(labels[i], x + size / 2, y + size / 2); 
  }

  fill(0);
  textSize(20);
  text(noteMessage, width / 2, height - 50);
}


function startMorseCodeChallenge() {
  morseCode = "..-.--.";
  userInput = "";
  roundActive = true;
  showSuccess = false;
  showFailure = false;
  setTimeout(() => {
    roundActive = false;
  }, 5000);
}
// displays image with code for 5 seconds for user to memorize code 
function drawMorseCodeChallenge() {
  if (roundActive) {
    image(imgMorseCode1, 0, 0, width, height);
  } else {
    image(imgMorseCode2, 0, 0, width, height);}
  fill(50,50,50);
  textSize(24);
  text("User input: " + userInput, width / 2, 300);
}

function drawCodeEntry() {
  image(imgCodeEntry, 0, 0, width, height);
  textSize(24);
  fill(0);
  text(userCodeInput, width / 2, 170);

  for (let i = 0; i <= 9; i++) {
    let x = 140 + (i % 5) * 80;
    let y = 220 + floor(i / 5) * 80;
    fill(200);
    rect(x, y, 60, 60);
    fill(0);
    text(i, x + 30, y + 30);
  }

  fill(255);
  rect(width / 2 - 35, 410, 70, 40);
  fill(0);
  text("Clear", width / 2, 420);
}

function mousePressed() {
  // handles navigation from welcome screen to instructions screen and instructions screen and back
  if (showWelcome) {
    if (mouseX > 112 && mouseX < 224 && mouseY > 508 && mouseY < 547) {
      try {
        // creating serial connection
        if (!port.opened()) {
          let usedPorts = usedSerialPorts();
          if (usedPorts.length > 0) {
            port.open(usedPorts[0], baudrate);
          } else {
            port.open(baudrate);
          }
        }
        console.log("Connected to serial!");
        startGame();
        showWelcome = false;
      } catch (err) {
        console.error("Connection failed:", err);
      }
    }
    
    if (mouseX > 275 && mouseX < 544 && mouseY > 506 && mouseY < 545) {
      showInstructions = true;
      showWelcome = false;
    }
    return;
  }

  if (showInstructions) {
    // Click anywhere to go back
    showInstructions = false;
    showWelcome = true;
  }
  // checks code entry
  if (currentGame === 4 && !bombDefused && !bombExploded) {
    for (let i = 0; i <= 9; i++) {
      let x = 140 + (i % 5) * 80;
      let y = 220 + floor(i / 5) * 80;
      if (mouseX > x && mouseX < x + 60 && mouseY > y && mouseY < y + 60) {
        userCodeInput += i;
        return;
      }
    }

    // clear button
    if (mouseX > width / 2 - 30 && mouseX < width / 2 + 30 &&
        mouseY > 400 && mouseY < 440) {
      userCodeInput = userCodeInput.slice(0, -1);
      return;
    }
    // successful code entry defuses bomb successfully
    if (userCodeInput.length === 4) {
      if (userCodeInput === correctCode) {
        bombDefused = true;
        port.write("DEFUSED\n");
      }
      else bombExploded = true;
    }
  }
}
// for arduino to choose note sent as correct note and play it for users to guess 
function sendNoteChallenge(noteIndex) {
  if (port.opened()) {
    port.write("NOTE:" + noteIndex + "\n");
  }
}
function startCodeEntry() {
  userCodeInput = "";
}

// resets all game variables for reset functionality once game ends 
function resetGame() {
  showWelcome = true;
  showInstructions = false;
  gameStarted = false;
  currentGame = 0;
  gameCompleted = [false, false, false, false];
  codeDigits = [];
  userCodeInput = "";
  correctCode = "";
  bombDefused = false;
  bombExploded = false;
  stageCompleted = false;
  stageCompleteTime = 0;
  stageDigit = -1;
  pressCount = 0;
  challengeActive = false;
  selectedAnswer = 0;
  correctAnswer = 5;
  feedback = "";
  currentSelection = -1;
  lockedIn = false;
  noteMessage = "";
  noteAnswerIndex = 0;
  morserrCode = "";
  userInput = "";
  roundActive = false;
  showSuccess = false;
  showFailure = false;
  playedExplosionSound = false;
  playedSuccessSound = false;
}
// if user presses key once game is over, it restarts everything
function keyPressed() {
  if (key === 'R' || key === 'r') {
    resetGame();
    port.write("RESET\n");
  }
}
Challenges & Lessons Learned
  • Serial Port Management: One recurring headache was managing serial port connections on browser refreshes and game resets. I had to add logic to prevent re-opening already open ports to avoid exceptions.

  • Real-Time Feedback: Timing and responsiveness were crucial. Since the game runs on a strict timer, any lag in serial communication or missed input could break the experience. Careful buffering and validation were necessary.

  • Game Flow Management: Keeping track of game state across 5 different modes, plus timers and sounds, took careful design. The stageCompleted flag and a timed transition window after each success proved essential.

Leave a Reply