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:
-
Button Mash Challenge – Tap a physical button repeatedly until a counter hits the target.
-
Math Riddle Quiz – Use a button to select answers and confirm; one wrong answer ends the game.
-
Note Match – Listen to a played note and match it using one of four physical options.
-
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.