Week 5 – Reading Response ( Computer Vision for Artists and Designers: Pedagogic Tools and Techniques for Novice Programmers )

Human vision and computer vision are very different. Humans are born with natural senses that let us see, feel, and understand what’s happening around us without even thinking about it. For example, if I see someone waving at me, I instantly know who they are and what that action means. Computers don’t have this kind of built-in understanding. They need to be given artificial “senses” through cameras and sensors. Instead of just seeing, a computer has to go step by step, detecting pixels, separating the object from the background, and then tracking where it moves. Humans process all of this automatically and with emotion, while computers rely only on data and instructions.

To help computers track what we want, we can use techniques like motion detection, background subtraction, brightness levels, and object tracking. These methods allow the computer to focus on what matters, like spotting movement or identifying a specific shape or color.

Computer vision brings something really exciting to the world of art because it makes art interactive. It allows artists, even those who aren’t very technical, to push their creativity further without as many limits. I find this so beautiful because it opens up new ways to experience and create art.

A great example of this is the Rain Room. In this installation, rain falls inside a dark room, but when someone walks through it, sensors detect their movement and stop the rain wherever they are standing. It feels like you have the power to control the rain. Another example is TeamLab Borderless, where massive digital projections respond to visitors’ movements, creating an ever-changing world of color and light.

What makes this so special is that it gives the audience a role in shaping the art. The original artist creates the setup, but every visitor changes how the piece looks and behaves through their actions. In a way, everyone becomes an artist. The artwork is never exactly the same twice, which makes it even more meaningful. It turns art into a shared, living experience where the line between the creator and the audience disappears.

Week 5 – Game Concept

This weeks focus : 

This week is the first week I’m working on my midterm project, I decided to focus on the game concept so I can have clear direction, I planned my entire concept from A to Z, but I did not focus on the technical side because I decided the most efficient way to start is with a clear plan.

The basket adventures : game concept

Goal:

Catch falling candies to score points and survive as long as possible, while avoiding poison candies that make you lose lives.

Lives

Player starts with 3 lives.

• Lose 1 life for each poison candy caught.

• When lives = 0 → Game Over screen appears.

Scoring:

+5 points for every second survived.

+10 points for each candy caught (optional extra boost).

• Display score at the top at all times.

Difficulty:

As time goes on, falling objects spawn faster and fall quicker.

Power-up (Immunity Mode):

• A special glowing candy sometimes falls.

• When caught → player becomes immune to poison for 10 seconds.

• A countdown timer appears on screen during immunity.

Game Modes (Themes)

The four modes are mostly visual changes:

1. Witch Land:

• Background: Haunted forest.

• Good objects: candy corn, pumpkins.

• Poison: Skulls or green potion bottles.

  • Power up: Bat.

2. Unicorn Land:

• Background: Pastel rainbow sky.

• Good objects: Cupcakes, rainbow candies.

• Poison: Spiky black storm clouds.

  • Power up: Star.

3. Funky Land:

• Background: Disco dance floor.

• Good objects: Ice cream.

• Poison: Gray or dull-colored shapes.

• Power up: sunglasses.

4. Animal Kingdom:

• Background: Jungle scene.

• Good objects: Bananas,  coconuts.

• Poison: snakes.

     • power up: leaves.

Different Screens

Game Flow Description

Home Page (Adventure Selection):

• The first screen shows the message:

“Where do you want to take the basket on an adventure?”

• The player chooses one of four themes:

1. Witch Land

2. Unicorn Land

3. Funky Land

4. Animal Kingdom

• Once a theme is chosen, it moves to the Instructions Page.

Instructions Page:

• Shows the selected theme background at the top.

• Displays clear instructions:

• What the good candy looks like and that it increases your score.

• What the poison candy looks like and that it takes away one life.

• The power-up candy image and that it gives immunity for 10 seconds.

• Controls for moving the basket (left and right arrow keys).

• How the scoring works:

“You gain 5 points for every second you survive.”

• A button or message appears: “Press SPACE to start!”

Gameplay:

• The selected theme’s background is displayed.

• Player moves the basket left and right to catch falling candies.

Good candy: Increases score.

Poison candy: Decreases lives by 1.

Power-up: Activates 10-second immunity with a visible countdown timer.

• The game gets faster and harder over time.

• When lives reach 0, the game ends and moves to the Game Over screen.

Game Over Screen:

• Displays:

Final score.

• A message: “Game Over!”

• Two options for the player:

1. Restart:

• Restarts the same theme immediately.

2. Choose New Theme:

• Returns to the Home Page where they can select a different theme.

At first, I thought of making a simple game with a basket catching candies because it felt like the kind of game anyone could play to relax or pass the time. I also wanted something my little siblings could enjoy, so I asked them for their thoughts. My younger sister immediately said it should be in a magical unicorn land, while one of my brothers insisted on a spooky witch land, and my other brother said it had to be in an animal kingdom. That’s when I realized it would be more fun and interactive to add multiple themed worlds. This way, the game could appeal to a wider audience and let players choose their own adventure, making it more exciting and personal for everyone who plays.

 

Code  highlight:

Figuring out the button placement was tricky at first. I used percentages for the positions and sizes instead of fixed numbers so the buttons would stay in the right spot no matter the screen size. It took a lot of trial and error to line them up perfectly with the image, and small changes made a big difference. Even though it was frustrating, I learned how to make my game flexible and work well on different devices.

// buttons placement based on background image
const buttonsPct = {
  unicorn: { xPct: 0.16, yPct: 0.23, wPct: 0.30, hPct: 0.26 }, // top-left
  animal:  { xPct: 0.54, yPct: 0.23, wPct: 0.30, hPct: 0.26 }, // top-right
  funky:   { xPct: 0.16, yPct: 0.60, wPct: 0.30, hPct: 0.26 }, // bottom-left
  witch:   { xPct: 0.54, yPct: 0.60, wPct: 0.30, hPct: 0.26 }  // bottom-right
};

 

Reading Response – Week 4

One thing that drives me crazy is when I’m looking for a parking spot in a busy garage that uses red and green lights to show if a spot is taken or free. Yesterday, I was rushing to make it to an appointment. I saw a spot far away, but then I noticed a green light closer to the mall entrance, which was supposed to mean the space was free. I drove toward it, only to find a car already parked there. By the time I went back to the original spot, it was taken. It was so frustrating and a complete waste of time.

From Norman’s perspective, this is a design problem caused by poor visibility and misleading signals. The green light gave me a false cue, similar to the confusing doors and devices he describes. A better design would use more accurate sensors that reliably detect when a car is present. It could even provide feedback, like showing when the spot was last updated, so drivers can trust what they see. This would prevent wasted time and make the whole parking experience smoother and less stressful.

     

When working on interactive media, especially projects that are heavy on user engagement, I would apply Norman’s principles by making the instructions very clear and giving clear, immediate feedback to every action. Norman emphasizes visibility and feedback, which are key for helping users understand what to do and what is happening as they interact with the program.

I think even in my p5.js projects, I can start practicing this. For example, if a user types an invalid input, instead of the program crashing, I could display a clear message telling them what went wrong and how to fix it. This way, the user isn’t left confused or frustrated. A good approach is to design as if the person using the program is a child, everything should be simple, obvious, and easy to understand without needing extra instructions. This makes the experience smoother, more engaging, and aligned with Norman’s idea of good, user-centered design.

Week 4 – Loading Data, Displaying text

I speak both Arabic and English, so when I learned that our project had to involve words, I knew I wanted to create something that brought both languages together. My first idea was simple: a program that translates colors typed in English into Arabic and displays them on the screen. At first, I liked it, but I quickly realized it felt too basic. Each time you typed a color, the same thing happened, for the same input you got the same output every single time.

To improve it, I thought to add a quiz that tested you on three random colors each time. This added some variation, but it still didn’t feel interactive enough. I wanted users to not just type answers but to interact with the piece and feel like they were shaping the experience. As I worked on making the program more dynamic, I found a YouTube video that helped me learn how to add randomness and interactivity to my code: https://youtu.be/-6v_AYyn49k?si=Ra0T98ejk4Xkcr-J. From that tutorial, I learned how to make the Arabic words appear in different random positions and sizes and how to create multiple modes so the user can switch between still, bouncing, and floating text. This gave the piece a balance between control and randomness, the user can make choices, but there’s still some sort of randomness. I also used custom fonts from Google Fonts by embedding them directly into my HTML file.

In the final version, I kept my original concept of translating colors, but now the project feels much more alive. The user can explore different modes, play with the experience, and see something new each time. It’s almost as if its art created by the words now. It’s structured enough for the user to have some control, but there’s always randomness, so it’s never exactly the same twice. For me, this connects to Chris Crawford’s ideas about interactivity, where both the user and the program are working together to create something meaningful, rather than the program just showing a static result. It’s no longer just a translation tool, it’s an interactive artwork that bridges both of my languages while letting others engage with it in a fun and personal way.

When I first added Arabic text to my program, it kept printing from left to right, which looked really strange because Arabic is supposed to flow right to left. It took me a couple of tries to figure out why this was happening. Eventually, I realized I needed to set the text direction directly in the code. Once I added a line to force the drawing context to use RTL, the Arabic words finally displayed correctly. It was a small but important fix that made the project look much more natural and readable.

// Draw Arabic word correctly (right to left)
   push();
   drawingContext.direction = 'rtl'; 
   textFont(arabicFont);
   textSize(item.size);
   fill(item.color);
   text(item.word, item.x, item.y);
   pop();
 }

 

Week 3 – Reading Response ( The Art of Interactive Design )

 

I think a strongly interactive system is one that feels like it’s really engaging with you, almost like having a conversation. The author describes interactivity as two sides listening, thinking, and responding, and I see that especially in how responsive a system is. When I was younger, I joined a LEGO robotics competition and built a robot from scratch. At first, it didn’t do anything, but once we added the Arduino parts and coded it, it started moving and reacting to our commands. It felt alive, like it was “listening” to us.

Artificial intelligence takes this even further. I heard about a study where researchers kept breaking a robot’s leg, and instead of shutting down, it figured out a new way to walk. That’s incredibly interactive because it shows learning and adaptation. Even something like Siri or Alexa shows this on a smaller scale, you ask a question, and it responds right away. For me, a truly interactive system listens, adapts, and almost feels like a partner, not just a machine following instructions.

I love how they made something normal we play with everyday interactive.

One idea I have for improving the degree of user interaction in my p5 sketches is adding an AI chat voice, kind of like Siri. Instead of having to leave the sketch and go back to the coding page whenever I have a question or need to fix something, I could just talk to the AI while working on my project. It would feel like we’re coding “together,” where I can ask questions out loud and get instant feedback or suggestions. This would make the process more fun and interactive, and it would keep me focused on creating instead of constantly switching between different pages or tools.

Week 3 – Functions, Arrays, and Object-Oriented Programming

For this project, I started with the really basic car code that was shared with us as an example. I liked how simple and clear it was, so I decided to build on that idea. I kept the general structure and movement the same, but instead of a car, I turned it into a spider. That meant changing the shapes, adding legs, and making it look a little more alive.

One thing I found pretty challenging was getting the legs to wiggle naturally. At first, they either didn’t move at all or moved in a really awkward way. It took me some trial and error, especially using functions like sin() and playing around with timing, to get it just right. In the end, I was able to make the spider’s legs move in a subtle way, which makes it feel like it has a bit of personality as it crawls around the canvas.
// make legs wigle
  legWiggle(index) {
    const phase = index * 10;
    return sin((frameCount + phase) * 0.05) * (this.legLength * 0.15);
  }
This piece of code makes the spider’s legs wiggle a bit, and it was honestly the hardest part for me. I had to keep experimenting with the numbers until the movement looked smooth and natural instead of random or awkward.

Assignment 1: Self-Portrait

This self-portrait is my attempt to visualize what people might picture when they hear my name, focusing on two key identifiers: coding and my hair bun. I chose to create a simple, stylized sketch to capture these core components in a straightforward way. The character is drawn in a minimalist profile, with the hair bun as the main feature. To represent my passion for programming, the entire scene is set against a dark, digital background filled with binary code.

Here is the link for my sketch

 

 

 

Final Documentation

Button Beats!

My final project is a game inspired by a popular game called Piano Tiles called Button Beats. Users are able to physically play the game using physical buttons, while the gameplay is displayed on a laptop screen and recreated in p5js, with some differences like a life powerup through the special golden tile.

Video of gameplay: https://drive.google.com/file/d/1q-BEMe4s6vl2vXgGhi7uOwFdDXdhTPeO/view?usp=sharing

Arduino

My arduino program is in charge of sending all the serial inputs from the push buttons when a player presses a key on the piano to the p5js. This is similar to the musical instrument assignment we did in class except the speaker is not in the arduino but rather music playing on the computer from p5 as long as the player pressed the right key in the right time frame. The arduino code reads the serial input from the button and sends that to p5 as A, B, C, or D depending on which button was clicked. The code for this can be seen below:

const int buttonPins[] = {2, 3, 4, 5};
const char buttonChars[] = {'A', 'B', 'C', 'D'};
const int numButtons = 4;

void setup() {
  Serial.begin(9600);
  for (int i = 0; i < numButtons; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }
}

#sends the serial of the button pressed as the letter A, B, C, or D 
void loop() {
  for (int i = 0; i < numButtons; i++) {
    if (digitalRead(buttonPins[i]) == LOW) {
      Serial.println(buttonChars[i]);
      delay(200);
    }
  }
}

P5JS

The p5js runs the code for the entire game. It sets up all the game states and all the game logic. The player will see a welcome screen at the start explaining the rules and a way to connect to the Arduino. Once the user presses enter, the game begins, the music starts playing, and the tiles are randomly generated for the user to physically press on the arduino. When a button is pushed on the arduino, the p5js receives this and follows the logic provided in p5js to ensure that there is currently a tile in that lane and marks that as pressed. If not, the player loses a life. Also, if the player fails to press the button before the tile leaves the screen, they lose a life. If a player loses all their lives and makes a mistake, the game goes to the game over screen and gives the player the option to restart. Here are pictures of all the game states:

Throughout the game, there is a chance to gain extra lives if the golden tile appears which has a 2.5% probability of appearing  at any time in the game. The black tiles also appear randomly, but do speed up as the game goes on which increases the game’s difficulty. The score is kept based on how many tiles you pressed before losing all your lives. Here is the link to the game: https://editor.p5js.org/izza.t/full/moFOZkumG

The circuit consists of the 4 push buttons connected to pins 2, 3, 4, and 5 as well as ground. Here is the schematic below:

I’m particularly proud of the way the game handles the inputs and creates a game that looks pretty similar to the original piano tiles itself. It was really fun from a user perspective to be able to use actual buttons to play the game instead of tapping on a screen. The game working and knowing when you’ve missed a tile or when a tile goes off screen is also something I’m pretty proud of.

One thing though is that the tiles are randomly generated and there is only one song. In the future, it would be nice to have it such that the keys are synced up to the song as is in the game Piano Tiles itself as well as options to select different songs.

 

 

Week 14 – Final Project Documentation

Concept Overview

“Sketch and Switch” is a modern take on the Etch-A-Sketch, but with a twist, where the user can draw with random colored lines with mechanical knobs, as this version uses Arduino hardware to control a digital canvas through two potentiometers and a button. The potentiometers allow left/right and up/down movement of the drawing point, and a button press toggles drawing mode while randomly altering the line’s color and thickness.

Project Interaction

Video interaction: https://drive.google.com/file/d/1h1HtV-_-JUEKgieFiu1-NDM2Pb2Thfwr/view?usp=sharing

Interaction Design

Users interact with two potentiometers and a button:

    • Left Potentiometer: Controls horizontal (X-axis) movement.
    • Right Potentiometer: Controls vertical (Y-axis) movement.
    • Button: Toggles drawing mode and changes the drawing color randomly.

Arduino Code

const int potX = A0;         // Potentiometer for horizontal movement
const int potY = A1;         // Potentiometer for vertical movement
const int buttonPin = 2;     // Pushbutton for toggling draw mode

int lastButtonState = HIGH;              
unsigned long lastDebounceTime = 0;      // Timestamp of last button state change
unsigned long debounceDelay = 20;        // Debounce time to avoid false triggers

void setup() {
  Serial.begin(115200);                  
  pinMode(buttonPin, INPUT_PULLUP);      
}

void loop() {
  int xVal = analogRead(potX);           // Read horizontal potentiometer value
  int yVal = analogRead(potY);           // Read vertical potentiometer value

  int reading = digitalRead(buttonPin);  
  int btnState = LOW;                    // Default button state to not pressed

  if (reading != lastButtonState) {
    lastDebounceTime = millis();
  }

  if ((millis() - lastDebounceTime) > debounceDelay) {
    btnState = (reading == LOW) ? 1 : 0;  // Set button state (pressed = 1)
  }

  lastButtonState = reading;

  // Send formatted data: x, y, buttonState
  Serial.print(xVal);
  Serial.print(",");
  Serial.print(yVal);
  Serial.print(",");
  Serial.println(btnState);

  delay(20); // Small delay to reduce serial flooding
}

Circuit Schematic

 

p5.js Code

let port;
let connectButton, disconnectButton, finishButton, startButton, saveButton, statusText;
let xPos = 0;
let yPos = 0;
let drawingEnabled = false;
let isConnected = false;
let prevX, prevY;
let lastButtonState = 0;
let started = false;
let tutorialShown = false;
let currentColor;
let studentImg;
let tutorialButton;

function preload() {
  studentImg = loadImage("Shamsa.PNG"); // preload image for the intro screen
}

function setup() {
  createCanvas(1280, 720); // fixed landscape canvas size
  background("#F5F5DC");

  port = createSerial(); // setup WebSerial port
  currentColor = color(random(255), random(255), random(255)); // initial random color

  // Setup start screen
  startButton = createButton("Start");
  styleButton(startButton);
  positionCenter(startButton, 0, 60);
  startButton.mousePressed(() => {
    startButton.hide();
    tutorialShown = true;
    showTutorialScreen(); // go to tutorial screen before drawing
  });

  statusText = createP("Status: Not connected");
  statusText.position(10, 10);
  statusText.hide(); // hidden until drawing mode begins
}

function styleButton(btn) {
  // Apply consistent style to all buttons
  btn.style("background-color", "#CF877D");
  btn.style("color", "black");
  btn.style("border-radius", "10px");
  btn.style("padding", "10px 15px");
  btn.style("font-size", "14px");
  btn.style("border", "none");
}

function positionCenter(btn, offsetX, offsetY) {
  // Center button horizontally/vertically with optional offset
  btn.position((width - btn.size().width) / 2 + offsetX, (height - btn.size().height) / 2 + offsetY);
}

function showTutorialScreen() {
  clear();
  background("#F5F5DC");

  // Instructions and disclaimer
  textAlign(CENTER);
  fill("#a8423d");
  textSize(32);
  text("Welcome to Sketch & Switch!", width / 2, 80);

  textSize(20);
  fill(0);
  text(
    "Disclaimer:\nThe blue knobs may be difficult at first, so twist them slowly and gently.\n" +
    "The one on the right moves ↑↓, and the one on the left moves ←→",
    width / 2, 160
  );

  text(
    "Instructions:\n1. Press 'Connect' to connect to your drawing device\n2. Twist knobs to move\n" +
    "3. Press the button on the board to change color (it will be randomized)\n" +
    "4. When finishing the drawing, click 'Finish Drawing' to clear it,\n" +
    "   or click 'Save as PNG' to download your art.\n\n Tip: Clockwise = ↑ or →, CounterClockwise = ↓ or ←",
    width / 2, 320
  );

  // Begin actual drawing
  tutorialButton = createButton("Start Drawing");
  styleButton(tutorialButton);
  tutorialButton.position(width / 2 - 70, height - 100);
  tutorialButton.mousePressed(() => {
    tutorialButton.remove();
    clear();
    background(255);
    started = true;
    setupDrawingUI(); // load UI controls for drawing
  });
}

function setupDrawingUI() {
  // Create control buttons
  connectButton = createButton("Connect");
  connectButton.mousePressed(() => {
    if (!port.opened()) {
      port.open("Arduino", 115200); // open WebSerial at 115200 baud
    }
  });
  styleButton(connectButton);

  disconnectButton = createButton("Disconnect");
  disconnectButton.mousePressed(() => {
    if (port.opened()) {
      port.close(); // safely close serial port
    }
  });
  styleButton(disconnectButton);

  finishButton = createButton("Finish Drawing");
  finishButton.mousePressed(() => {
    background(255); // clear canvas
    drawingEnabled = false;
  });
  styleButton(finishButton);

  saveButton = createButton("Save as PNG");
  saveButton.mousePressed(() => {
    saveCanvas("drawing", "png"); // download current canvas
  });
  styleButton(saveButton);

  positionUI(); // arrange buttons
  statusText.show();
}

function positionUI() {
  // Align control buttons along the top
  let baseX = width / 2 - 250;
  let y = 10;
  connectButton.position(baseX, y);
  disconnectButton.position(baseX + 130, y);
  finishButton.position(baseX + 260, y);
  saveButton.position(baseX + 420, y);
}

function draw() {
  if (!started) {
    // Intro screen only if tutorial not yet shown
    if (!tutorialShown) {
      background("#F5F5DC");
      textAlign(CENTER, CENTER);
      textSize(48);
      fill("#a8423d");
      text("Sketch & Switch!", width / 2, height / 2 - 100);

      textSize(24);
      fill(0);
      text("Press Start to Begin", width / 2, height / 2 - 40);

      imageMode(CENTER);
      image(studentImg, width / 4, height / 2 - 30, studentImg.width / 3, studentImg.height / 3);
    }
    return;
  }

  // Serial data handling (reads only once per frame to prevent lag)
  if (port.opened()) {
    isConnected = true;
    statusText.html("Status: Connected");

    let data = port.readUntil("\n");
    if (data && data.trim().length > 0) {
      processSerial(data.trim()); // pass cleaned data to be handled
    }
  } else {
    isConnected = false;
    statusText.html("Status: Not connected");
  }

  // Draw a small dot when not drawing (cursor)
  if (!drawingEnabled && isConnected) {
    fill(currentColor);
    noStroke();
    ellipse(xPos, yPos, 6, 6);
  }
}

function processSerial(data) {
  // Parse "x,y,button" format from Arduino
  let parts = data.split(",");
  if (parts.length !== 3) return;

  let xVal = int(parts[0]);
  let yVal = int(parts[1]);
  let btn = int(parts[2]);

  if (isNaN(xVal) || isNaN(yVal) || isNaN(btn)) return;

  // Map potentiometer values to canvas dimensions
  xPos = map(xVal, 0, 1023, 0, width);
  yPos = map(yVal, 0, 1023, 0, height);

  // Toggle drawing mode when button is pressed
  if (btn === 1 && lastButtonState === 0) {
    drawingEnabled = !drawingEnabled;
    currentColor = color(random(255), random(255), random(255));
    prevX = xPos;
    prevY = yPos;
  }

  // Draw if in drawing mode
  if (drawingEnabled) {
    stroke(currentColor);
    strokeWeight(2);
    line(prevX, prevY, xPos, yPos);
    prevX = xPos;
    prevY = yPos;
  }

  lastButtonState = btn; // update for debounce
}

 

Arduino and p5.js Communication

Communication is established via serial connection:

    • Arduino: Sends comma-separated values (X, Y, ButtonState) at a set interval.
    • p5.js: Reads incoming serial data, parses the values, and updates the drawing accordingly.

 

Highlights

I’m proud of the fact that the push button consistently functions to toggle between the drawing modes of randomized thickness and color. Moreover, with optimal baud rate adjustment and data handling, the potentiometers also function with minimal lag, showing smoother and finger movement.

In addition, the project also has clear on-screen instructions and simple0 controls to allow users across any age range to easily become involved, whether they are experts or complete newbies to physical computing.

Areas for Future Improvement

While Sketch & Switch functions smoothly, there’s still plenty of room to build on its foundations:

    • Add a feature where it allow users to position the drawing point before enabling drawing mode, giving them more control over line placement.
    • Adding a color wheel and thickness slider in the UI so users can manually choose colors and line widths, rather than relying solely on randomness.
    • Add an undo button to let users correct mistakes without having to clear the entire canvas.
    • Replace the current components with larger potentiometers and a larger push button for improved tactile feedback and easier control.

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.