All Posts

Week 13: Work on Final Project

User Testing

For my project, I created an interactive media piece that tells the stories of students who faced consequences due to political protests. The user testing aimed to see how intuitive and engaging the experience was without any instructions.

I found that providing a small amount of context greatly improved the user experience. Users could scan a QR code to access additional multimedia content, which helped them engage more deeply. While some users were initially uncertain, a brief introduction to the controls and background information made them feel more comfortable exploring the project.

I also noticed that users who spent more time interacting with the piece took away more meaningful insights. They often stood and engaged with it for several minutes, discovering layers of meaning for them as they went along.

To improve the work in the future, I may decide to add an introduction screen with some helpful guidance, as well as provide a bit more context to what I was attempting to convey.

 

Week 13 – Project Concept & Work in Progress

Project Concept & Work in Progress

Introduction:

The final project is turning out to be great! A concept that combines Cornhole game, as mentioned earlier, and a gamified  message of putting your stuff back in their place. Seeing IM lab assistants go around, cleaning up after the students made me wonder what can get the message across while making it bother interactive and interesting. Something that makes people keep coming back to your project.

Project Development:

The first take was the hardware. The idea is to incorporate a release mechanism alongside a barrel. To construct a barrel, an idle sturdy cardboard was salvaged from the IM lab. It was cut down into two with a saw. Each being 44 cm in length and 50 mm in diameter approximately.

Then, a broken PVC pipe, which was originally part of a discard vacuum cleaner was used. This was done to recycle discarded item. It was clamped and then cut to fit the size of the barrel.

Slit/ cavity was then made into the pipe like structure for the release mechanism to latch into.

The original idea was to shoot pingpong balls. Since the pipe was hollow, it was filled with some plastic trash, and broken items to give it mass. The basic principles of conservation of momentum were to be applied.

M1V1 = M2V2 with 1 indicating the pipe , and 2 indicating the pingpong ball. Hence the ball will have greater speed to make up for the lesser mass.

Next, the discarded utensils like spoon was used. Clamped into place, I used saw to cut it in half. The other half was to be used as the release/lock mechanism for the launcher.

Next, for the tension, instead of just few handful rubber bands, in the spirit of reuse and recycle, I extracted the elastic from the masks.

Next, pilot holes were drilled using a drill machine with larger drill bit. M4 screws with washers and nuts were added to hold the rubber bands in place. A bit distance was kept between the washer and nut to hook the elastic inside them, and also to avoid the tail end obstruction with the PVC.

Since the weight of the whole thing only kept on increasing, the efficiency was to avoid y-axis movement, and instead go for vertical acceleration and a projectile motion.

Projectile Motion For Vertical Velocity | GeeksforGeeks

Last but not the least, wooden pieces from IM lab were used alongside wheels and a 48:1 gear motor for turning the launcher within the horizontal axis.

The final assembled project looks something like this:

Motor driver was added to regulate and power the voltage being drawn from Arduino towards the geared motor.

Arduino and P5 Code:

Arduino:

The arduino code has pin mapped out. The motor driver receives PWM signal from

 

const int ain1Pin = 3;
const int ain2Pin = 4;
const int pwmPin = 5;
int score = 0;

const int startButton = A0;
const int greenButton = A1;
const int redButton = A3;
const int trigger = A5;

const int greenLED = 12;
const int redLED = 8;
const int blueLED = 7;

int trigVal = 0;
const int threshold = 400;

enum GameState { MENU, GAMEPLAY, PAUSED };
GameState gameState = MENU;

void setup() {
  Serial.begin(9600);

  pinMode(ain1Pin, OUTPUT);
  pinMode(ain2Pin, OUTPUT);
  pinMode(pwmPin, OUTPUT);

  pinMode(greenLED, OUTPUT);
  pinMode(redLED, OUTPUT);
  pinMode(blueLED, OUTPUT);

  digitalWrite(redLED, HIGH);
  digitalWrite(greenLED, LOW);
  digitalWrite(blueLED, LOW);

  delay(500);
}

void loop() {
  int startVal = analogRead(startButton);
  int greenVal = analogRead(greenButton);
  int redVal = analogRead(redButton);
  trigVal = analogRead(trigger);

  static bool lastStartPressed = false;
  static bool lastTriggerPressed = false;

  bool startPressed = startVal > threshold;
  bool triggerPressed = trigVal > 1020;

  // Serial communication from p5.js
  if (Serial.available() > 0) {
    String message = Serial.readStringUntil('\n');
    message.trim();

    if (message == "HELLO") {
      Serial.println("ACKNOWLEDGED");
    } else if (message == "RESET") {
      gameState = MENU;
      digitalWrite(redLED, HIGH);
      digitalWrite(greenLED, LOW);
      digitalWrite(blueLED, LOW);
      digitalWrite(ain1Pin, LOW);
      digitalWrite(ain2Pin, LOW);
      analogWrite(pwmPin, 0);
    } else if (message == "Game over") {
      // Optional cleanup
    }
  }

  // Handle start button press
  if (startPressed && !lastStartPressed) {
    if (gameState == MENU) {
      Serial.println("Starting game!");
      gameState = GAMEPLAY;

      digitalWrite(redLED, LOW);
      digitalWrite(greenLED, HIGH);
      digitalWrite(blueLED, LOW);

      digitalWrite(ain1Pin, LOW);
      digitalWrite(ain2Pin, LOW);
      analogWrite(pwmPin, 0);
    }
    else if (gameState == GAMEPLAY) {
      Serial.println("Game paused!");
      gameState = PAUSED;

      digitalWrite(greenLED, LOW);
      digitalWrite(blueLED, HIGH);

      digitalWrite(ain1Pin, HIGH);
      digitalWrite(ain2Pin, HIGH);
      analogWrite(pwmPin, 255);
    }
    else if (gameState == PAUSED) {
      Serial.println("Resuming game!");
      gameState = GAMEPLAY;

      digitalWrite(greenLED, HIGH);
      digitalWrite(blueLED, LOW);

      digitalWrite(ain1Pin, LOW);
      digitalWrite(ain2Pin, LOW);
      analogWrite(pwmPin, 0);
    }
  }
  lastStartPressed = startPressed;

  // Score trigger: rising edge detection
  if (gameState == GAMEPLAY && triggerPressed && !lastTriggerPressed) {
    score++;
    Serial.print("SCORE:");
    Serial.println(score);
  }
  lastTriggerPressed = triggerPressed;

  // Motor control during gameplay
  if (gameState == GAMEPLAY) {
    if (greenVal > threshold) {
      Serial.println("Green button pressed - motor right");
      digitalWrite(ain1Pin, LOW);
      digitalWrite(ain2Pin, HIGH);
      analogWrite(pwmPin, 255);
    }
    else if (redVal > threshold) {
      Serial.println("Red button pressed - motor left");
      digitalWrite(ain1Pin, HIGH);
      digitalWrite(ain2Pin, LOW);
      analogWrite(pwmPin, 255);
    }
    else {
      digitalWrite(ain1Pin, LOW);
      digitalWrite(ain2Pin, LOW);
      analogWrite(pwmPin, 0);
    }
  }
}

P5:

let rVal = 0;
let alpha = 255;
let gameState = 'MENU';  // 'MENU', 'GAMEPLAY', 'RESULT', 'WAITING_FOR_START'
let counter = 60;
let points = 0;
let score = 0;


let left = 0;
let right = 0;

let resultTimer = 0;


function setup() {
  createCanvas(640, 480);
  textAlign(CENTER, CENTER);
  textSize(24);
}

function draw() {
  background(map(rVal, 0, 1023, 0, 255), 255, 255);

  fill(255, 0, 255, map(alpha, 0, 1023, 0, 255));
  // text("Game State: " + gameState, 20, 30);

  if (gameState === 'MENU') {
    resultTimer = 0;
    showMenuScreen();
  } else if (gameState === 'WAITING_FOR_START') {
    showWaitingScreen();
  } else if (gameState === 'GAMEPLAY') {
    showGameScreen();
  } else if (gameState === 'RESULT') {
    showResultScreen();
    resultTimer++;
    if (resultTimer === 180) { // ~3 seconds
      writeSerial("RESET\n");
      gameState = 'MENU';
    }
  }

  if (mouseIsPressed) {
    if (mouseX <= width / 2) {
      left = 1;
    } else {
      right = 1;
    }
  } else {
    left = right = 0;
  }
}

function readSerial(data) {
  data = trim(data);

  // Start game and reset counter and points
  if (data === "Starting game!") {
    counter = 60;
    points = 0;
    gameState = 'GAMEPLAY';
    return;
  }

  // Check if the data contains the score information
  if (data.startsWith("SCORE:")) {
    points = int(data.split(":")[1]);  // Extract score from Arduino message
    return;
  }

  // Process rVal and alpha from Arduino (existing logic)
  let fromArduino = split(data, ",");
  if (fromArduino.length === 2) {
    rVal = int(fromArduino[0]);
    alpha = int(fromArduino[1]);
  }

  // Send button press information back to Arduino
  let sendToArduino = left + "," + right + "\n";
  writeSerial(sendToArduino);
}
 

function showMenuScreen() {
  fill(0);
  textSize(32);
  text("Project Clean Shot", width / 2, height / 2 - 50);
  textSize(24);
  text("Press Space Bar to Start Communication", width / 2, height / 2);
}

function showWaitingScreen() {
  fill(0);
  textSize(28);
  text("Waiting for Arduino to start the game...", width / 2, height / 2);
}

function showGameScreen() {
  fill(0);
  textSize(64);
  text(counter, width / 2, height / 2 - 50);
  textSize(32);
  text("Points: " + points , width / 2, height / 2 + 50);

  if (frameCount % 60 === 0 && counter > 0) {
    counter--;
  }

  if (counter <= 0) {
    gameState = 'RESULT';
    writeSerial("Game over\n");
  }
}

function showResultScreen() {
  fill(0);
  textSize(48);
  text("Game Over", width / 2, height / 2 - 50);
  textSize(32);
  text("Points: " + points, width / 2, height / 2);
  textSize(24);
  text("Returning to Menu...", width / 2, height / 2 + 50);
}

function keyPressed() {
  if (key === " " && gameState === 'MENU') {
    setUpSerial(); // comes from the serial adapter file
    gameState = 'WAITING_FOR_START';
  }
}

P5 Demo link:  https://editor.p5js.org/alinoor_3707/sketches/QbmSLt2kR

P5 and Arduino communication:

P5 and Arduino both communicate using serial mode of communication, which via the USB communicates to one another. The Arduino for instance in our case, after measuring physical values, generates digital signal in the form of ‘Serial.println’ for instance. This is then read by P5js using the web serial API. Since I couldn’t attend the earlier class where this was discussed, I had a hard time understanding the working, and ended up venturing into the NodeJS based p5Serial software after watching an outdated tutorial online. It was after close-reading the documentation that I realized that there is a web-based serial communication interface as well. Nonetheless, using p5.web-serial the data was read. Hence, the Arduino would write and the P5 program would listen. It is; however, working other way around as well when the timer ends and the arduino is told to change the state from gameplay to menu.

Description of interactivity:

The main focus and area of attention was the hardware. 30 percent of time was dedicated to P5 based interaction and majority of the rest was dedicated to Arduino and mechanism behind the rotation and launch of the barrel. The idea was to build a tangible and sturdy structure i.e responsive. Borrowing the idea from one of the readings: A Brief Rant on the Future of Interaction Design,  I wanted to build something that was not hidden underneath the glass-screen as the author described it, but was tactile and tangible in nature.

Circuit schematic:

For the circuitry, since a motor was incorporated to handle the horizontal movement, this time I referred to the tutorial notes provided by Professor Shiloh alongside the sketch for the schematic:

After mapping and implementing this sketch, the next matter at hand was the implementation of the Buttons and LEDs, which were done after first drafting a sketch:

NOTE: Since the Sparkfun TB6612 FNG motor driver wasn’t available in drawing tool, so an alternative has been used to refer to the mapping of the one used.

The sketch digram:

Aspect I am proud of:

The best and most favorite part that makes me happy to get done with was the crafting and hacking. There was a temptation to tinker around with the New Prusa 3-D printers inside the IM lab, however, in the spirit of craftmanship, and getting hands dirty, I decided to drill, saw, cut, glue, sand, break, and do all sorts of wonderful things. It was a crash-course, but I am now much more confident in my ability to build things, instead of placing order to 3rd party contractors to build stuff. This makes me immensely proud of myself and the thing I built with my hand – no matter the imperfections.

Areas for future improvement:

One of the areas that I did work on, but didn’t end up working was the latch mechanism for triggering the release of the barrel after it had been loaded. It popped off due to high tension in the rubber-bands/ elastic, and due to weak structural integrity of the launcher. In the future I would like to work with techniques that help avoid loose fittings and things breaking off.

Moreover, I wanted to add haptics and gyroscope, however, having limited experience with the sensor and low on time, I decided to go with tactile buttons. In the future I would like to work on response hand-gesture controlled mechanism, which also works in the y-axis with an additional motor!

Finial Project

Building Magic Munchkin Battle: A Journey Through AR Card Game Development

Welcome to the wild, whimsical world of Magic Munchkin Battle, an augmented reality (AR) card game that brings Adventure Time characters to life on your tabletop! As a developer, I embarked on this project to blend physical card play with digital interactivity, using Python, OpenCV, Socket.IO, Node.js, and Arduino. This blog post dives deep into the code, the struggles I faced, the versioning process, the bugs that haunted me, the challenges I overcame, and the improvements I’d make to create the best possible version. Buckle up for a technical tale of triumphs and tribulations, with plenty of space for pictures to bring the journey to life!

Document link in case you loose the post:

Documentation – Magic Munchkin Battle


What is Magic Munchkin Battle?

Magic Munchkin Battle is a two-player AR card game inspired by Adventure Time. Players use physical cards embedded with ArUco markers to summon characters like Finn, Jake, or Female Jake onto a digital battlefield. A webcam tracks these cards, displaying their positions and rotations on an OpenCV window and syncing the game state to browser-based clients via Socket.IO. Players battle enemies (Goblins, Slimes, Trolls, and Ogres), select attacks based on card rotation, and log game data for machine learning analysis. An Arduino can enhance the experience with haptic feedback via LED strips and speakers, though keyboard and mouse inputs serve as fallbacks.

The game runs on a Python server (combined_server.py), a Node.js server (server.js), and a client-side HTML/JS interface (watch.html). Here’s a quick overview of the tech stack and features:

  • Tech Stack:

    • Python: OpenCV for AR marker detection, Flask-SocketIO for real-time communication.

    • Node.js: Serves client pages and proxies Socket.IO.

    • Arduino: Optional for LED/button feedback.

    • HTML/JS: Renders the game UI in browsers using p5.js.

  • Features:

    • AR card tracking with 52 unique markers.

    • Real-time multiplayer for two players, plus a spectator view.

    • Enemy AI with scaling difficulty.

    • ML data logging for game analytics.

    • Sound effects, background music, and card animations.


The Code: A Deep Dive

Let’s break down the core components, focusing on how Player 1 and Player 2 are integrated into combined_server.py, the heart of the game. This Python script handles AR tracking, game logic, Socket.IO communication, Arduino integration, and ML logging.

1. AR Card Tracking

The track_cards function uses OpenCV to detect ArUco markers via a webcam. Each marker (0–51) maps to one of 13 Adventure Time characters (e.g., marker 37 → Female Jake). The code calculates the card’s position (x, y) and rotation (rot) to select attacks for both players.

aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100)
detector = cv2.aruco.ArucoDetector(aruco_dict, parameters)
corners, ids, rejected = detector.detectMarkers(frame)
if ids is not None:
    for i, corner in enumerate(corners):
        marker_id = int(ids[i][0])
        x = int(corner[0][0][0])
        y = int(corner[0][0][1])
        character = character_map[marker_id]
        player = 'p1' if x < 320 else 'p2'
        dx = corner[0][1][0] - corner[0][0][0]
        dy = corner[0][1][1] - corner[0][0][1]
        rot = float(np.arctan2(dy, dx) * 180 / np.pi)
        card_positions[player] = {'x': x, 'y': y, 'rot': rot, 'character': character}
  • Logic: Splits the 640×480 frame at x=320 to assign cards to Player 1 (left) or Player 2 (right). Rotation (0–180°) determines attack selection (e.g., 0–45° → Quick Attack).

  • Player 1 vs. Player 2: Ensures both players’ cards are tracked independently, with P1’s actions on the left and P2’s on the right.

  • Output: Updates card_positions and emits to clients via Socket.IO.

2. Game Logic

The game state (intro, playing, paused, gameover) drives the flow. Both Player 1 and Player 2 select cards/attacks, battle enemies, and earn points. The attack_enemy function handles combat:

def attack_enemy(player_idx):
    attack = players[player_idx]['selected_attack']
    crit = random.random() < 0.2
    attack_power = attack['attack'] * (2 if crit else 1)
    enemy_idx = random.randint(0, len(enemies) - 1)
    enemies[enemy_idx]['health'] -= attack_power
    players[player_idx]['points'] += 5
    if enemies[enemy_idx]['health'] <= 0:
        enemies.pop(enemy_idx)
        players[player_idx]['points'] += 10
  • Logic: Applies attack damage, checks for critical hits (20% chance), and removes defeated enemies. Difficulty scales every four battles for both players.

  • Player 1 vs. Player 2: Treats both players symmetrically, allowing independent or cooperative play against enemies.

  • Output: Updates health/points and emits game state.

3. Socket.IO Communication

Real-time updates use Flask-SocketIO with eventlet, ensuring both players stay synchronized:

sio = socketio.Server(cors_allowed_origins='*', async_mode='eventlet')
@sio.event
def connect(sid, environ):
    sio.emit('init', {'players': players, 'enemies': enemies, 'gameState': game_state}, room=sid)
sio.emit('update_card_data', {'cardData': card_positions, 'health': [p['health'] for p in players]}, to=None)
  • Logic: Emits card positions, health, and game state to all clients (P1, P2, spectators).

  • Player 1 vs. Player 2: Broadcasts both players’ data, enabling real-time interaction.

  • Output: Browser clients render cards/enemies for both players.

4. Arduino Integration

The init_arduino and send_arduino_command functions manage serial communication for both players:

def init_arduino():
    for port in ['/dev/cu.usbmodem11101', '/dev/cu.usbmodem11001', '/dev/cu.usbmodem101']:
        try:
            arduino = serial.Serial(port, 9600, timeout=1)
            logger.info(f"Serial connection established on {port}")
            return
        except serial.SerialException as e:
            logger.warning(f"Failed to open {port}: {e}")

def send_arduino_command(player_id, action, value=None):
    if arduino and arduino.is_open:
        prefix = f"P{player_id}_"
        if action == 'character_click':
            arduino.write(f"{prefix}CHARACTER:{value}\n".encode())
        elif action == 'attack':
            arduino.write(f"{prefix}ATTACK:1\n".encode())
  • Logic: Tries multiple ports, sends commands (e.g., P1_ATTACK:1 for Player 1, P2_ATTACK:1 for Player 2) for LED/button feedback.

  • Player 1 vs. Player 2: Sends distinct commands to each player’s LED strip and speaker.

  • Output: Enhances physical interaction (optional).

5. ML Data Logging

Game data is logged to game_data.csv for future ML analysis:

def log_ml_data(win=False):
    with open(ml_log_file, 'a', newline='') as f:
        writer = csv.writer(f)
        writer.writerow([datetime.now().isoformat(), game_state, len(player_cards['p1']), len(player_cards['p2']), ...])
  • Logic: Tracks game state, card counts, health, and wins for both players.

  • Output: CSV file for training models to optimize gameplay.


The Evolution: From Initial Idea to Final Product

Initial Idea: A Solo AR Adventure (April 2025)

The original vision was a solo card game where one player used physical cards with ArUco markers, tracked by a webcam, to battle enemies. The Arduino would light a single LED strip (e.g., green for card selection, red for attack), and a buzzer would play sound effects. The game was controlled via a simple Python script with an OpenCV window, no web interface, and no multiplayer.

  • Core Features:

    • One player with a deck of 5 cards.

    • Static enemies with fixed health.

    • Basic LED feedback.

    • No network support.

  • Player 1 Focus: Initially, only Player 1 existed, with no consideration for a second player.

Intermediate Steps: Adding Player 2 and Multiplayer (April 2025)

I expanded the game to support two players, introducing Player 2 with their own LED strip and speaker. I added Socket.IO for real-time multiplayer, allowing players to join via a web interface (watch.html). The server began managing both Player 1 and Player 2, and I introduced game states to handle flow.

  • Player 2 Integration:

    • Added a second LED strip (pin 7) and speaker (pin 9), with distinct feedback (e.g., P2’s attack sound uses lower tones like 220 Hz).

    • Split the camera feed at x=320: left for Player 1, right for Player 2.

    • Updated the web interface to render both players’ cards and stats, with Player 2’s elements mirrored on the right.

  • Challenges:

    • Synchronizing Player 1 and Player 2 actions without conflicts.

    • Ensuring the Arduino could handle dual commands without lag.

Final Product: A Polished AR Multiplayer Game (May 2025)

The current version is a fully multiplayer AR game with hardware integration, a responsive web interface, and robust game logic. Both Player 1 and Player 2 can select cards, attack enemies, draw, and discard, with real-time feedback via LEDs, sounds, and the optional LCD. The server logs ML data, and the game supports spectators.

  • Key Additions:

    • LCD support to display events for both players (e.g., “P1: Finn,” “P2: Female Jake”).

    • Error handling for camera and serial issues.

    • Optimized web rendering with p5.js, preloading assets with fallbacks for both players’ cards and enemies.

  • Player 1 vs. Player 2: Fully symmetrical gameplay, with mirrored UI and hardware feedback.


The Struggles: A Developer’s Nightmare

Building Magic Munchkin Battle was a rollercoaster of bugs, crashes, and hardware woes. Here’s a detailed look at what went wrong, with examples from the logs, affecting both Player 1 and Player 2.

1. OpenCV Errors Galore

The AR tracking was plagued with OpenCV issues. Early logs showed:

2025-05-08 18:40:30,030 - ERROR - Error in track_cards loop: Unknown C++ exception from OpenCV code
  • What Happened: OpenCV’s ArUco detector threw vague exceptions, likely due to invalid frames or misconfigured parameters.

  • Example: I placed marker_37.png (Female Jake, Player 2) in the webcam view, expecting it to register. Instead, the server crashed with “Unknown C++ exception,” affecting Player 2’s ability to join the game.

  • Fix: Added frame validation (if not ret or frame.size == 0) and tried multiple camera indices.

  • Challenge: Debugging OpenCV’s C++ exceptions was elusive, worsened by macOS’s Continuity Camera conflicts.

2. ArUco Marker Detection Failures

Marker drawing often failed:

2025-05-08 18:41:07,784 - ERROR - Error processing marker 37: OpenCV(4.10.0) :-1: error: (-5:Bad argument) in function 'drawDetectedMarkers'
> Overload resolution failed:
>  - ids is not a numpy array, neither a scalar
  • What Happened: The ids array from detector.detectMarkers wasn’t formatted correctly for cv2.aruco.drawDetectedMarkers.

  • Example: Marker 37 was detected for Player 2, but the OpenCV window didn’t draw the outline, crashing the loop and leaving Player 2 without visual feedback.

  • Fix: Ensured ids was a NumPy array (np.array(ids, dtype=np.int32)) and passed per-marker ids correctly.

  • Challenge: OpenCV’s ArUco API lacked clear edge-case documentation.

3. JSON Serialization Nightmares

Socket.IO broke due to NumPy types:

2025-05-08 18:41:07,786 - ERROR - Error in track_cards loop: Object of type float32 is not JSON serializable
  • What Happened: Card rotation (rot) was a NumPy float32, which Python’s JSON encoder couldn’t handle.

  • Example: After detecting marker 37 for Player 2, the server crashed when emitting card_positions, desyncing Player 2’s client.

  • Fix: Converted float32 to Python float and created a serializable_card_positions dictionary.

  • Challenge: Tracking NumPy types in complex data was time-consuming.

4. Arduino Port Lock

Arduino integration failed initially:

2025-05-08 18:40:28,342 - WARNING - Failed to open /dev/cu.usbmodem11101: [Errno 16] Resource busy
  • What Happened: Another process (e.g., Arduino IDE) locked the port.

  • Example: I expected Player 1 and Player 2’s button presses to trigger LED feedback, but the server skipped Arduino, falling back to keyboard input.

  • Fix: Used lsof to find the PID and kill -9 <PID>, then set port permissions (sudo chmod 666 /dev/cu.usbmodem11101).

  • Challenge: macOS’s serial port management was opaque, requiring manual intervention.

5. Camera Continuity Issues

macOS’s Continuity Camera caused disruptions:

2025-05-08 ... - WARNING - Continuity Camera warning (fixed with Info.plist)
  • What Happened: macOS used an iPhone as a webcam, conflicting with the USB webcam.

  • Example: Frames were inconsistent, causing “Unknown C++ exception” errors, affecting both Player 1 and Player 2’s card detection.

  • Fix: Added an Info.plist to the virtual environment to disable Continuity Camera.

  • Challenge: Sparse Apple documentation made this a hacky fix.


Why the Game Isn’t Fully Playable

Despite progress, Magic Munchkin Battle isn’t fully playable due to several issues impacting both Player 1 and Player 2. Here’s why:

1. Camera Detection Inconsistencies

  • Issue: track_cards struggles with inconsistent marker detection. Poor lighting, occlusion, or camera lag causes cards to be missed or misassigned (e.g., Player 2’s card detected as Player 1’s).

  • Impact: Players can’t reliably select cards. For example, if Player 2 places marker_37 (Female Jake) on the right but it’s assigned to Player 1, Player 2 can’t attack.

  • Cause: OpenCV’s sensitivity to lighting and my limited error handling (e.g., skipping invalid IDs) aren’t robust enough.

2. Arduino Command Overlaps

  • Issue: Simultaneous actions (e.g., Player 1 and Player 2 attacking) overwhelm the Arduino’s serial buffer, dropping commands.

  • Impact: One player’s action might fail (e.g., Player 2’s LED doesn’t light), breaking feedback.

  • Cause: Blocking delay() calls in sound functions (e.g., playP2Pow()) prevent quick command processing.

3. Game State Desync

  • Issue: The game state desyncs between the server and clients, especially during transitions. If Player 1 starts the game but Player 2’s client misses the start_stop event, Player 2 stays in intro.

  • Impact: Players can’t progress together, halting multiplayer play.

  • Cause: Socket.IO’s event delivery isn’t guaranteed, and I lack a sync handshake.

4. Web Interface Lag on Mobile

  • Issue: The p5.js canvas lags on mobile when rendering both players’ cards, especially with animations.

  • Impact: Players on mobile miss clicks, disrupting gameplay for both Player 1 and Player 2.

  • Cause: Full-frame redraws overwhelm mobile browsers.


Versioning: The Evolution of the Code

  • Version 1: Barebones AR Tracking (March 2025):

    • Features: Basic ArUco detection, static card positions, Flask server.

    • Issues: No Socket.IO, frequent OpenCV crashes, no Player 2 or Arduino.

    • Example Bug: Marker 0 (Finn) detected, but moving it crashed the server.

    • Fix: Added error handling.

  • Version 2: Socket.IO and Game Logic (April 2025):

    • Features: Real-time updates, combat, ML logging, introduced Player 2.

    • Issues: drawDetectedMarkers errors, JSON serialization failures, port conflicts.

    • Example Bug: Marker 37 (Player 2) crashed due to ids format.

    • Fix: Fixed ids, converted types, added port retries.

  • Version 3: Polished Game (May 2025):

    • Features: Full multiplayer, enemy AI, difficulty scaling, Arduino/keyboard inputs.

    • Issues: Minor camera index issues, occasional lag.

    • Example Bug: Camera index 0 picked Continuity Camera.

    • Fix: Tried indices 0–2, added Info.plist.

  • Current State: Stable AR tracking, reliable Socket.IO, optional Arduino, ML-ready data.


Challenges Faced

OpenCV’s Black Box

OpenCV’s C++ exceptions were cryptic, requiring verbose logging and try-except blocks to isolate issues like invalid frames.

macOS Quirks

Continuity Camera and serial port locks were macOS-specific, solved with Info.plist and lsof/kill, but felt hacky.

Real-Time Synchronization

Syncing Player 1 and Player 2’s actions across clients required careful serialization and throttled emits.

Hardware Integration

Arduino’s limited power and port issues made it optional, with keyboard/mouse fallbacks.

Asset Management

Missing assets (e.g., finn.png) caused crashes, mitigated with placeholders.


Things I’d Improve for the Best Version

Enhancing the Initial Concept

  • Card Combos: Allow Player 1 and Player 2 to combine cards (e.g., Finn + Jake = “Team Attack”) for bonus damage, adding strategy.

  • Environmental Effects: Introduce “field cards” (e.g., “Ice King’s Lair”) affecting both players, enhancing immersion.

New Ideas for the Final Product

  • Dynamic Enemy AI: Enemies target the weaker player (e.g., Troll attacks Player 2 if their health is lower), using ML from game_data.csv.

  • Team Mode: Player 1 and Player 2 team up against tougher enemies, sharing a health pool.

  • WebAR: Overlay card effects (e.g., Finn swinging a sword) on the webcam feed using WebAR, enhancing the AR experience.

Fixing Current Issues

  • Camera Detection: Add adaptive thresholding and a calibration step for lighting.

  • Arduino Commands: Implement a command queue with acknowledgments.

  • Game State Sync: Add a heartbeat event to resync clients.

  • Mobile Performance: Switch to WebGL and simplify animations.

Additional Improvements

  • Better OpenCV Debugging: Use debug builds or custom logging.

  • Robust Arduino: Dynamically detect ports, test with a physical Uno.

  • Asset Pipeline: Use real images/sounds from a CDN.

  • Performance: Optimize track_cards with frame skipping, use asyncio.

  • ML Integration: Develop ml_model.py for real-time feedback.

  • UI Polish: Add CSS animations and a tutorial mode.

  • Testing: Write unit tests and automate AR testing.


Lessons Learned

  • AR is Hard: Robust error handling and hardware compatibility are critical.

  • Log Everything: Detailed logs saved me from OpenCV ghosts.

  • Iterate Fast: Frequent versioning tackled bugs incrementally.

  • Hardware is Unpredictable: Fallbacks were essential.

  • Community Matters: Forums and docs provided key fixes.

  • Play:

    • Open http://10.228.236.43:5001/ in two browsers.

    • Place ArUco markers (e.g., data/marker_37.png for Player 2) in the webcam view.

    • Press s to start, a (Player 1) or down (Player 2) to attack.

  • Check Data: See /Users/Worker/Desktop/magic_munchkin_battle/game_data.csv for ML data and share your scores!


Conclusion

Magic Munchkin Battle was a labor of love, blending AR, multiplayer gaming, and Adventure Time flair for both Player 1 and Player 2. From OpenCV crashes to Arduino port battles, every bug taught resilience. The current version (V3) is stable but not fully playable due to detection and sync issues. With WebAR, ML insights, and a polished UI, the best version is within reach.

Have questions or want to contribute?  Let’s make Magic Munchkin Battle even more magical!

Below is the code+ setup instructions!

IMAGES:

Project Images

Combined_server.py:

# I set up the core server for Magic Munchkin Battle here. This script handles everything: AR tracking, game logic, Socket.IO communication, Arduino integration, and ML logging.
# It’s the heart of the game, connecting the physical cards to the digital battlefield.

import cv2  # I use OpenCV for AR marker detection to track the physical cards.
import numpy as np  # NumPy helps with array operations, especially for marker positions and rotations.
import serial  # For Arduino communication to control LED strips and speakers.
import time  # To manage timing for game loops and delays.
import threading  # I use threads to run the enemy attack loop and card tracking concurrently.
import socketio  # Socket.IO for real-time communication between the server and clients (players/spectators).
import logging  # Logging helps me debug issues like OpenCV errors or Arduino failures.
import random  # For random events like critical hits or enemy selection.
import os  # To handle file paths for assets and ML data logging.
import csv  # For logging ML data to a CSV file.
from datetime import datetime  # To timestamp ML logs.
from pynput import keyboard  # I added keyboard controls as a fallback when Arduino isn’t available.
import eventlet  # Eventlet is required for Socket.IO’s async mode.
import eventlet.wsgi  # To run the WSGI server for Socket.IO.

# I configure logging to track everything—info, warnings, errors. It’s been a lifesaver for debugging OpenCV’s cryptic errors.
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# I initialize the Socket.IO server with eventlet for real-time updates to the browser clients.
sio = socketio.Server(cors_allowed_origins='*', async_mode='eventlet')
# I set up the WSGI app to serve the client pages (watch.html) and static data folder for assets.
app = socketio.WSGIApp(sio, static_files={
    '/watch': {'content_type': 'text/html', 'filename': 'watch.html'},
    '/data': {'content_type': '', 'filename': 'data/'}
})
server_ip = '10.228.236.43'  # My local IP for hosting the server.
port = 5000  # The port I chose for the server.
logger.info(f"Server running on {server_ip}:{port}")

# I define global variables to manage the game state and player data.
card_positions = {'p1': {'x': 0, 'y': 0, 'rot': 0, 'character': None}, 'p2': {'x': 0, 'y': 0, 'rot': 0, 'character': None}}  # Tracks each player’s card position and rotation.
assigned_cards = {}  # Maps marker IDs to players to prevent reassignment.
player_cards = {'p1': set(), 'p2': set()}  # Tracks which cards (markers) are currently detected for each player.
last_positions = {}  # Stores the last known position of each marker to detect movement.
game_state = 'intro'  # Manages the game flow: intro, playing, paused, gameover.
game_started = False  # Tracks if the game has started.
timer = 0  # Game timer for ML logging.
difficulty_multiplier = 1.0  # Scales enemy difficulty as the game progresses.
battle_count = 0  # Counts battles to adjust difficulty every 4 battles.
# I define the players list with initial stats for both Player 1 and Player 2.
players = [
    {'id': 'p1', 'deck': [], 'discard_pile': [], 'full_deck': [], 'health': 100, 'points': 0, 'selected': None, 'selected_attack': None, 'status_effects': []},
    {'id': 'p2', 'deck': [], 'discard_pile': [], 'full_deck': [], 'health': 100, 'points': 0, 'selected': None, 'selected_attack': None, 'status_effects': []}
]
enemies = []  # List of active enemies in the game.
enemy_health = []  # Tracks enemy health for client updates.
arduino = None  # Will hold the serial connection to Arduino if available.
# I set up the data path for assets like background images.
data_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'data'))
# I make sure the data folder exists, creating it if necessary.
if not os.path.exists(data_path):
    logger.error(f"Data folder not found at {data_path}")
    os.makedirs(data_path)

# I set up ML data logging to analyze gameplay and improve the game later.
ml_log_file = 'game_data.csv'  # File to store game data for ML analysis.
ml_log_headers = ['timestamp', 'game_state', 'p1_card_count', 'p2_card_count', 'p1_health', 'p2_health', 'enemy_count', 'difficulty', 'attacks', 'pauses', 'game_duration', 'win']  # Headers for the CSV.
ml_log_data = {'attacks': 0, 'pauses': 0, 'start_time': time.time()}  # Tracks runtime stats for logging.
# If the log file doesn’t exist, I create it with the headers.
if not os.path.exists(ml_log_file):
    with open(ml_log_file, 'w', newline='') as f:
        writer = csv.writer(f)
        writer.writerow(ml_log_headers)

# I map the 52 ArUco markers to 13 Adventure Time characters, with 4 markers per character.
characters = [
    "Finn", "Jake", "Marceline", "Flame Princess", "Ice King", "Princess Bubblegum", "BMO",
    "Lumpy Space Princess", "Banana Guard", "Female Jake", "Gunther", "Female Finn", "Tree Trunks"
]
character_map = {i: characters[i // 4] for i in range(52)}  # Maps marker IDs to character names.

# I wrote this function to update card positions for each player and ensure they stay within the camera frame.
def update_card_position(player, new_x, new_y):
    if player in card_positions:
        # I clamp the coordinates to the 640x480 frame to avoid out-of-bounds issues.
        card_positions[player]['x'] = max(0, min(new_x, 640))
        card_positions[player]['y'] = max(0, min(new_y, 480))
        # I emit the updated card positions and game state to all clients.
        sio.emit('update_card_data', {
            'cardData': card_positions,
            'health': [players[0]['health'], players[1]['health']],
            'points': [players[0]['points'], players[1]['points']],
            'enemyHealth': enemy_health,
            'gameState': game_state,
            'difficulty': difficulty_multiplier,
            'port': port
        }, to=None)

# I initialize the Arduino connection for LED and sound feedback.
def init_arduino():
    global arduino
    try:
        # I try multiple possible ports since macOS port names can vary.
        possible_ports = ['/dev/cu.usbmodem11101', '/dev/cu.usbmodem11001', '/dev/cu.usbmodem101']
        for port in possible_ports:
            try:
                arduino = serial.Serial(port, 9600, timeout=1)
                time.sleep(2)  # I added a delay to let the connection stabilize.
                logger.info(f"Serial connection established for Arduino on {port}")
                return
            except serial.SerialException as e:
                logger.warning(f"Failed to open {port}: {e}")
        logger.warning("No Arduino ports available. Continuing without Arduino.")
    except Exception as e:
        logger.error(f"Failed to initialize Arduino: {e}")
        logger.warning("Continuing without Arduino serial connection")

# I use this to send commands to the Arduino, like lighting LEDs or playing sounds for each player.
def send_arduino_command(player_id, action, value=None):
    if arduino and arduino.is_open:
        try:
            prefix = f"P{player_id}_"  # I use P1_ or P2_ to differentiate players.
            if action == 'character_click':
                arduino.write(f"{prefix}CHARACTER:{value}\n".encode())  # Command to show character selection.
            elif action == 'attack':
                arduino.write(f"{prefix}ATTACK:1\n".encode())  # Command to indicate an attack.
            elif action == 'attack_load':
                arduino.write(f"{prefix}LOAD:{int(value * 255)}\n".encode())  # Command to show attack loading progress.
        except Exception as e:
            logger.error(f"Failed to send Arduino command for Player {player_id}: {e}")

# I read commands from the Arduino, like button presses, to trigger game actions.
def read_arduino():
    commands = []
    if arduino and arduino.is_open:
        try:
            if arduino.in_waiting > 0:
                command = arduino.readline().decode().strip()
                if command:
                    commands.append(command)
        except Exception as e:
            logger.error(f"Failed to read Arduino: {e}")
    return commands

# I log game data for ML analysis to understand player behavior and game balance.
def log_ml_data(win=False):
    with open(ml_log_file, 'a', newline='') as f:
        writer = csv.writer(f)
        writer.writerow([
            datetime.now().isoformat(),
            game_state,
            len(player_cards['p1']),
            len(player_cards['p2']),
            players[0]['health'],
            players[1]['health'],
            len(enemies),
            difficulty_multiplier,
            ml_log_data['attacks'],
            ml_log_data['pauses'],
            time.time() - ml_log_data['start_time'],
            int(win)
        ])

# I create a deck for each player with 5 cards, randomly selected from the character pool.
def create_deck():
    character_defs = [
        {"name": "Finn", "attacks": [
            {"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0},
            {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5},
            {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0},
            {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}
        ]},
        {"name": "Jake", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Marceline", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Flame Princess", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Ice King", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Princess Bubblegum", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "BMO", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Lumpy Space Princess", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Banana Guard", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Female Jake", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Gunther", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Female Finn", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]},
        {"name": "Tree Trunks", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 4, "load": 1.0}, {"name": "Normal Attack", "attack": 4, "health": 3, "load": 1.5}, {"name": "Build-Up Attack", "attack": 6, "health": 2, "load": 2.0}, {"name": "Very Big Attack", "attack": 8, "health": 2, "load": 3.0}]}
    ]
    deck = []
    for char in character_defs:
        for _ in range(4):  # I give each character 4 cards to match the 52 markers.
            deck.append(char.copy())
    random.shuffle(deck)  # I shuffle to make the deck random for each player.
    return deck[:5]  # I limit the initial deck to 5 cards for balance.

# I define the enemies with their stats, scaling them with the difficulty multiplier.
def create_enemies():
    enemies = [
        {"name": "Goblin", "health": 12 * 1.2, "cards": [{"name": "Goblin", "attacks": [{"name": "Quick Attack", "attack": 2, "health": 2, "load": 1.0}], "image": "goblin"}]},
        {"name": "Slime", "health": 14 * 1.2, "cards": [{"name": "Slime", "attacks": [{"name": "Quick Attack", "attack": 2, "health": 2, "load": 1.0}], "image": "slime"}]},
        {"name": "Troll", "health": 16 * 1.2, "cards": [{"name": "Troll", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 2, "load": 1.0}], "image": "troll"}]},
        {"name": "Ogre", "health": 21 * 1.2, "cards": [{"name": "Ogre", "attacks": [{"name": "Quick Attack", "attack": 3, "health": 2, "load": 1.0}], "image": "ogre"}]}
    ]
    for enemy in enemies:
        # I scale enemy health with difficulty, but cap it to avoid making the game too hard.
        enemy["health"] = int(enemy["health"] * min(difficulty_multiplier, 1.5))
        for card in enemy["cards"]:
            for attack in card["attacks"]:
                attack["attack"] = int(attack["attack"] * min(difficulty_multiplier, 1.5))
    return enemies

# I handle client connections via Socket.IO, sending initial game data to new clients.
@sio.event
def connect(sid, environ):
    logger.info(f"Client connected: {sid}")
    sio.emit('init', {'players': players, 'enemies': enemies, 'gameState': game_state, 'difficulty': difficulty_multiplier, 'port': port}, room=sid)
    sio.emit('gameState', {'gameState': game_state, 'gameStarted': game_started, 'timer': timer}, room=sid)

# I assign clients as players or spectators when they join.
@sio.event
def join(sid, data):
    logger.info(f"Client {sid} joined as {data['type']}")
    if data['type'] == 'player':
        for idx, player in enumerate(players):
            if not hasattr(player, 'sid') or player['sid'] is None:
                player['sid'] = sid
                sio.emit('assignPlayer', {'playerId': idx}, room=sid)
                logger.info(f"Assigned {sid} as Player {idx + 1}")
                return
        sio.emit('assignPlayer', {'playerId': None}, room=sid)  # No player slots available.
    else:
        sio.emit('assignPlayer', {'playerId': None}, room=sid)  # Spectator.

# I clean up when a client disconnects, freeing up their player slot.
@sio.event
def disconnect(sid):
    logger.info(f"Client disconnected: {sid}")
    for player in players:
        if hasattr(player, 'sid') and player['sid'] == sid:
            player['sid'] = None

# I handle starting/stopping the game, resetting state if needed.
@sio.event
def start_stop(sid, data):
    global game_started, game_state, timer, difficulty_multiplier, battle_count, ml_log_data
    game_started = data['gameStarted']
    game_state = 'playing' if game_started else 'intro'
    timer = 0
    if not game_started:
        reset_game()  # I reset the game state when stopping.
        log_ml_data(win=False)  # I log the game session for ML analysis.
    sio.emit('start_stop', {'gameStarted': game_started, 'difficulty': difficulty_multiplier, 'port': port}, to=None)

# I handle card/attack selection for each player.
@sio.event
def select(sid, data):
    player_idx = next((i for i, p in enumerate(players) if hasattr(p, 'sid') and p['sid'] == sid), -1)
    if player_idx != -1:
        players[player_idx]['selected'] = data.get('card')
        players[player_idx]['selected_attack'] = data.get('attack')
        if players[player_idx]['selected']:
            send_arduino_command(player_idx + 1, 'character_click', players[player_idx]['selected'])  # I trigger Arduino feedback for selection.
        sio.emit('select', {'player': player_idx, 'card': players[player_idx]['selected'], 'attack': players[player_idx]['selected_attack']}, to=None)

# I process attacks initiated by players via the web interface.
@sio.event
def attack(sid, data):
    player_idx = next((i for i, p in enumerate(players) if hasattr(p, 'sid') and p['sid'] == sid), -1)
    if player_idx != -1 and players[player_idx]['selected'] and players[player_idx]['selected_attack']:
        attack_enemy(player_idx)
        sio.emit('attack', {'player': player_idx}, to=None)
        send_arduino_command(player_idx + 1, 'attack')

# I let players draw cards from their full deck.
@sio.event
def draw_card(sid, data):
    player_idx = next((i for i, p in enumerate(players) if hasattr(p, 'sid') and p['sid'] == sid), -1)
    if player_idx != -1 and players[player_idx]['full_deck']:
        card = players[player_idx]['full_deck'].pop()
        players[player_idx]['deck'].append(card)
        sio.emit('draw_card', {'player': player_idx, 'card': card}, to=None)

# I handle discarding cards to manage hand limits.
@sio.event
def discard_card(sid, data):
    player_idx = next((i for i, p in enumerate(players) if hasattr(p, 'sid') and p['sid'] == sid), -1)
    if player_idx != -1 and data.get('card_index') is not None and 0 <= data['card_index'] < len(players[player_idx]['deck']):
        card = players[player_idx]['deck'].pop(data['card_index'])
        players[player_idx]['discard_pile'].append(card)
        sio.emit('discard_card', {'player': player_idx, 'card_index': data['card_index']}, to=None)

# I implement the attack logic for players, including critical hits and difficulty scaling.
def attack_enemy(player_idx):
    global battle_count, difficulty_multiplier, game_state
    if enemies and players[player_idx]['selected'] and players[player_idx]['selected_attack']:
        attack = players[player_idx]['selected_attack']
        crit = random.random() < 0.2  # I give a 20% chance for a critical hit.
        crit_multiplier = 2 if crit else 1
        attack_power = attack['attack'] * crit_multiplier
        # I apply any status effects like Victory Boost to increase attack power.
        for effect in players[player_idx]['status_effects']:
            if effect['name'] == 'Victory Boost':
                attack_power += effect['amount']
        enemy_idx = random.randint(0, len(enemies) - 1)
        enemy = enemies[enemy_idx]
        enemy['health'] -= attack_power
        players[player_idx]['points'] += 5
        ml_log_data['attacks'] += 1
        message = f"Player {player_idx + 1}: {'Critical Hit! ' if crit else ''}Dealt {attack_power} damage!"
        sio.emit('message', {'text': message, 'timer': 180}, to=None)
        send_arduino_command(player_idx + 1, 'attack')
        if enemy['health'] <= 0:
            enemies.pop(enemy_idx)
            players[player_idx]['points'] += 10
            # I add a Victory Boost status effect for defeating an enemy.
            players[player_idx]['status_effects'].append({'name': 'Victory Boost', 'effect': 'attack', 'amount': 1, 'duration': 3})
            sio.emit('message', {'text': f"Player {player_idx + 1}: Enemy defeated! +10 points!", 'timer': 180}, to=None)
            battle_count += 1
            # I increase difficulty every 4 battles, but cap it at 1.5.
            if battle_count % 4 == 0:
                difficulty_multiplier = min(difficulty_multiplier + 0.1, 1.5)
                for enemy in enemies:
                    enemy['health'] = int(enemy['health'] * difficulty_multiplier)
                    for card in enemy["cards"]:
                        for attack in card["attacks"]:
                            attack["attack"] = int(attack["attack"] * min(difficulty_multiplier, 1.5))
        enemy_health[:] = [e['health'] for e in enemies]
        update_status_effects()
        # I check for game over conditions: no enemies (win) or player health at 0 (lose).
        if not enemies:
            game_state = 'gameover'
            log_ml_data(win=True)
        elif players[player_idx]['health'] <= 0:
            game_state = 'gameover'
            log_ml_data(win=False)
        sio.emit('update_game', {
            'players': players,
            'enemies': enemies,
            'enemyHealth': enemy_health,
            'gameState': game_state,
            'difficulty': difficulty_multiplier
        }, to=None)

# I manage status effects, reducing their duration each turn and removing expired ones.
def update_status_effects():
    for player in players:
        for effect in player['status_effects'][:]:
            effect['duration'] -= 1
            if effect['duration'] <= 0:
                player['status_effects'].remove(effect)

# I reset the game state when starting a new game or after a game over.
def reset_game():
    global players, enemies, enemy_health, difficulty_multiplier, battle_count, player_cards, ml_log_data
    players = [
        {'id': 'p1', 'deck': create_deck(), 'discard_pile': [], 'full_deck': create_deck(), 'health': 100, 'points': 0, 'selected': None, 'selected_attack': None, 'status_effects': [], 'sid': None},
        {'id': 'p2', 'deck': create_deck(), 'discard_pile': [], 'full_deck': create_deck(), 'health': 100, 'points': 0, 'selected': None, 'selected_attack': None, 'status_effects': [], 'sid': None}
    ]
    enemies = create_enemies()
    enemy_health = [e['health'] for e in enemies]
    difficulty_multiplier = 1.0
    battle_count = 0
    player_cards = {'p1': set(), 'p2': set()}
    ml_log_data = {'attacks': 0, 'pauses': 0, 'start_time': time.time()}
    log_ml_data()

# I run a separate thread for enemies to attack players periodically.
def enemy_attack():
    global game_state, enemies, players, enemy_health, ml_log_data
    while True:
        if game_state == 'playing' and enemies:
            for player_idx, player in enumerate(players):
                if random.random() < 0.33:  # I give enemies a 33% chance to attack each cycle.
                    attack = random.choice(enemies[0]['cards'][0]['attacks'])
                    player['health'] -= attack['attack']
                    sio.emit('message', {'text': f"Enemy attacked Player {player_idx + 1}! -{attack['attack']} HP", 'timer': 180}, to=None)
                    if player['health'] <= 0:
                        game_state = 'gameover'
                        log_ml_data(win=False)
                        sio.emit('update_game', {
                            'players': players,
                            'enemies': enemies,
                            'enemyHealth': enemy_health,
                            'gameState': game_state,
                            'difficulty': difficulty_multiplier
                        }, to=None)
                        break
        time.sleep(3)  # I set enemies to attack every 3 seconds.

# I enforce a card limit of 5 per player to prevent clutter and ensure fair play.
def check_card_limit():
    global game_state, ml_log_data
    for player_id in ['p1', 'p2']:
        player_idx = 0 if player_id == 'p1' else 1
        if len(player_cards[player_id]) > 5 and game_state != 'paused':
            game_state = 'paused'
            ml_log_data['pauses'] += 1
            sio.emit('message', {
                'text': f"Player {player_idx + 1}: More than 5 cards detected. Please discard excess cards.",
                'timer': -1
            }, to=None)
            log_ml_data()
        elif len(player_cards[player_id]) <= 5 and game_state == 'paused':
            game_state = 'playing'
            sio.emit('message', {'text': f"Player {player_idx + 1}: Card limit resolved. Game resumed.", 'timer': 180}, to=None)
            sio.emit('update_game', {
                'players': players,
                'enemies': enemies,
                'enemyHealth': enemy_health,
                'gameState': game_state,
                'difficulty': difficulty_multiplier
            }, to=None)

# This is the main loop for tracking cards with OpenCV and handling game inputs.
def track_cards():
    global card_positions, assigned_cards, last_positions, player_cards, game_state
    try:
        init_arduino()  # I initialize Arduino at the start of tracking.
        cap = None
        # I try multiple camera indices because macOS can be finicky with camera selection.
        for i in range(3):
            try:
                cap = cv2.VideoCapture(i, cv2.CAP_ANY)
                if cap.isOpened():
                    logger.info(f"Camera opened on index {i}")
                    break
                cap.release()
            except Exception as e:
                logger.warning(f"Failed to open camera on index {i}: {e}")
        if not cap or not cap.isOpened():
            logger.error("Failed to open any camera. Exiting track_cards.")
            return

        cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

        # I verify the camera resolution to ensure it matches my expected 640x480.
        width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
        height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
        logger.info(f"Camera resolution: {width}x{height}")

        # I load the background image for the OpenCV window, with a fallback if it’s missing.
        background_path = os.path.join(data_path, 'ooo_background.png')
        background = cv2.imread(background_path)
        if background is None:
            logger.warning(f"Failed to load background image at {background_path}")
            background = np.zeros((480, 640, 3), dtype=np.uint8)
            background[:] = (50, 50, 50)
        background = cv2.resize(background, (640, 480))

        # I set up the ArUco dictionary and detector for marker tracking.
        aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100)
        parameters = cv2.aruco.DetectorParameters()
        detector = cv2.aruco.ArucoDetector(aruco_dict, parameters)

        # I define keyboard controls as a fallback for Arduino button presses.
        def on_press(key):
            global game_state
            try:
                if game_state != 'paused':
                    if key == keyboard.KeyCode.from_char('s') and game_state == 'intro':
                        sio.emit('start_stop', {'gameStarted': True, 'difficulty': difficulty_multiplier}, to=None)
                    elif game_state == 'playing':
                        # I use 'a' for Player 1 and 'down' for Player 2 to trigger attacks.
                        if key == keyboard.KeyCode.from_char('a') and players[0]['selected'] and players[0]['selected_attack']:
                            attack_enemy(0)
                            sio.emit('attack', {'player': 0}, to=None)
                            send_arduino_command(1, 'attack')
                        elif key == keyboard.Key.down and players[1]['selected'] and players[1]['selected_attack']:
                            attack_enemy(1)
                            sio.emit('attack', {'player': 1}, to=None)
                            send_arduino_command(2, 'attack')
            except Exception as e:
                logger.error(f"Keyboard error: {e}")

        listener = keyboard.Listener(on_press=on_press)
        listener.start()

        while True:
            try:
                ret, frame = cap.read()
                if not ret or frame is None or frame.size == 0:
                    logger.warning("Failed to capture valid frame.")
                    time.sleep(0.1)
                    continue

                display_frame = background.copy()
                try:
                    corners, ids, rejected = detector.detectMarkers(frame)
                    if ids is not None:
                        ids = np.array(ids, dtype=np.int32)  # I ensure ids is a NumPy array to avoid drawDetectedMarkers errors.
                except Exception as e:
                    logger.error(f"Error in marker detection: {e}")
                    continue

                player_cards['p1'].clear()
                player_cards['p2'].clear()

                if ids is not None and len(ids) > 0:
                    for i, corner in enumerate(corners):
                        try:
                            marker_id = int(ids[i][0])
                            if marker_id not in range(52):
                                logger.warning(f"Invalid marker ID {marker_id}")
                                continue

                            x = int(corner[0][0][0])
                            y = int(corner[0][0][1])
                            character = character_map[marker_id]
                            player = 'p1' if x < 320 else 'p2'  # I split the frame at x=320 for Player 1 (left) and Player 2 (right).

                            if marker_id not in assigned_cards:
                                assigned_cards[marker_id] = player
                                logger.info(f"Marker ID {marker_id} ({character}) assigned to {player}")

                            player_cards[player].add(marker_id)
                            card_positions[player]['x'] = x
                            card_positions[player]['y'] = y
                            card_positions[player]['character'] = character
                            dx = corner[0][1][0] - corner[0][0][0]
                            dy = corner[0][1][1] - corner[0][0][1]
                            rot = float(np.arctan2(dy, dx) * 180 / np.pi)  # I calculate rotation and convert to float for JSON serialization.
                            card_positions[player]['rot'] = rot

                            # I draw the detected markers on the OpenCV window for visual feedback.
                            cv2.aruco.drawDetectedMarkers(display_frame, [corner], np.array([[marker_id]], dtype=np.int32))
                            cv2.putText(display_frame, f"{character} ({player})", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
                        except Exception as e:
                            logger.error(f"Error processing marker {marker_id}: {e}")
                            continue

                        if marker_id in last_positions:
                            last_x, last_y = last_positions[marker_id]
                            # I check for significant movement to reassign cards if needed.
                            if abs(x - last_x) > 50 or abs(y - last_y) > 50:
                                del assigned_cards[marker_id]
                        last_positions[marker_id] = (x, y)

                        player_idx = 0 if player == 'p1' else 1
                        if player_cards[player]:
                            players[player_idx]['selected'] = character
                            # I select an attack based on card rotation (0-180 degrees maps to 4 attacks).
                            attack_idx = min(int(abs(rot) / 45), 3)
                            players[player_idx]['selected_attack'] = players[player_idx]['deck'][0]['attacks'][attack_idx]
                            send_arduino_command(player_idx + 1, 'character_click', character)

                # I clean up assigned cards and positions when markers are no longer detected.
                for marker_id in list(assigned_cards.keys()):
                    if ids is None or marker_id not in ids.flatten():
                        player = assigned_cards[marker_id]
                        card_positions[player]['character'] = None
                        del assigned_cards[marker_id]
                        if marker_id in last_positions:
                            del last_positions[marker_id]

                check_card_limit()  # I enforce the card limit after each frame.

                # I ensure all card position data is JSON serializable to avoid Socket.IO errors.
                serializable_card_positions = {
                    player: {
                        'x': float(pos['x']),
                        'y': float(pos['y']),
                        'rot': float(pos['rot']),
                        'character': pos['character']
                    } for player, pos in card_positions.items()
                }

                sio.emit('update_card_data', {
                    'cardData': serializable_card_positions,
                    'health': [players[0]['health'], players[1]['health']],
                    'points': [players[0]['points'], players[1]['points']],
                    'enemyHealth': enemy_health,
                    'gameState': game_state,
                    'difficulty': float(difficulty_multiplier),
                    'port': port
                }, to=None)

                # I handle Arduino button presses to start the game or trigger attacks.
                for command in read_arduino():
                    if command == "BUTTON_PRESSED" and game_state != 'paused':
                        if game_state == "intro":
                            sio.emit('start_stop', {'gameStarted': True, 'difficulty': difficulty_multiplier}, to=None)
                        elif game_state == "playing":
                            for player_idx in range(2):
                                if players[player_idx]['selected'] and players[player_idx]['selected_attack']:
                                    attack_enemy(player_idx)
                                    sio.emit('attack', {'player': player_idx}, to=None)
                                    send_arduino_command(player_idx + 1, 'attack')
                                    break

                cv2.imshow('Magic Munchkin Battle', display_frame)
                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

            except Exception as e:
                logger.error(f"Error in track_cards loop: {e}")
                time.sleep(0.1)
                continue

            time.sleep(0.1)  # I add a small delay to prevent the loop from running too fast.

    except Exception as e:
        logger.error(f"Critical error in track_cards: {e}")
    finally:
        # I ensure cleanup of resources to avoid leaving the camera or Arduino in a bad state.
        if 'cap' in locals() and cap:
            cap.release()
        if arduino and arduino.is_open:
            arduino.close()
        cv2.destroyAllWindows()
        if 'listener' in locals():
            listener.stop()

# I start the game by setting up decks, enemies, and running the server.
def main():
    players[0]['deck'] = create_deck()
    players[0]['full_deck'] = create_deck()
    players[1]['deck'] = create_deck()
    players[1]['full_deck'] = create_deck()
    global enemies, enemy_health
    enemies = create_enemies()
    enemy_health[:] = [e['health'] for e in enemies]
    threading.Thread(target=enemy_attack, daemon=True).start()
    threading.Thread(target=track_cards, daemon=True).start()
    eventlet.wsgi.server(eventlet.listen((server_ip, port)), app)

if __name__ == '__main__':
    main()

Index.html:

<!DOCTYPE html>
<html>
<head>
  <title>Magic Munchkin Battle - Multiplayer</title>
  <!-- I include p5.js for rendering the game UI in the browser. -->
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.min.js"></script>
  <!-- I add p5.sound for sound effects and background music. -->
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/addons/p5.sound.min.js"></script>
  <!-- I use Socket.IO to communicate with the server in real time. -->
  <script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js"></script>
  <style>
    /* I style the page to fit the canvas and overlay elements. */
    body { margin: 0; overflow: hidden; background: #000; font-family: Arial, sans-serif; }
    canvas { display: block; width: 100%; height: 100%; }
    /* I create an overlay for pause and game over screens. */
    #overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); color: white; display: none; justify-content: center; align-items: center; font-size: 32px; text-align: center; }
    .tooltip { background: rgba(0, 0, 0, 0.8); padding: 10px; border-radius: 5px; }
    /* I position buttons for muting and starting/stopping the game. */
    #muteButton { position: absolute; top: 10px; right: 10px; padding: 5px 10px; background: #444; color: white; border: none; cursor: pointer; }
    #startButton { position: absolute; top: 50px; right: 10px; padding: 5px 10px; background: #4CAF50; color: white; border: none; cursor: pointer; }
  </style>
</head>
<body>
  <div id="overlay"></div>
  <button id="muteButton" onclick="toggleMute()">Mute</button>
  <button id="startButton" onclick="startStop()">Start/Stop</button>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      let socket;  // I’ll use this to connect to the server via Socket.IO.
      let gameState = 'intro';  // I track the game state (intro, playing, paused, gameover).
      // I define player data for both Player 1 and Player 2.
      let players = [
        { health: 100, deck: [], discardPile: [], fullDeck: [], score: 0, selectedCard: null, selectedAttack: null, statusEffects: [] },
        { health: 100, deck: [], discardPile: [], fullDeck: [], score: 0, selectedCard: null, selectedAttack: null, statusEffects: [] }
      ];
      let enemies = [];  // I store enemy data here.
      let enemyHealth = [];  // I track enemy health for rendering.
      let currentEnemy = 0;  // I keep track of which enemy is being targeted.
      let difficultyMultiplier = 1.0;  // I use this to show the current difficulty level.
      let message = '';  // I display temporary messages like attack notifications.
      let messageTimer = 0;  // I control how long messages are shown.
      let timer = 0;  // I track game duration for display.
      let gameStarted = false;  // I track if the game has started.
      let playerId = null;  // I store the player ID (0 for P1, 1 for P2, null for spectator).
      let isMuted = false;  // I track if sound is muted.

      let images = {};  // I store all game images here.
      let sounds = {};  // I store sound effects here.
      let backgroundMusic;  // I load background music separately.

      // I store card data for both players, updated by the server.
      let cardData = { p1: { x: 0, y: 0, rot: 0, character: null }, p2: { x: 0, y: 0, rot: 0, character: null } };

      // I list all characters for image and sound loading.
      const characters = [
        "Finn", "Jake", "Marceline", "Flame Princess", "Ice King", "Princess Bubblegum", "BMO",
        "Lumpy Space Princess", "Banana Guard", "Female Jake", "Gunther", "Female Finn", "Tree Trunks"
      ];

      // I preload all images and sounds to ensure they’re ready before the game starts.
      function preload() {
        // I wrote a helper to load images with a fallback if they fail to load.
        const loadImageSafe = (key, filename, fallbackColor) => {
          images[key] = loadImage(filename, () => console.log(`${key} loaded`), () => {
            console.error(`${key} failed, using fallback`);
            images[key] = createImage(80, 80);
            images[key].loadPixels();
            for (let i = 0; i < images[key].pixels.length; i += 4) {
              images[key].pixels[i] = red(fallbackColor);
              images[key].pixels[i + 1] = green(fallbackColor);
              images[key].pixels[i + 2] = blue(fallbackColor);
              images[key].pixels[i + 3] = 255;
            }
            images[key].updatePixels();
          });
        };

        // I wrote a helper to load sounds with error handling.
        const loadSoundSafe = (key, filename) => {
          sounds[key] = loadSound(filename, () => console.log(`${key} loaded`), () => console.error(`${key} failed`));
        };

        // I load all character images, enemies, and UI elements with fallbacks.
        loadImageSafe('background', 'data/ooo_background.png', color(0, 100, 200));
        loadImageSafe('finn', 'data/finn.png', color(30, 144, 255));
        loadImageSafe('jake', 'data/jake.png', color(255, 215, 0));
        loadImageSafe('marceline', 'data/marceline.png', color(128, 0, 128));
        loadImageSafe('flame_princess', 'data/flame_princess.png', color(255, 69, 0));
        loadImageSafe('ice_king', 'data/ice_king.png', color(0, 191, 255));
        loadImageSafe('princess_bubblegum', 'data/princess_bubblegum.png', color(255, 105, 180));
        loadImageSafe('bmo', 'data/bmo.png', color(46, 139, 87));
        loadImageSafe('lumpy_space_princess', 'data/lumpy_space_princess.png', color(186, 85, 211));
        loadImageSafe('banana_guard', 'data/banana_guard.png', color(255, 255, 0));
        loadImageSafe('female_jake', 'data/female_jake.png', color(218, 165, 32));
        loadImageSafe('gunther', 'data/gunther.png', color(0, 0, 0));
        loadImageSafe('female_finn', 'data/female_finn.png', color(135, 206, 250));
        loadImageSafe('tree_trunks', 'data/tree_trunks.png', color(107, 142, 35));
        loadImageSafe('goblin', 'data/goblin.png', color(50, 205, 50));
        loadImageSafe('slime', 'data/slime.png', color(0, 250, 154));
        loadImageSafe('troll', 'data/troll.png', color(165, 42, 42));
        loadImageSafe('ogre', 'data/ogre.png', color(139, 69, 19));
        loadImageSafe('card_back', 'data/card_back.png', color(25, 25, 112));
        loadImageSafe('bin', 'data/bin.png', color(169, 169, 169));

        // I load sound effects and background music.
        loadSoundSafe('boing', 'data/boing.wav');
        loadSoundSafe('pow', 'data/pow.wav');
        loadSoundSafe('cheer', 'data/cheer.wav');
        loadSoundSafe('discard', 'data/discard.wav');
        backgroundMusic = loadSound('data/background_music.mp3', () => console.log('background_music loaded'), () => console.error('background_music failed'));
      }

      // I set up the p5.js canvas and Socket.IO connection.
      function setup() {
        createCanvas(windowWidth, windowHeight);
        frameRate(60);
        textAlign(CENTER, CENTER);
        imageMode(CENTER);

        socket = io('http://10.228.236.43:5000');  // I connect to my server.
        socket.on('connect', () => {
          console.log('Connected to server');
          socket.emit('join', { type: window.location.pathname === '/watch' ? 'spectator' : 'player' });  // I join as a player or spectator.
        });

        // I assign the player ID when the server responds.
        socket.on('assignPlayer', (data) => {
          playerId = data.playerId;
          console.log(playerId === null ? 'Assigned as Spectator' : `Assigned as Player ${playerId + 1}`);
        });

        // I initialize game data when the server sends it.
        socket.on('init', (data) => {
          players = data.players;
          enemies = data.enemies;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
        });

        // I update card positions and game stats from the server.
        socket.on('update_card_data', (data) => {
          cardData = data.cardData;
          players[0].health = data.health[0];
          players[1].health = data.health[1];
          players[0].score = data.points[0];
          players[1].score = data.points[1];
          enemyHealth = data.enemyHealth;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
        });

        // I update the full game state when the server sends changes.
        socket.on('update_game', (data) => {
          players = data.players.map(p => ({
            ...p,
            deck: p.deck.map(c => new Card(c.name, c.attacks, images[c.name.toLowerCase().replace(/ /g, '_')])),
            discardPile: p.discard_pile,
            fullDeck: p.full_deck,
            selectedCard: p.selected ? { name: p.selected, attacks: p.deck.find(c => c.name === p.selected)?.attacks || [] } : null,
            selectedAttack: p.selected_attack,
            statusEffects: p.status_effects
          }));
          enemies = data.enemies;
          enemyHealth = data.enemyHealth;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
        });

        // I handle card selection events, playing a sound if not muted.
        socket.on('select', (data) => {
          const player = players[data.player];
          player.selectedCard = player.deck.find(c => c.name === data.card) || null;
          player.selectedAttack = data.attack || null;
          if (player.selectedCard && sounds.boing && !isMuted) sounds.boing.play();
        });

        // I play a sound for attacks if not muted.
        socket.on('attack', (data) => {
          if (sounds.pow && !isMuted) sounds.pow.play();
        });

        // I display messages from the server, like attack results.
        socket.on('message', (data) => {
          message = data.text;
          messageTimer = data.timer;
          if (message.includes('Enemy defeated') && sounds.cheer && !isMuted) sounds.cheer.play();
        });

        // I handle game start/stop events, controlling background music.
        socket.on('start_stop', (data) => {
          gameStarted = data.gameStarted;
          gameState = data.gameStarted ? 'playing' : 'intro';
          timer = 0;
          difficultyMultiplier = data.difficulty;
          if (gameStarted && backgroundMusic && !backgroundMusic.isPlaying() && !isMuted) backgroundMusic.loop();
          else if (backgroundMusic) backgroundMusic.stop();
        });

        // I handle drawing cards, playing a sound.
        socket.on('draw_card', (data) => {
          players[data.player].deck.push(new Card(data.card.name, data.card.attacks, images[data.card.name.toLowerCase().replace(/ /g, '_')]));
          if (sounds.boing && !isMuted) sounds.boing.play();
        });

        // I handle discarding cards, playing a sound.
        socket.on('discard_card', (data) => {
          players[data.player].discardPile.push(players[data.player].deck.splice(data.card_index, 1)[0]);
          if (sounds.discard && !isMuted) sounds.discard.play();
        });
      }

      // I resize the canvas when the window size changes.
      function windowResized() {
        resizeCanvas(windowWidth, windowHeight);
      }

      // I draw the game UI each frame.
      function draw() {
        if (images.background) image(images.background, width / 2, height / 2, width, height);
        else background(0, 100, 200);  // I use a fallback background color if the image fails.

        if (gameStarted) timer += deltaTime / 1000;  // I update the game timer.

        let scaleX = width / 800;  // I scale elements based on window size.
        let scaleY = height / 600;

        // I draw the appropriate screen based on the game state.
        if (gameState === 'intro') drawIntroScreen(scaleX, scaleY);
        else if (['playing', 'paused'].includes(gameState)) drawGameScreen(scaleX, scaleY);
        else if (gameState === 'gameover') drawGameoverScreen(scaleX, scaleY);

        // I display the game timer and difficulty in the top-right corner.
        textSize(16 * scaleX);
        fill(255);
        text(`Time: ${Math.floor(timer)}s`, width - 80 * scaleX, 30 * scaleY);
        text(`Difficulty: ${difficultyMultiplier.toFixed(1)}x`, width - 80 * scaleX, 50 * scaleY);

        // I show messages like attack notifications for a set duration.
        if (message && (messageTimer === -1 || messageTimer > 0)) {
          fill(255, 255, 0);
          textSize(20 * scaleX);
          text(message, width / 2, height / 2);
          if (messageTimer !== -1) messageTimer--;
        }

        // I indicate if the client is in spectator mode.
        if (playerId === null) {
          fill(255);
          textSize(20 * scaleX);
          text("Spectator Mode", width / 2, 20 * scaleY);
        }
      }

      // I define a class for attacks to manage loading progress.
      class Attack {
        constructor(name, attack, health, loadTimeSeconds) {
          this.name = name;
          this.attack = attack;
          this.health = health;
          this.loadTimeSeconds = loadTimeSeconds;
          this.startLoadTime = null;
          this.isLoading = false;
        }
      }

      // I define a Card class to handle card visuals and animations.
      class Card {
        constructor(name, attacks, image) {
          this.name = name;
          this.attacks = attacks.map(a => new Attack(a.name, a.attack, a.health, a.load));
          this.image = image;
          this.animOffsetX = 0;
          this.animDirection = random([-1, 1]);
          this.animSpeed = random(0.5, 1.5);
          this.x = 0;
          this.y = 0;
        }

        startLoadingAll() {
          this.attacks.forEach(attack => {
            if (!attack.isLoading) {
              attack.startLoadTime = millis();
              attack.isLoading = true;
            }
          });
        }

        getLoadProgress(attack) {
          if (!attack.isLoading || !attack.startLoadTime) return 0;
          return min((millis() - attack.startLoadTime) / 1000 / attack.loadTimeSeconds, 1);
        }

        updateAnimation() {
          this.animOffsetX += this.animSpeed * this.animDirection;
          if (abs(this.animOffsetX) > 5) this.animDirection *= -1;
        }
      }

      // I draw the main game screen with players, cards, and enemies.
      function drawGameScreen(scaleX, scaleY) {
        players.forEach((player, idx) => {
          // I display player stats like health and score.
          fill(255);
          textSize(16 * scaleX);
          text(`Player ${idx + 1} Health: ${player.health}`, 20 * scaleX, (30 + idx * 50) * scaleY);
          text(`Score: ${player.score}`, 20 * scaleX, (50 + idx * 50) * scaleY);
          fill(255, 0, 0);
          rect(120 * scaleX, (20 + idx * 50) * scaleY, 100 * scaleX, 10 * scaleY);
          fill(0, 255, 0);
          rect(120 * scaleX, (20 + idx * 50) * scaleY, (player.health / 100) * 100 * scaleX, 10 * scaleY);

          // I show status effects if any exist.
          player.statusEffects.forEach((effect, i) => {
            text(`${effect.name}: ${effect.duration} turns`, 20 * scaleX, (70 + idx * 50 + i * 20) * scaleY);
          });

          // I position cards differently for Player 1 (left) and Player 2 (right).
          const startX = idx === 0 ? 50 : 400;
          player.deck.forEach((card, i) => {
            card.updateAnimation();
            card.x = (startX + i * 110) * scaleX;
            card.y = (height - 140) * scaleY;
            // I adjust card position based on AR tracking data.
            if (cardData[`p${idx + 1}`].character === card.name) {
              card.x = cardData[`p${idx + 1}`].x * (width / 640);
              card.y = cardData[`p${idx + 1}`].y * (height / 480);
              push();
              translate(card.x, card.y);
              rotate(radians(cardData[`p${idx + 1}`].rot));
              if (card.image) image(card.image, card.animOffsetX * scaleX, 0, 80 * scaleX, 80 * scaleY);
              pop();
            } else {
              if (card.image) image(card.image, card.x + card.animOffsetX * scaleX, card.y, 80 * scaleX, 80 * scaleY);
            }
            // I highlight the selected card with a yellow border.
            if (player.selectedCard && player.selectedCard.name === card.name) {
              drawingContext.shadowBlur = 20;
              drawingContext.shadowColor = 'yellow';
              stroke(255, 255, 0);
              strokeWeight(4 * scaleX);
              rect((card.x - 45 * scaleX), (card.y - 45 * scaleY), 90 * scaleX, 90 * scaleY);
              strokeWeight(1);
              stroke(0);
              drawingContext.shadowBlur = 0;
            }
            // I show a tooltip with attack details on hover and allow selection.
            if (playerId !== null && card.x - 40 * scaleX <= mouseX && mouseX <= card.x + 40 * scaleX && card.y - 40 * scaleY <= mouseY && mouseY <= card.y + 40 * scaleY) {
              fill(0, 0, 0, 200);
              rect((card.x - 75 * scaleX), (card.y - 120 * scaleY), 150 * scaleX, 100 * scaleY, 10 * scaleX);
              fill(255);
              textAlign(LEFT);
              card.attacks.forEach((attack, j) => {
                text(`${attack.name}: ATK ${attack.attack}, HP ${attack.health}, ${attack.loadTimeSeconds}s`, (card.x - 70 * scaleX), (card.y - 100 + j * 20) * scaleY);
              });
              textAlign(CENTER);
              if (mouseIsPressed) {
                socket.emit('select', { player: playerId, card: card.name });
              }
            }

            // I show a loading bar for the selected attack.
            if (player.selectedCard && player.selectedCard.name === card.name && player.selectedAttack) {
              const progress = card.getLoadProgress(player.selectedAttack);
              fill(255, 0, 0);
              rect((card.x - 40 * scaleX), (card.y + 45 * scaleY), 80 * scaleX, 5 * scaleY);
              fill(0, 255, 0);
              rect((card.x - 40 * scaleX), (card.y + 45 * scaleY), 80 * progress * scaleX, 5 * scaleY);
              if (progress >= 1 && playerId !== null) {
                socket.emit('attack', { player: playerId });
              }
            }
          });

          // I draw the draw pile for players to pick new cards.
          if (images.card_back && playerId !== null) {
            image(images.card_back, idx === 0 ? (width - 200 * scaleX) : (width - 100 * scaleX), (height - 100) * scaleY, 80 * scaleX, 80 * scaleY);
            text(`Draw (${player.fullDeck.length})`, idx === 0 ? (width - 160 * scaleX) : (width - 60 * scaleX), (height - 60) * scaleY);
            if (mouseX >= (idx === 0 ? width - 240 * scaleX : width - 140 * scaleX) && mouseX <= (idx === 0 ? width - 160 * scaleX : width - 60 * scaleX) &&
                mouseY >= (height - 140 * scaleY) && mouseY <= (height - 60 * scaleY) && mouseIsPressed) {
              socket.emit('draw_card', { player: playerId });
            }
          }

          // I draw the discard pile for players to discard cards.
          if (images.bin && playerId !== null) {
            image(images.bin, idx === 0 ? (width - 100 * scaleX) : (width - 200 * scaleX), (height - 100) * scaleY, 80 * scaleX, 80 * scaleY);
            text(`Discard (${player.discardPile.length})`, idx === 0 ? (width - 60 * scaleX) : (width - 160 * scaleX), (height - 60) * scaleY);
            if (mouseX >= (idx === 0 ? width - 140 * scaleX : width - 240 * scaleX) && mouseX <= (idx === 0 ? width - 60 * scaleX : width - 160 * scaleX) &&
                mouseY >= (height - 140 * scaleY) && mouseY <= (height - 60 * scaleY) && mouseIsPressed) {
              const cardIndex = floor((mouseX - (idx === 0 ? 50 * scaleX : 400 * scaleX)) / (110 * scaleX));
              if (cardIndex >= 0 && cardIndex < player.deck.length) {
                socket.emit('discard_card', { player: playerId, card_index: cardIndex });
              }
            }
          }
        });

        // I display enemies with their health bars.
        enemies.forEach((enemy, i) => {
          fill(0, 255, 0);
          textSize(14 * scaleX);
          text(`${enemy.name} Health: ${enemy.health}`, (width - 150 * scaleX), (80 + i * 80) * scaleY);
          fill(255, 0, 0);
          rect((width - 200 * scaleX), (90 + i * 80) * scaleY, 100 * scaleX, 10 * scaleY);
          fill(0, 255, 0);
          rect((width - 200 * scaleX), (90 + i * 80) * scaleY, (enemy.health / (enemy.health + 10)) * 100 * scaleX, 10 * scaleY);
          if (images[enemy.cards[0].image]) {
            image(images[enemy.cards[0].image], (width - 150) * scaleX, (150 + i * 80) * scaleY, 100 * scaleX, 100 * scaleY);
          }
        });

        // I show the pause overlay when the game is paused.
        if (gameState === 'paused') {
          document.getElementById('overlay').style.display = 'flex';
          document.getElementById('overlay').innerText = 'Game Paused\nRemove excess cards';
        } else {
          document.getElementById('overlay').style.display = 'none';
        }
      }

      // I draw the intro screen before the game starts.
      function drawIntroScreen(scaleX, scaleY) {
        fill(255);
        textSize(32 * scaleX);
        text("Magic Munchkin Battle", width / 2, height / 2 - 100 * scaleY);
        textSize(20 * scaleX);
        text("Waiting for game to start...", width / 2, height / 2);
        if (images.finn) image(images.finn, (width / 2 - 100 * scaleX), (height / 2 + 50 * scaleY), 80 * scaleX, 80 * scaleY);
        if (images.jake) image(images.jake, (width / 2 + 20 * scaleX), (height / 2 + 50 * scaleY), 80 * scaleX, 80 * scaleY);
      }

      // I draw the game over screen, showing the final scores.
      function drawGameoverScreen(scaleX, scaleY) {
        document.getElementById('overlay').style.display = 'flex';
        let text = '';
        if (players.some(p => p.health <= 0)) {
          text = "Game Over - Player Lost!\n";
        } else if (!enemies.length) {
          text = "Victory! All Enemies Defeated!\n";
        }
        players.forEach((player, idx) => {
          text += `Player ${idx + 1} Score: ${player.score}\n`;
        });
        document.getElementById('overlay').innerText = text;
      }

      // I toggle sound muting for the game.
      function toggleMute() {
        isMuted = !isMuted;
        document.getElementById('muteButton').textContent = isMuted ? 'Unmute' : 'Mute';
        if (backgroundMusic) backgroundMusic.setVolume(isMuted ? 0 : 1);
      }

      // I handle starting/stopping the game via the button.
      function startStop() {
        socket.emit('start_stop', { gameStarted: !gameStarted });
      }

      // I disable mouse clicks for spectators.
      function mousePressed() {
        if (playerId === null) return;
      }
    });
  </script>
</body>
</html>

ML_model.py:

# I wrote this script to analyze game data and suggest improvements using machine learning.
# It’s a stretch goal to make Magic Munchkin Battle more balanced and fun based on player stats.

import pandas as pd  # I use pandas to handle the CSV data for analysis.
import numpy as np  # NumPy for numerical operations in preprocessing.
from sklearn.ensemble import RandomForestRegressor  # I chose RandomForest for its robustness in predicting win likelihood.
from sklearn.model_selection import train_test_split  # To split data into training and testing sets.
import logging  # Logging helps me track the ML process and errors.
import os  # For file path handling.
import csv  # To create the CSV if it doesn’t exist.

# I set up logging to monitor the ML process.
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# I ensure the game_data.csv file exists with the correct headers.
def initialize_game_data(file_path='game_data.csv'):
    """Create game_data.csv with headers if it doesn't exist."""
    if not os.path.exists(file_path):
        headers = ['timestamp', 'game_state', 'p1_card_count', 'p2_card_count', 'p1_health', 'p2_health', 
                   'enemy_count', 'difficulty', 'attacks', 'pauses', 'game_duration', 'win']
        with open(file_path, 'w', newline='') as f:
            writer = csv.writer(f)
            writer.writerow(headers)
        logger.info(f"Created empty {file_path} with headers")
    return file_path

# I load the game data from the CSV file for analysis.
def load_game_data(file_path='game_data.csv'):
    try:
        initialize_game_data(file_path)
        data = pd.read_csv(file_path)
        if data.empty:
            logger.warning(f"{file_path} is empty. Run games to generate data.")
            return None
        logger.info(f"Loaded {len(data)} game sessions")
        return data
    except Exception as e:
        logger.error(f"Failed to load game data: {e}")
        return None

# I preprocess the data to extract features and the target variable (win).
def preprocess_data(data):
    features = ['p1_card_count', 'p2_card_count', 'p1_health', 'p2_health', 'enemy_count', 'difficulty', 'attacks', 'pauses', 'game_duration']
    target = 'win'
    X = data[features].fillna(0)  # I fill missing values with 0 to avoid errors.
    y = data[target].fillna(0)
    return X, y

# I train a RandomForest model to predict the likelihood of winning.
def train_model(X, y):
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    model = RandomForestRegressor(n_estimators=100, random_state=42)
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)
    logger.info(f"Model R^2 score: {score:.4f}")
    return model

# I analyze feature importance to suggest gameplay improvements.
def suggest_improvements(model, data):
    feature_importance = pd.Series(model.feature_importances_, index=data.columns).sort_values(ascending=False)
    logger.info("Feature Importance:\n" + str(feature_importance))

    suggestions = []
    # I check if pauses are a significant factor, suggesting UI or detection improvements.
    if feature_importance.get('pauses', 0) > 0.2:
        suggestions.append("High pause frequency: Increase card limit to 6 or improve AR detection accuracy.")
    # I check if games are too long, suggesting balance adjustments.
    if feature_importance.get('game_duration', 0) > 0.2 and data['game_duration'].mean() > 600:
        suggestions.append("Games too long: Reduce enemy health by 10% or increase attack power.")
    # I check if the game is too hard based on win rate and difficulty.
    if feature_importance.get('difficulty', 0) > 0.2 and data['win'].mean() < 0.3:
        suggestions.append("Game too hard: Lower difficulty cap to 1.3 or reduce enemy attack frequency.")
    # I check if card counts are an issue, suggesting UI tweaks.
    if feature_importance.get('p1_card_count', 0) > 0.15 or feature_importance.get('p2_card_count', 0) > 0.15:
        suggestions.append("UI: Increase card spacing or improve marker placement guidance.")
    
    return suggestions

# I run the ML analysis and print suggestions.
def main():
    data = load_game_data()
    if data is None:
        logger.info("Please run 'combined_server.py' to play games and generate data in 'game_data.csv'.")
        return
    if len(data) < 10:
        logger.warning(f"Insufficient data: {len(data)} sessions found. Need at least 10 sessions.")
        return
    
    X, y = preprocess_data(data)
    model = train_model(X, y)
    suggestions = suggest_improvements(model, X)
    
    logger.info("Suggested Improvements:")
    for s in suggestions:
        logger.info(f"- {s}")

if __name__ == "__main__":
    main()

Server.js:

// I created this Node.js server to act as a bridge between the Python server and the browser clients.
// It handles client connections, serves static files, and forwards events between the Python server and clients.

const express = require('express');  // I use Express to handle HTTP requests and serve static files.
const http = require('http');  // I need the HTTP module to create a server for Socket.IO.
const { Server } = require('socket.io');  // Socket.IO allows real-time communication with browser clients.
const ioClient = require('socket.io-client');  // I use this to connect to the Python server as a client.
const path = require('path');  // Path helps me handle file paths for serving static files.

const app = express();  // I initialize the Express app to handle HTTP requests.
const server = http.createServer(app);  // I create an HTTP server to integrate with Socket.IO.
const io = new Server(server, {
    cors: {
        origin: '*',  // I allow all origins for CORS to simplify development and testing.
        methods: ['GET', 'POST']  // I restrict methods to GET and POST for safety.
    }
});

// I set up Express to serve static files like images and HTML pages.
// This route serves the 'data' folder for assets like character images and sounds.
app.use('/data', express.static(path.join(__dirname, 'data')));

// I serve the spectator view (watch.html) when clients access the '/watch' route.
app.get('/watch', (req, res) => res.sendFile(path.join(__dirname, 'watch.html')));

// I serve the player view (index.html) for the root route.
app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'index.html')));

// I track the number of active players to ensure we don’t exceed the 2-player limit.
let playerCount = 0;

// I connect to the Python server running on port 5000 to forward game data.
const pythonServerUrl = 'http://10.228.236.43:5000';
const pythonSocket = ioClient(pythonServerUrl);

// I set up event handlers to forward messages from the Python server to all connected clients.
// This ensures that game state updates are propagated to both players and spectators.
pythonSocket.on('connect', () => {
    console.log('Connected to Python server');
});

pythonSocket.on('init', (data) => {
    // I forward the initial game data (players, enemies, etc.) to all clients.
    io.emit('init', data);
});

pythonSocket.on('gameState', (data) => {
    // I forward the game state (intro, playing, etc.) to clients for UI updates.
    io.emit('gameState', data);
});

pythonSocket.on('startStop', (data) => {
    // I forward start/stop events to control the game flow on the client side.
    io.emit('start_stop', data);
});

pythonSocket.on('select', (data) => {
    // I forward card/attack selection events to update the UI for all clients.
    io.emit('select', data);
});

pythonSocket.on('attack', (data) => {
    // I forward attack events to trigger animations and sound effects on clients.
    io.emit('attack', data);
});

pythonSocket.on('draw_card', (data) => {
    // I forward draw card events to update the player’s deck on the UI.
    io.emit('draw_card', data);
});

pythonSocket.on('discard_card', (data) => {
    // I forward discard card events to update the player’s discard pile on the UI.
    io.emit('discard_card', data);
});

pythonSocket.on('update_card_data', (data) => {
    // I forward card position and game stats updates to keep the UI in sync.
    io.emit('update_card_data', data);
});

pythonSocket.on('update_game', (data) => {
    // I forward full game state updates (players, enemies, etc.) to clients.
    io.emit('update_game', data);
});

pythonSocket.on('message', (data) => {
    // I forward in-game messages (e.g., attack results) to display on the client UI.
    io.emit('message', data);
});

pythonSocket.on('disconnect', () => {
    // I log when the connection to the Python server is lost to debug potential issues.
    console.log('Disconnected from Python server');
});

// I handle client connections to the Node.js server via Socket.IO.
io.on('connection', (socket) => {
    console.log(`Client connected: ${socket.id}`);

    socket.on('join', (data) => {
        // I assign the client as a player if there are fewer than 2 players.
        if (data.type === 'player' && playerCount < 2) {
            const playerId = playerCount;
            playerCount++;
            socket.playerId = playerId;  // I attach the playerId to the socket for tracking.
            socket.emit('assignPlayer', { playerId });
            console.log(`Assigned client ${socket.id} as Player ${playerId + 1}`);
        } else {
            // Otherwise, the client is a spectator.
            socket.emit('assignPlayer', { playerId: null });
            console.log(`Assigned client ${socket.id} as Spectator`);
        }
    });

    // I forward client events to the Python server to process game logic.
    socket.on('start_stop', (data) => {
        pythonSocket.emit('start_stop', data);
    });

    socket.on('select', (data) => {
        pythonSocket.emit('select', data);
    });

    socket.on('attack', (data) => {
        pythonSocket.emit('attack', data);
    });

    socket.on('draw_card', (data) => {
        pythonSocket.emit('draw_card', data);
    });

    socket.on('discard_card', (data) => {
        pythonSocket.emit('discard_card', data);
    });

    socket.on('disconnect', () => {
        // I clean up player slots when a client disconnects.
        console.log(`Client disconnected: ${socket.id}`);
        if (socket.playerId !== null && socket.playerId !== undefined) {
            playerCount = Math.max(0, playerCount - 1);
        }
    });
});

// I start the Node.js server on port 5001, separate from the Python server on 5000.
const PORT = 5001;
server.listen(PORT, '0.0.0.0', () => {
    console.log(`Node.js server running on http://10.228.236.43:${PORT}`);
});

 

Watch.html:

<!DOCTYPE html>
<html>
<head>
  <title>Magic Munchkin Battle - Spectator View</title>
  <!-- I include p5.js to handle the rendering of the game state for spectators. -->
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.min.js"></script>
  <!-- I include p5.sound for playing sound effects, even though spectators don’t interact much. -->
  <script src="https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/addons/p5.sound.min.js"></script>
  <!-- I use Socket.IO to receive real-time updates from the server about the game state. -->
  <script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js"></script>
  <style>
    /* I style the page to ensure the canvas fits the screen and text is readable. */
    body { margin: 0; overflow: hidden; background: #000; font-family: Arial, sans-serif; }
    canvas { display: block; width: 100%; height: 100%; }
    /* I create an overlay for displaying paused or game over states. */
    #overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); color: white; display: none; justify-content: center; align-items: center; font-size: 32px; text-align: center; }
    /* I add a mute button for spectators to control sound. */
    #muteButton { position: absolute; top: 10px; right: 10px; padding: 5px 10px; background: #444; color: white; border: none; cursor: pointer; }
  </style>
</head>
<body>
  <div id="overlay"></div>
  <button id="muteButton" onclick="toggleMute()">Mute</button>
  <script>
    document.addEventListener('DOMContentLoaded', () => {
      let socket;  // I’ll use this to connect to the server via Socket.IO.
      let gameState = 'intro';  // I track the game state to show the correct screen.
      // I define player data for rendering purposes, updated by the server.
      let players = [
        { health: 100, deck: [], discardPile: [], fullDeck: [], score: 0, selectedCard: null, selectedAttack: null, statusEffects: [] },
        { health: 100, deck: [], discardPile: [], fullDeck: [], score: 0, selectedCard: null, selectedAttack: null, statusEffects: [] }
      ];
      let enemies = [];  // I store enemy data for display.
      let enemyHealth = [];  // I track enemy health to show health bars.
      let difficultyMultiplier = 1.0;  // I display the current difficulty level.
      let message = '';  // I show messages like attack notifications.
      let messageTimer = 0;  // I control how long messages are displayed.
      let timer = 0;  // I track game duration for display.
      let gameStarted = false;  // I track if the game has started.
      let isMuted = false;  // I let spectators mute sounds.
      let port = 5000;  // I set the default port, updated by server messages.

      let images = {};  // I store all game images here.
      let sounds = {};  // I store sound effects here.
      let backgroundMusic;  // I load background music separately.

      // I store card data for rendering player positions, updated by the server.
      let cardData = { p1: { x: 0, y: 0, rot: 0, character: null }, p2: { x: 0, y: 0, rot: 0, character: null } };

      // I list all characters to load their images and sounds.
      const characters = [
        "Finn", "Jake", "Marceline", "Flame Princess", "Ice King", "Princess Bubblegum", "BMO",
        "Lumpy Space Princess", "Banana Guard", "Female Jake", "Gunther", "Female Finn", "Tree Trunks"
      ];

      // I preload all assets to ensure they’re ready before the game starts.
      function preload() {
        // I wrote a helper to load images with a fallback if they fail to load.
        const loadImageSafe = (key, filename, fallbackColor) => {
          images[key] = loadImage(filename, () => console.log(`${key} loaded`), () => {
            console.error(`${key} failed, using fallback`);
            images[key] = createImage(80, 80);
            images[key].loadPixels();
            for (let i = 0; i < images[key].pixels.length; i += 4) {
              images[key].pixels[i] = red(fallbackColor);
              images[key].pixels[i + 1] = green(fallbackColor);
              images[key].pixels[i + 2] = blue(fallbackColor);
              images[key].pixels[i + 3] = 255;
            }
            images[key].updatePixels();
          });
        };

        // I wrote a helper to load sounds with error handling.
        const loadSoundSafe = (key, filename) => {
          sounds[key] = loadSound(filename, () => console.log(`${key} loaded`), () => console.error(`${key} failed`));
        };

        // I load all images for characters, enemies, and UI elements.
        loadImageSafe('background', 'data/ooo_background.png', color(0, 100, 200));
        loadImageSafe('finn', 'data/finn.png', color(30, 144, 255));
        loadImageSafe('jake', 'data/jake.png', color(255, 215, 0));
        loadImageSafe('marceline', 'data/marceline.png', color(128, 0, 128));
        loadImageSafe('flame_princess', 'data/flame_princess.png', color(255, 69, 0));
        loadImageSafe('ice_king', 'data/ice_king.png', color(0, 191, 255));
        loadImageSafe('princess_bubblegum', 'data/princess_bubblegum.png', color(255, 105, 180));
        loadImageSafe('bmo', 'data/bmo.png', color(46, 139, 87));
        loadImageSafe('lumpy_space_princess', 'data/lumpy_space_princess.png', color(186, 85, 211));
        loadImageSafe('banana_guard', 'data/banana_guard.png', color(255, 255, 0));
        loadImageSafe('female_jake', 'data/female_jake.png', color(218, 165, 32));
        loadImageSafe('gunther', 'data/gunther.png', color(0, 0, 0));
        loadImageSafe('female_finn', 'data/female_finn.png', color(135, 206, 250));
        loadImageSafe('tree_trunks', 'data/tree_trunks.png', color(107, 142, 35));
        loadImageSafe('goblin', 'data/goblin.png', color(50, 205, 50));
        loadImageSafe('slime', 'data/slime.png', color(0, 250, 154));
        loadImageSafe('troll', 'data/troll.png', color(165, 42, 42));
        loadImageSafe('ogre', 'data/ogre.png', color(139, 69, 19));
        loadImageSafe('card_back', 'data/card_back.png', color(25, 25, 112));
        loadImageSafe('bin', 'data/bin.png', color(169, 169, 169));

        // I load sound effects and background music for the spectator view.
        loadSoundSafe('boing', 'data/boing.wav');
        loadSoundSafe('pow', 'data/pow.wav');
        loadSoundSafe('cheer', 'data/cheer.wav');
        loadSoundSafe('discard', 'data/discard.wav');
        backgroundMusic = loadSound('data/background_music.mp3', () => console.log('background_music loaded'), () => console.error('background_music failed'));
      }

      // I set up the p5.js canvas and Socket.IO connection for spectators.
      function setup() {
        createCanvas(windowWidth, windowHeight);
        frameRate(60);
        textAlign(CENTER, CENTER);
        imageMode(CENTER);

        socket = io('http://10.228.236.43:5000');  // I connect to the server.
        socket.on('connect', () => {
          console.log('Connected to server as spectator');
          socket.emit('join', { type: 'spectator' });  // I join as a spectator.
        });

        // I initialize game data when the server sends it.
        socket.on('init', (data) => {
          players = data.players;
          enemies = data.enemies;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
          port = data.port;
        });

        // I update card positions and game stats from the server.
        socket.on('update_card_data', (data) => {
          cardData = data.cardData;
          players[0].health = data.health[0];
          players[1].health = data.health[1];
          players[0].score = data.points[0];
          players[1].score = data.points[1];
          enemyHealth = data.enemyHealth;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
          port = data.port;
        });

        // I update the full game state when the server sends changes.
        socket.on('update_game', (data) => {
          players = data.players.map(p => ({
            ...p,
            deck: p.deck.map(c => new Card(c.name, c.attacks, images[c.name.toLowerCase().replace(/ /g, '_')])),
            discardPile: p.discard_pile,
            fullDeck: p.full_deck,
            selectedCard: p.selected ? { name: p.selected, attacks: p.deck.find(c => c.name === p.selected)?.attacks || [] } : null,
            selectedAttack: p.selected_attack,
            statusEffects: p.status_effects
          }));
          enemies = data.enemies;
          enemyHealth = data.enemyHealth;
          gameState = data.gameState;
          difficultyMultiplier = data.difficulty;
        });

        // I play a sound when a card is selected, if not muted.
        socket.on('select', (data) => {
          const player = players[data.player];
          player.selectedCard = player.deck.find(c => c.name === data.card) || null;
          player.selectedAttack = data.attack || null;
          if (player.selectedCard && sounds.boing && !isMuted) sounds.boing.play();
        });

        // I play a sound for attacks, if not muted.
        socket.on('attack', (data) => {
          if (sounds.pow && !isMuted) sounds.pow.play();
        });

        // I display messages like attack results or enemy defeats.
        socket.on('message', (data) => {
          message = data.text;
          messageTimer = data.timer;
          if (message.includes('Enemy defeated') && sounds.cheer && !isMuted) sounds.cheer.play();
        });

        // I handle game start/stop events, controlling background music.
        socket.on('start_stop', (data) => {
          gameStarted = data.gameStarted;
          gameState = data.gameStarted ? 'playing' : 'intro';
          timer = 0;
          difficultyMultiplier = data.difficulty;
          port = data.port;
          if (gameStarted && backgroundMusic && !backgroundMusic.isPlaying() && !isMuted) backgroundMusic.loop();
          else if (backgroundMusic) backgroundMusic.stop();
        });

        // I update the spectator’s view when a card is drawn.
        socket.on('draw_card', (data) => {
          players[data.player].deck.push(new Card(data.card.name, data.card.attacks, images[data.card.name.toLowerCase().replace(/ /g, '_')]));
          if (sounds.boing && !isMuted) sounds.boing.play();
        });

        // I update the spectator’s view when a card is discarded.
        socket.on('discard_card', (data) => {
          players[data.player].discardPile.push(players[data.player].deck.splice(data.card_index, 1)[0]);
          if (sounds.discard && !isMuted) sounds.discard.play();
        });
      }

      // I resize the canvas when the window size changes.
      function windowResized() {
        resizeCanvas(windowWidth, windowHeight);
      }

      // I draw the game UI each frame for spectators.
      function draw() {
        if (images.background) image(images.background, width / 2, height / 2, width, height);
        else background(0, 100, 200);  // I use a fallback background if the image fails.

        if (gameStarted) timer += deltaTime / 1000;  // I update the game timer.

        let scaleX = width / 800;  // I scale elements based on window size.
        let scaleY = height / 600;

        // I draw the appropriate screen based on the game state.
        if (gameState === 'intro') drawIntroScreen(scaleX, scaleY);
        else if (['playing', 'paused'].includes(gameState)) drawGameScreen(scaleX, scaleY);
        else if (gameState === 'gameover') drawGameoverScreen(scaleX, scaleY);

        // I display the game timer and difficulty in the top-right corner.
        textSize(16 * scaleX);
        fill(255);
        text(`Time: ${Math.floor(timer)}s`, width - 80 * scaleX, 30 * scaleY);
        text(`Difficulty: ${difficultyMultiplier.toFixed(1)}x`, width - 80 * scaleX, 50 * scaleY);

        // I show messages for a set duration.
        if (message && (messageTimer === -1 || messageTimer > 0)) {
          fill(255, 255, 0);
          textSize(20 * scaleX);
          text(message, width / 2, height / 2);
          if (messageTimer !== -1) messageTimer--;
        }

        // I indicate that this is the spectator view.
        fill(255);
        textSize(20 * scaleX);
        text("Spectator Mode", width / 2, 20 * scaleY);
      }

      // I define a class for attacks, though spectators don’t interact with them.
      class Attack {
        constructor(name, attack, health, loadTimeSeconds) {
          this.name = name;
          this.attack = attack;
          this.health = health;
          this.loadTimeSeconds = loadTimeSeconds;
          this.startLoadTime = null;
          this.isLoading = false;
        }
      }

      // I define a Card class to handle card visuals and animations.
      class Card {
        constructor(name, attacks, image) {
          this.name = name;
          this.attacks = attacks.map(a => new Attack(a.name, a.attack, a.health, a.load));
          this.image = image;
          this.animOffsetX = 0;
          this.animDirection = random([-1, 1]);
          this.animSpeed = random(0.5, 1.5);
          this.x = 0;
          this.y = 0;
        }

        startLoadingAll() {
          this.attacks.forEach(attack => {
            if (!attack.isLoading) {
              attack.startLoadTime = millis();
              attack.isLoading = true;
            }
          });
        }

        getLoadProgress(attack) {
          if (!attack.isLoading || !attack.startLoadTime) return 0;
          return min((millis() - attack.startLoadTime) / 1000 / attack.loadTimeSeconds, 1);
        }

        updateAnimation() {
          this.animOffsetX += this.animSpeed * this.animDirection;
          if (abs(this.animOffsetX) > 5) this.animDirection *= -1;
        }
      }

      // I draw the main game screen for spectators.
      function drawGameScreen(scaleX, scaleY) {
        players.forEach((player, idx) => {
          // I display player stats like health and score.
          fill(255);
          textSize(16 * scaleX);
          text(`Player ${idx + 1} Health: ${player.health}`, 20 * scaleX, (30 + idx * 50) * scaleY);
          text(`Score: ${player.score}`, 20 * scaleX, (50 + idx * 50) * scaleY);
          fill(255, 0, 0);
          rect(120 * scaleX, (20 + idx * 50) * scaleY, 100 * scaleX, 10 * scaleY);
          fill(0, 255, 0);
          rect(120 * scaleX, (20 + idx * 50) * scaleY, (player.health / 100) * 100 * scaleX, 10 * scaleY);

          // I show status effects for each player.
          player.statusEffects.forEach((effect, i) => {
            text(`${effect.name}: ${effect.duration} turns`, 20 * scaleX, (70 + idx * 50 + i * 20) * scaleY);
          });

          // I position cards differently for Player 1 (left) and Player 2 (right).
          const startX = idx === 0 ? 50 : 400;
          player.deck.forEach((card, i) => {
            card.updateAnimation();
            card.x = (startX + i * 110) * scaleX;
            card.y = (height - 140) * scaleY;
            // I adjust card position based on AR tracking data.
            if (cardData[`p${idx + 1}`].character === card.name) {
              card.x = cardData[`p${idx + 1}`].x * (width / 640);
              card.y = cardData[`p${idx + 1}`].y * (height / 480);
              push();
              translate(card.x, card.y);
              rotate(radians(cardData[`p${idx + 1}`].rot));
              if (card.image) image(card.image, card.animOffsetX * scaleX, 0, 80 * scaleX, 80 * scaleY);
              pop();
            } else {
              if (card.image) image(card.image, card.x + card.animOffsetX * scaleX, card.y, 80 * scaleX, 80 * scaleY);
            }
            // I highlight the selected card with a yellow border.
            if (player.selectedCard && player.selectedCard.name === card.name) {
              drawingContext.shadowBlur = 20;
              drawingContext.shadowColor = 'yellow';
              stroke(255, 255, 0);
              strokeWeight(4 * scaleX);
              rect((card.x - 45 * scaleX), (card.y - 45 * scaleY), 90 * scaleX, 90 * scaleY);
              strokeWeight(1);
              stroke(0);
              drawingContext.shadowBlur = 0;
            }
            // I show a loading bar for the selected attack.
            if (player.selectedCard && player.selectedCard.name === card.name && player.selectedAttack) {
              const progress = card.getLoadProgress(player.selectedAttack);
              fill(255, 0, 0);
              rect((card.x - 40 * scaleX), (card.y + 45 * scaleY), 80 * scaleX, 5 * scaleY);
              fill(0, 255, 0);
              rect((card.x - 40 * scaleX), (card.y + 45 * scaleY), 80 * progress * scaleX, 5 * scaleY);
            }
          });

          // I show the draw and discard piles’ counts.
          if (images.card_back) {
            image(images.card_back, idx === 0 ? (width - 200 * scaleX) : (width - 100 * scaleX), (height - 100) * scaleY, 80 * scaleX, 80 * scaleY);
            text(`Draw (${player.fullDeck.length})`, idx === 0 ? (width - 160 * scaleX) : (width - 60 * scaleX), (height - 60) * scaleY);
          }
          if (images.bin) {
            image(images.bin, idx === 0 ? (width - 100 * scaleX) : (width - 200 * scaleX), (height - 100) * scaleY, 80 * scaleX, 80 * scaleY);
            text(`Discard (${player.discardPile.length})`, idx === 0 ? (width - 60 * scaleX) : (width - 160 * scaleX), (height - 60) * scaleY);
          }
        });

        // I display enemies with their health bars.
        enemies.forEach((enemy, i) => {
          fill(0, 255, 0);
          textSize(14 * scaleX);
          text(`${enemy.name} Health: ${enemy.health}`, (width - 150 * scaleX), (80 + i * 80) * scaleY);
          fill(255, 0, 0);
          rect((width - 200 * scaleX), (90 + i * 80) * scaleY, 100 * scaleX, 10 * scaleY);
          fill(0, 255, 0);
          rect((width - 200 * scaleX), (90 + i * 80) * scaleY, (enemy.health / (enemy.health + 10)) * 100 * scaleX, 10 * scaleY);
          if (images[enemy.cards[0].image]) {
            image(images[enemy.cards[0].image], (width - 150) * scaleX, (150 + i * 80) * scaleY, 100 * scaleX, 100 * scaleY);
          }
        });

        // I show the pause overlay when the game is paused.
        if (gameState === 'paused') {
          document.getElementById('overlay').style.display = 'flex';
          document.getElementById('overlay').innerText = 'Game Paused\nRemove excess cards';
        } else {
          document.getElementById('overlay').style.display = 'none';
        }
      }

      // I draw the intro screen for spectators.
      function drawIntroScreen(scaleX, scaleY) {
        fill(255);
        textSize(32 * scaleX);
        text("Magic Munchkin Battle", width / 2, height / 2 - 100 * scaleY);
        textSize(20 * scaleX);
        text("Waiting for game to start...", width / 2, height / 2);
        if (images.finn) image(images.finn, (width / 2 - 100 * scaleX), (height / 2 + 50 * scaleY), 80 * scaleX, 80 * scaleY);
        if (images.jake) image(images.jake, (width / 2 + 20 * scaleX), (height / 2 + 50 * scaleY), 80 * scaleX, 80 * scaleY);
      }

      // I draw the game over screen, showing final scores.
      function drawGameoverScreen(scaleX, scaleY) {
        document.getElementById('overlay').style.display = 'flex';
        let text = '';
        if (players.some(p => p.health <= 0)) {
          text = "Game Over - Player Lost!\n";
        } else if (!enemies.length) {
          text = "Victory! All Enemies Defeated!\n";
        }
        players.forEach((player, idx) => {
          text += `Player ${idx + 1} Score: ${player.score}\n`;
        });
        document.getElementById('overlay').innerText = text;
      }

      // I toggle sound muting for spectators.
      function toggleMute() {
        isMuted = !isMuted;
        document.getElementById('muteButton').textContent = isMuted ? 'Unmute' : 'Mute';
        if (backgroundMusic) backgroundMusic.setVolume(isMuted ? 0 : 1);
      }

      // I disable mouse clicks since spectators can’t interact.
      function mousePressed() {
        return;
      }
    });
  </script>
</body>
</html>

Generate_markers.py:

# I wrote this script to generate ArUco markers for Magic Munchkin Battle.
# These markers are used for augmented reality card tracking, linking physical cards to digital characters.

import cv2  # I use OpenCV to generate and manipulate the ArUco markers.
import numpy as np  # NumPy helps create the initial marker array and handle image data.

# I select the DICT_4X4_100 dictionary, which supports 100 unique 4x4 ArUco markers.
# This dictionary suits my needs since I need 52 markers (4 per character for 13 Adventure Time characters).
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100)

# I loop through 52 markers (0 to 51) to generate one for each card in the game.
for i in range(52):
    # I create a blank 200x200 pixel image to hold each marker.
    marker = np.zeros((200, 200), dtype=np.uint8)
    # I generate the ArUco marker image for the current ID, using 200x200 pixels with a 1-pixel border.
    # The border ensures the marker is detectable by OpenCV’s ArUco detector.
    marker = cv2.aruco.generateImageMarker(aruco_dict, i, 200, marker, 1)
    # I save each marker as a PNG file in the 'markers' folder with a unique filename.
    # I chose PNG for lossless quality and to maintain the black-and-white pattern.
    cv2.imwrite(f'markers/marker_{i}.png', marker)

Setup_instructions.txt:

# setup_instructions.txt - Installation and Setup Guide for Magic Munchkin Battle
# Created by [Your Name] on May 09, 2025
# This file contains everything needed to set up and run Magic Munchkin Battle, including dependencies,
# external resources, and step-by-step instructions. I’ve included detailed comments to help others
# understand why each item is required and how to troubleshoot potential issues. I noticed some gaps
# in my earlier lists, so I’ve merged and enhanced them to be thorough—let’s make this project easy
# to share and debug!

# --- Python Dependencies (via pip) ---
# These are the Python packages I chose to power the game’s core functionality, AR tracking, and ML analysis.
# Install them in the virtual environment at /Users/Worker/Desktop/magic_munchkin_battle/.venv.
# If a package fails to install, check your internet connection or try updating pip with 'pip install --upgrade pip'.

# opencv-python: I picked this for AR marker detection and video capture in combined_server.py, and to generate markers in generate_markers.py.
# Troubleshooting: If installation fails, ensure you have a compatible OpenCV version (e.g., 4.x) and sufficient disk space.
pip install opencv-python

# numpy: I use this for numerical operations like marker coordinates and array handling across combined_server.py, generate_markers.py, and ml_model.py.
# Troubleshooting: If you get import errors, verify numpy installed correctly with 'pip show numpy'.
pip install numpy

# flask-socketio: I went with this to enable real-time communication between the Python server and browser clients, handling events like card updates.
# Troubleshooting: If Socket.IO events don’t work, ensure flask is installed (it’s a dependency) with 'pip install flask'.
pip install flask-socketio

# eventlet: I added this as the async backend for Flask-SocketIO to manage multiple client connections smoothly in combined_server.py.
# Troubleshooting: If you see async errors, confirm eventlet version compatibility (e.g., 0.33.0+) with 'pip show eventlet'.
pip install eventlet

# pyserial: I included this to communicate with the Arduino for LED and sound feedback, though it’s optional if you skip the hardware.
# Troubleshooting: If serial connection fails, check your Arduino port (e.g., /dev/cu.usbmodem11101) and install pyserial with admin rights if needed.
pip install pyserial

# pynput: I threw this in for keyboard input as a fallback when the Arduino isn’t connected, useful for testing with keys like 's' and 'a'.
# Troubleshooting: If keyboard inputs aren’t detected, ensure pynput has permission to access input devices on your OS.
pip install pynput

# pandas: I forgot to mention this earlier, but I use it in ml_model.py to manipulate game data from game_data.csv for ML insights.
# Troubleshooting: If data loading fails, verify pandas installed and game_data.csv exists in the project directory.
pip install pandas

# scikit-learn: I also missed this—it's for the RandomForestRegressor model in ml_model.py to analyze game data and suggest improvements.
# Troubleshooting: If ML models fail, check scikit-learn version (e.g., 1.2+) with 'pip show scikit-learn' and ensure numpy is up-to-date.
pip install scikit-learn

# Note: flask-socketio might pull in flask, python-engineio, and python-socketio as dependencies. If issues arise, I recommend installing flask explicitly with 'pip install flask'.
# Additional Tip: If any pip command fails, try running it with --verbose for more error details (e.g., 'pip install opencv-python --verbose').

# --- Node.js Dependencies (via npm) ---
# These power the Node.js server (server.js) to serve files and bridge the Python server with browsers.
# If npm install fails, ensure Node.js is correctly installed and your internet is active.

# express: I selected this to handle HTTP requests and serve static files like index.html and the data folder in server.js.
# Troubleshooting: If the server doesn’t start, check for port conflicts (default is 5001) or missing files in the data folder.
npm install express

# socket.io: I use this for real-time communication between the Node.js server and browser clients, forwarding game updates.
# Troubleshooting: If real-time updates fail, ensure the version matches the client-side CDN (4.7.5) and check server logs.
npm install socket.io

# socket.io-client: I realized I left this out earlier—it lets the Node.js server connect to the Python server, relaying events to clients.
# Troubleshooting: If the connection to the Python server drops, verify the Python server is running on port 5000 and network settings allow traffic.
npm install socket.io-client

# Note: I hardcoded version 4.7.5 in the browser (via CDN), so I suggest matching it with 'npm install socket.io@4.7.5 socket.io-client@4.7.5' to avoid compatibility issues.
# Also, http and path are Node.js built-ins, so no install needed.
# Additional Tip: Run 'npm list' after installation to confirm versions match.

# --- External Resources (CDNs, Assets, Hardware, Tools) ---

# --- CDNs (Loaded in index.html and watch.html) ---
# These JavaScript libraries are loaded directly in the browser for game rendering and communication.
# I chose CDNs for convenience, but they need internet access.

# p5.js (version 1.4.2): I use this to render the game UI, animations, and visuals like cards and enemies in index.html and watch.html.
# URL: https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/p5.min.js
# No install needed; just include it with <script>.
# Troubleshooting: If visuals don’t load, check your internet or download p5.min.js and serve it locally.

# p5.sound (version 1.4.2): I added this p5.js addon for sound effects and background music in index.html and watch.html.
# URL: https://cdn.jsdelivr.net/npm/p5@1.4.2/lib/addons/p5.sound.min.js
# No install needed; include with <script>.
# Troubleshooting: If sounds fail, verify the data folder has .wav and .mp3 files or check browser audio permissions.

# socket.io-client (version 4.7.5): I rely on this for browser clients to connect to the Node.js server for real-time updates.
# URL: https://cdn.jsdelivr.net/npm/socket.io-client@4.7.5/dist/socket.io.min.js
# No install needed; include with <script>.
# Troubleshooting: If connections fail, ensure the Node.js server is running and the URL (e.g., 10.228.236.43:5001) is accessible.

# Note: If offline, I suggest downloading these files and serving them locally (e.g., place in project folder and update <script> tags).
# Additional Tip: Test CDNs by opening the URLs in a browser to confirm they’re accessible.

# --- Game Assets (Images and Sounds in the data Folder) ---
# These are the visual and audio assets I designed the game around, stored in /Users/Worker/Desktop/magic_munchkin_battle/data/.
# Ensure the data folder is writable to avoid file creation errors.

# Images:
# ooo_background.png: I use this as the background for the OpenCV window and p5.js canvas.
# Character images: finn.png, jake.png, marceline.png, flame_princess.png, ice_king.png, princess_bubblegum.png, bmo.png, lumpy_space_princess.png, banana_guard.png, female_jake.png, gunther.png, female_finn.png, tree_trunks.png - These represent the playable characters.
# Enemy images: goblin.png, slime.png, troll.png, ogre.png - These are the foes players fight.
# UI images: card_back.png (draw pile), bin.png (discard pile) - These enhance the game interface.

# Sounds:
# boing.wav: I play this when a card is selected.
# pow.wav: I trigger this during attacks.
# cheer.wav: I use this when an enemy is defeated.
# discard.wav: I play this when discarding a card.
# background_music.mp3: I loop this for background ambiance.

# How to Source:
# I provided a placeholder script below to generate minimal assets for testing. For the real deal, I recommend sourcing Adventure Time images (with permission) or creating pixel art with tools like Aseprite, and grabbing royalty-free sounds from sites like freesound.org.
# Placeholder Generation (uses a gray value for visibility):
python -c "import cv2, numpy as np; [cv2.imwrite(f'data/{n}.png', np.zeros((50, 50, 3), dtype=np.uint8) + 100) for n in ['finn', 'jake', 'marceline', 'flame_princess', 'ice_king', 'princess_bubblegum', 'bmo', 'lumpy_space_princess', 'banana_guard', 'female_jake', 'gunther', 'female_finn', 'tree_trunks', 'goblin', 'slime', 'troll', 'ogre', 'ooo_background', 'card_back', 'bin']]"
touch data/boing.wav data/pow.wav data/cheer.wav data/discard.wav data/background_music.mp3

# Troubleshooting: If the script fails, ensure cv2 and numpy are installed, and check for write permissions with 'ls -ld data' (should show rw for your user).
# Note: Placeholders are 50x50, but I render at 80x80 in the game, so real assets should match for quality. Also, Adventure Time assets need licensing for public use.
# Additional Tip: Test asset loading by manually adding a sample image (e.g., finn.png) and checking in-game.

# --- ArUco Markers (Generated by generate_markers.py) ---
# These are the physical markers for AR tracking, generated and stored in the data folder (I adjusted from markers/ to data/ for consistency).
# Print these and attach them to cards for AR functionality.

# Generate:
python -c "import cv2; aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100); [cv2.imwrite(f'data/marker_{i}.png', cv2.aruco.drawMarker(aruco_dict, i, 200)) for i in range(52)]"

# Troubleshooting: If generation fails, verify cv2 is installed and the data folder exists. Check disk space with 'df -h'.
# Note: I need to print these and attach them to cards. The script assumes the data folder exists, so I’ll create it if missing. I noticed a discrepancy with markers/ in generate_markers.py—consider updating combined_server.py to use data/ consistently.
# Additional Tip: Test a single marker (e.g., marker_0.png) with a webcam to confirm detection.

# --- Hardware Requirements ---
# These are the physical components I integrated for an enhanced experience.
# Ensure hardware is properly connected before running the game.

# Camera:
# Hardware: Any USB or built-in webcam compatible with macOS.
# Why: I use this for AR marker detection in combined_server.py, assuming 640x480 resolution.
# Troubleshooting: If no video feed, check camera connection, permissions (System Settings > Privacy & Security > Camera), or try a different index (e.g., cv2.VideoCapture(1)).
# Note: On macOS, I might need to grant camera permissions in System Settings > Privacy & Security > Camera.

# Arduino (Optional):
# Hardware: Arduino board (e.g., Uno, Nano) with USB cable.
# Why: I use this for button inputs and LED/sound feedback, connecting to /dev/cu.usbmodem11101 or similar.
# Troubleshooting: If no Arduino response, verify the port (use 'ls /dev/cu*' on macOS or 'ls /dev/ttyUSB*' on Linux), ensure the Arduino is programmed, and check cable connections.
# Note: Port names vary (e.g., /dev/ttyUSB0 on Linux, COM3 on Windows), so I’ll adjust possible_ports if needed.

# --- External Tools ---
# These are the software tools I rely on to build and run the project.
# Install these first to avoid downstream issues.

# Python 3.8+:
# Download: https://www.python.org/downloads/
# Why: I need this to run the Python scripts and set up the virtual environment.
# Troubleshooting: If 'python' isn’t recognized, use 'python3' or install the latest version and add it to your PATH.

# Node.js 14+:
# Download: https://nodejs.org/en/download/
# Why: I use this to run server.js and manage Node.js dependencies with npm.
# Troubleshooting: If 'node' or 'npm' fails, reinstall Node.js and verify with 'node -v' and 'npm -v'.

# npm:
# Included with Node.js.
# Why: I need this to install Node.js packages like express and socket.io.
# Troubleshooting: If npm commands fail, update it with 'npm install -g npm'.

# Git (Optional):
# Download: https://git-scm.com/downloads
# Why: I think version control is handy for tracking changes, though it’s not mandatory.
# Troubleshooting: If git isn’t found, install it or skip this step.

# Arduino IDE (Optional):
# Download: https://www.arduino.cc/en/software
# Why: I need this to program the Arduino for serial commands if using hardware.
# Troubleshooting: If the IDE fails to upload, check board selection (e.g., Uno) and port settings.

# Homebrew (Optional):
# Install: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Why: I find this useful for installing tools like Git on macOS, though it’s not required if I install manually.
# Example: brew install git
# Troubleshooting: If installation hangs, ensure curl works and you have admin rights.

# Info.plist (macOS-specific):
# Create:
mkdir -p /Users/Worker/Desktop/magic_munchkin_battle/.venv/bin
cat > /Users/Worker/Desktop/magic_munchkin_battle/.venv/bin/Info.plist << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSCameraUseContinuityCameraDeviceType</key>
    <string>YES</string>
</dict>
</plist>
EOF
# Why: I added this to disable macOS Continuity Camera, ensuring OpenCV uses my USB webcam.
# Troubleshooting: If camera still uses Continuity, verify the file is in .venv/bin and restart the terminal.

# --- Project Directory Structure ---
# Base Directory: /Users/Worker/Desktop/magic_munchkin_battle
# Virtual Environment: /Users/Worker/Desktop/magic_munchkin_battle/.venv
# Folders: data/ (assets and markers), markers/ (optional if I revert to separate marker storage)
# Files: game_data.csv (generated by combined_server.py and used by ml_model.py)

# Note: I need to ensure the data folder exists and has write permissions (chmod -R 755 . on Unix).
# Troubleshooting: If files can’t be created, check permissions with 'ls -ld data' and fix with 'chmod -R u+w data'.

# --- Installation Instructions ---
# Here’s how I set up the project—follow these steps in the terminal to get started.
# Run each step carefully and check for errors—don’t skip verification!

1. **Set Up Python Environment**:
   cd /Users/Worker/Desktop/magic_munchkin_battle
   python -m venv .venv
   source .venv/bin/activate  # On Windows, use .venv\Scripts\activate
   pip install opencv-python numpy flask-socketio eventlet pyserial pynput pandas scikit-learn
   # Troubleshooting: If venv fails, ensure python is installed and try 'python3 -m venv .venv'. If pip fails, update it with 'pip install --upgrade pip'.

2. **Set Up Node.js Environment**:
   cd /Users/Worker/Desktop/magic_munchkin_battle
   npm init -y
   npm install express socket.io socket.io-client
   # Troubleshooting: If npm init fails, manually create package.json with '{"name": "magic-munchkin-battle", "version": "1.0.0"}'. If install fails, clear cache with 'npm cache clean --force'.

3. **Install External Tools**:
   - Download and install **Python 3.8+** from https://www.python.org/downloads/.
   - Download and install **Node.js 14+** from https://nodejs.org/en/download/.
   - (Optional) Install **Git** from https://git-scm.com/downloads or via Homebrew:
     /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
     brew install git
   - (Optional) Install **Arduino IDE** from https://www.arduino.cc/en/software.
   # Troubleshooting: If downloads fail, use a browser or VPN if behind a firewall.

4. **Prepare Hardware**:
   - Connect a webcam to your Mac.
   - (Optional) Connect an Arduino to /dev/cu.usbmodem11101 or another port.
   # Troubleshooting: Test the webcam with a simple app (e.g., Photo Booth on macOS). For Arduino, use the IDE’s Serial Monitor to confirm connection.

5. **Generate ArUco Markers and Assets**:
   cd /Users/Worker/Desktop/magic_munchkin_battle
   mkdir -p data
   python -c "import cv2; aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_100); [cv2.imwrite(f'data/marker_{i}.png', cv2.aruco.drawMarker(aruco_dict, i, 200)) for i in range(52)]"
   python -c "import cv2, numpy as np; [cv2.imwrite(f'data/{n}.png', np.zeros((50, 50, 3), dtype=np.uint8) + 100) for n in ['finn', 'jake', 'marceline', 'flame_princess', 'ice_king', 'princess_bubblegum', 'bmo', 'lumpy_space_princess', 'banana_guard', 'female_jake', 'gunther', 'female_finn', 'tree_trunks', 'goblin', 'slime', 'troll', 'ogre', 'ooo_background', 'card_back', 'bin']]"
   touch data/boing.wav data/pow.wav data/cheer.wav data/discard.wav data/background_music.mp3
   # Troubleshooting: If scripts error, ensure cv2 and numpy are installed. Check disk space with 'df -h' and folder permissions.

6. **Add Info.plist (macOS)**:
   mkdir -p /Users/Worker/Desktop/magic_munchkin_battle/.venv/bin
   cat > /Users/Worker/Desktop/magic_munchkin_battle/.venv/bin/Info.plist << EOF
   <?xml version="1.0" encoding="UTF-8"?>
   <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
   <plist version="1.0">
   <dict>
       <key>NSCameraUseContinuityCameraDeviceType</key>
       <string>YES</string>
   </dict>
   </plist>
   EOF
   # Troubleshooting: If creation fails, verify write access to .venv/bin with 'ls -ld .venv/bin' and fix with 'chmod u+w .venv/bin'.

7. **Run the Project**:
   - Start the Python server: python combined_server.py  # Runs on port 5000
   - Start the Node.js server: node server.js  # Runs on port 5001
   - Access the game:
     - Player view: http://10.228.236.43:5001
     - Spectator view: http://10.228.236.43:5001/watch
   - After playing, run python ml_model.py to analyze game data.
   # Troubleshooting: If servers don’t start, check port availability with 'lsof -i :5000' or ':5001' and kill conflicting processes (e.g., 'kill -9 <PID>'). If the game page doesn’t load, verify the IP and ensure both servers are running.

# --- Additional Considerations ---
# These are tips and potential issues I encountered that others should watch out for.
# Read these to avoid common pitfalls!

# Network and IP: I hardcoded 10.228.236.43, so if your network changes, update it in all files or use a config file (e.g., config.py with SERVER_IP = "0.0.0.0").
# Troubleshooting: Test your IP with 'ifconfig' (macOS/Linux) or 'ipconfig' (Windows) and update if needed.

# Camera: I found camera issues on macOS; ensure permissions are granted.
# Troubleshooting: If no feed, grant permissions, restart the terminal, or test with 'python -c "import cv2; print(cv2.VideoCapture(0).isOpened())"' (should return True).

# Arduino Ports: I used /dev/cu.usbmodem11101, but adjust for Linux/Windows (e.g., /dev/ttyUSB0, COM3).
# Troubleshooting: List ports with 'ls /dev/cu*' (macOS) or 'ls /dev/ttyUSB*' (Linux) and update combined_server.py if needed.

# Assets: Placeholders work, but real assets need proper sizing (e.g., 80x80) and licensing.
# Troubleshooting: If assets don’t display, verify file names match exactly and formats are supported (PNG for images, WAV/MP3 for sounds).

# Documentation: I should add a README.md with these steps—consider it a future task!
# Suggestion: Use this file as a base for README.md to save time.

# Testing: I haven’t added tests yet; adding unittest or pytest would be smart.
# Suggestion: Start with testing combined_server.py’s track_cards function.

# ML Analysis: I need sufficient data in game_data.csv—enhance logging in combined_server.py for more details (e.g., selected characters).
# Troubleshooting: If ml_model.py fails, ensure game_data.csv has data and check for syntax errors.

# --- Verification ---
# After setup, I recommend verifying everything to catch issues early:
source /Users/Worker/Desktop/magic_munchkin_battle/.venv/bin/activate
pip list  # Should show opencv-python, numpy, flask-socketio, eventlet, pyserial, pynput, pandas, scikit-learn
cd /Users/Worker/Desktop/magic_munchkin_battle
npm list  # Should show express, socket.io, socket.io-client
ls data   # Should show marker_*.png, *.png, *.wav, *.mp3
# Troubleshooting: If any command fails, re-run the installation steps and check error messages.

# That’s it! I hope this guide makes it easy for others to join the Magic Munchkin Battle fun. Let me know if you hit snags!

 

Week 11 – Production assignment + Preliminary concept – David & Ali noor

INTRODUCTION:

This assignment had us learn and implement serial communication between Arduino and P5.js. The conversation could be one direction or bi-directional. The script and additional web-based API, provided by NYU’s I.M faculty was used to implement concept, which utilized Arduino and P5.js.

SCHEMATIC AND WIRING:

The sketch for the wiring and connections.

Schematic for the Arduino connection and port mapping

For this assignment, potentiometer was used for adjusting the vertical affects of the wind. The FSR – Force Sensitive Resistor was used to move the ball horizontally. Depending on the pressure, you would have rightward movement. For the LED in the middle, that was used to indicate impact with the surface. Hence would light up whenever the ball would bounce. As for the right most LED, it would light up, when the button on P5.JS was triggered, i.e increase the brightness. The port-mapping and pin assignment are visible on both the sketch, as well as the schematic.

P5 PROJECT AND CODE:

let fsrVal = 0;
let potVal = 0;
let led1Brightness = 0; 
let bounceFlag = 0;
let position, velocity, acceleration, gravity, wind;
let drag = 0.99;
let mass = 50;
let prevBounce = false;
let button;
function setup() {
  createCanvas(640, 360);
  position = createVector(width / 2, 0);
  velocity = createVector(0, 0);
  acceleration = createVector(0, 0);
  gravity = createVector(0, 0.5 * mass);
  wind = createVector(0, 0);
  button = createButton('Increase Brightness');
  button.position(10, height + 10);
  button.mousePressed(() => {
    led1Brightness = (led1Brightness + 200) % 256;
  });
}
function draw() {
  background(255);
  // ========== Task 1: move ellipse horizontally using FSR only ==========
  let xMapped = map(fsrVal, 0, 1023, 50, width - 50);
  position.x = xMapped;
  // ========== Task 3: adjust gravity based on potentiometer ==========
  let gravityScale = map(potVal, 0, 1023, 0.1, 0.5);
  gravity.y = gravityScale * mass;
  applyForce(wind);
  applyForce(gravity);
  velocity.add(acceleration);
  velocity.mult(drag);
  position.add(velocity);
  acceleration.mult(0);
  ellipse(position.x, position.y, mass, mass);
  // ========== Bounce Logic ==========
  let isBouncing = false;
  if (position.y > height - mass / 2) {
    velocity.y *= -0.9;
    position.y = height - mass / 2;
    isBouncing = true;
  }
  // Detect rising edge (bounce just occurred)
  bounceFlag = isBouncing && !prevBounce ? 1 : 0;
  prevBounce = isBouncing;
  // ========== Serial send ==========
  if (serialActive) {
    let sendToArduino = led1Brightness + "," + bounceFlag + "\n";
    writeSerial(sendToArduino);
  }
  // Debugging values
  fill(0);
  text(`FSR: ${fsrVal}`, 10, 20);
  text(`Pot: ${potVal}`, 10, 40);
  text(`LED Brightness: ${led1Brightness}`, 10, 60);
}
function applyForce(force) {
  let f = p5.Vector.div(force, mass);
  acceleration.add(f);
}
function keyPressed() {
  if (key == " ") {
    setUpSerial();
  }
  
   if (key === 'l' || key === 'L') {
    velocity.y = -10; // Negative Y velocity = upward bounce
  }
}
// Serial reading
function readSerial(data) {
  if (data != null) {
    let fromArduino = split(trim(data), ",");
    if (fromArduino.length === 2) {
      fsrVal = int(fromArduino[0]);
      potVal = int(fromArduino[1]);
    }
  }
}
/*
int fsrPin = A0;        // Task 1 - Move ellipse
int led1Pin = 3;        // Task 2 - LED controlled by p5.js button (PWM)
int potPin = A2;        // Task 3 - Gravity control
int led2Pin = 5;        // Task 3 - Blink on bounce (PWM)
int led1Brightness = 0;
bool bounceFlag = false;
void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(led1Pin, OUTPUT);
  pinMode(led2Pin, OUTPUT);
  // Initial blink
  digitalWrite(led1Pin, HIGH);
  digitalWrite(led2Pin, HIGH);
  delay(200);
  digitalWrite(led1Pin, LOW);
  digitalWrite(led2Pin, LOW);
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("0,0");
    delay(300);
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}
void loop() {
  while (Serial.available()) {
    digitalWrite(LED_BUILTIN, HIGH);
    int led1Val = Serial.parseInt();       // brightness from p5.js (0–255)
    int bounce = Serial.parseInt();        // whether ball bounced (1 or 0)
    if (Serial.read() == '\n') {
      analogWrite(led1Pin, led1Val);
      if (bounce == 1) {
        digitalWrite(led2Pin, HIGH);
        delay(50);
        digitalWrite(led2Pin, LOW);
      }
      int fsrVal = analogRead(fsrPin);
      int potVal = analogRead(potPin);
      Serial.print(fsrVal);
      Serial.print(",");
      Serial.println(potVal);
    }
  }
  digitalWrite(LED_BUILTIN, LOW);
}
*/

ARDUINO CODE:

int fsrPin = A0;        // Task 1 - Move ellipse
int led1Pin = 3;        // Task 2 - LED controlled by p5.js button (PWM)
int potPin = A2;        // Task 3 - Gravity control
int led2Pin = 5;        // Task 3 - Blink on bounce (PWM)
int led1Brightness = 0;
bool bounceFlag = false;
void setup() {
  Serial.begin(9600);
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(led1Pin, OUTPUT);
  pinMode(led2Pin, OUTPUT);
  // Initial blink
  digitalWrite(led1Pin, HIGH);
  digitalWrite(led2Pin, HIGH);
  delay(200);
  digitalWrite(led1Pin, LOW);
  digitalWrite(led2Pin, LOW);
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("0,0");
    delay(300);
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}
void loop() {
  while (Serial.available()) {
    digitalWrite(LED_BUILTIN, HIGH);
    int led1Val = Serial.parseInt();       // brightness from p5.js (0–255)
    int bounce = Serial.parseInt();        // whether ball bounced (1 or 0)
    if (Serial.read() == '\n') {
      analogWrite(led1Pin, led1Val);
      if (bounce == 1) {
        digitalWrite(led2Pin, HIGH);
        delay(50);
        digitalWrite(led2Pin, LOW);
      }
      int fsrVal = analogRead(fsrPin);
      int potVal = analogRead(potPin);
      Serial.print(fsrVal);
      Serial.print(",");
      Serial.println(potVal);
    }
  }
  digitalWrite(LED_BUILTIN, LOW);
}

DEMO VIDEO:

Video Demo

In the video, the FSR is used to move the ball on the X-axis i.e task 1. The potentiometer value, which is set to zero, is tuned up. When it increases, the vertical wind pressure makes the bounce and rebound lower. Nonetheless, whenever it bounces, the red LED in the middle lights up i.e task 3. Using P5.js, the brightness can be adjusted, which is demonstrated and visualized through the LED on the right-most side.

CHALLENGES:

Since I was in the hospital, couldn’t get to team up with anyone, missed out on the lecture, it was extremely hard to configure and understand the serial communication between P5.js and Arduino. Nonetheless, after going on and on, again and again over Professor Shiloh’s notes, I finally got it to work.

The tutorials online had a software that used node.js for the communication. A misdirection which costed me my time and energy. Took forever to debug. Since my classmates were busy and I had everything piled up, getting it done on time was hard. Nonetheless, managed to do so. As for the other challenges, the LED light for increase in Brightness was initially green. It wouldn’t increase in brightness significantly, which led me to suspect issue with the code. After much time spent, it was realized that that LED despite working, for some-reason didn’t respond to the PWM signals as accurately as the RED one. So, it was swapped with the RED LED.

Preliminary Concept for the final Project

The concept for the final project kept on varying from a robotic soccer leg ( discussed with Professor Aya) to a prosthetic hand, which was discussed in detail with Professor Shiloh. The final project as of now remains unclear. I do want to make something crazy and go all out. Currently I think of something that involves me reusing and crafting from scratch by reusing already available materials (hacking). Hence, something which fits a perfect balance between hardware, and soft-logic. Something that makes use of all of the concepts (or most of them), which were learnt inside the class.

 

Week 11- Reading

My Deep Dive into Design Meets Disability

Graham Pullin’s Design Meets Disability (2009) is a bold wake-up call that reframes how we approach assistive technology. It opens with the leg splint, a niche product that, instead of following the usual “trickle-down” tech pipeline, shows how designs for smaller communities can reshape mainstream innovation. Pullin exposes a thorny issue: aids like prosthetics, hearing aids, or wheelchairs are often engineered to fade into the background, as if their visibility signals weakness or shame. This tug-of-war between standing out and blending in—presentation versus concealment—is a design dilemma that’s as philosophical as it is practical. He points to eyewear as a success story: once a clunky medical necessity, glasses have become a cultural staple, from Ray-Ban’s chic frames to Warby Parker’s trendy designs. Yet, the NHS’s preference for transparent, “discreet” frames betrays a lingering bias toward invisibility. Then there’s HearWear, a hearing aid concept that dares to be seen, embracing style to challenge stigma. These examples underline Pullin’s thesis: design isn’t a sidekick—it’s the soul of a product’s purpose and perception.

Pullin argues that assistive devices, especially prosthetics, demand a unique design approach. They’re not just tools but extensions of the body, requiring a delicate balance of function, comfort, and aesthetic appeal. A prosthetic leg needs to support weight and movement while looking and feeling like it belongs to its wearer. Yet, the designers who pull this off—blending biomechanics with artistry—are rarely celebrated. Compare that to the iPod, which swept awards in the early 2000s for its minimalist design and pocket-sized revolution. The iPod’s sleek curves and intuitive click wheel made it a cultural icon, but why don’t we see prosthetic designers or hearing aid innovators on the same pedestal? It feels like a double standard, and I’d argue it stems from society’s discomfort with disability itself. We’re quick to praise tech that’s “cool” but hesitant to spotlight tools that confront our biases about ability.

This reading resonates with ideas from interaction design, like those in Bret Victor’s A Brief Rant on the Future of Interaction Design, where aesthetics can make complex systems feel intuitive. But there’s a dark side: chasing beauty can undermine utility. Pullin’s reference to decorative teapots—gorgeous but impractical—drives this home. I’m convinced that assistive tech needs to ditch the cloak of concealment and embrace bold, visible designs. Imagine hearing aids as vibrant as Beats headphones or prosthetic arms with customizable, 3D-printed patterns that reflect personality. These could normalize disability, shifting public perception from pity to respect. Why hide a hearing aid when it could be a conversation starter, signaling confidence? This approach could dismantle stigma, making assistive tech a symbol of empowerment rather than a whispered necessity. But it’s not just about looks—design must amplify function. A hearing aid that’s a fashion statement but muffles sound is a failure. Engineering and aesthetics need to be in lockstep, like a perfectly tuned engine.

That said, I’m wary of design tipping into excess. In the rush to make assistive tech trendy, we risk gimmicks—think prosthetics with neon logos or hearing aids with flashy, battery-draining LEDs. These might grab headlines but could alienate users who need reliability over Instagram appeal. Balance is critical: a prosthetic should feel like an extension of self, not a costume piece. In 2025, we’re seeing Pullin’s ideas take shape. Companies like Open Bionics offer bionic arms with swappable, stylish covers, while Cochlear’s latest hearing aids integrate Bluetooth for seamless phone calls, blending utility with modern flair. These advancements are exciting, but they’re not universal—high costs and limited access mean many users are stuck with outdated, “invisible” aids. This gap fuels my skepticism about the industry’s priorities; too often, cutting-edge designs cater to the wealthy or well-connected, leaving others behind.

Pullin’s work also sparks a broader question: who gets to define “normal”? The push to camouflage aids assumes disability needs to be erased to fit in, but I’d argue that’s backward. Bold design can challenge that norm, making society adapt to diverse bodies rather than forcing users to conform. Take Aimee Mullins, the Paralympian and model who’s rocked prosthetic legs as fashion statements—her Cheetah blades and carved wooden legs are art, function, and defiance rolled into one. She’s proof that design can shift culture, but it takes courage to prioritize visibility over conformity.

This book left me energized but frustrated. Pullin’s vision is a blueprint for a world where assistive tech is celebrated, not hidden, but we’re not there yet. Accessibility, affordability, and cultural acceptance lag behind the prototypes. I’m hopeful, though—advances like AI-driven prosthetics that learn user movements or hearing aids with real-time language translation show what’s possible when design and engineering sync up. Design Meets Disability is a rallying cry to keep pushing, not just for better tools but for a society that sees disability as part of human diversity, not a flaw to erase. It’s got me thinking about how every product, from a phone to a wheelchair, could be designed with this kind of intention—and that’s a future worth building.

Week 10 Reading

My Reflection on “A Brief Rant on the Future of Interaction Design”

Bret Victor’s A Brief Rant on the Future of Interaction Design (2011) is a wake-up call that cuts through the haze of futuristic hype. The Microsoft Productivity Vision video it references—full of glossy, swipe-heavy interfaces—had me sold on a seamless tomorrow. But Victor dismantles that fantasy with a single, grounded truth: our hands, with their dexterity and sensitivity, are the ultimate interface. His analogy of a hammer as a simple, perfect tool drives it home: tech should amplify what our hands do best, not replace it with sterile gestures. The concept of “tactile richness” isn’t just clever—it’s a challenge to rethink what makes interaction meaningful.

I’m with Victor when he calls out the tech industry’s obsession with slapping “game-changer” labels on recycled ideas. Every new device launch feels like a tired sequel—same plot, flashier effects. But I part ways with him on his skepticism about non-tactile interfaces. Victor seems to dismiss the potential of tech that doesn’t involve direct touch, but in 2025, we’re seeing breakthroughs that beg to differ. Take haptic feedback systems that mimic texture or spatial computing devices like Apple’s Vision Pro, which blend gesture, gaze, and voice into fluid interactions. These aren’t just incremental; they’re redefining how we engage with digital spaces. I’d argue they deliver a kind of richness Victor overlooks, one that’s less about physical grip and more about intuitive flow.

That said, Victor’s point about marketing glossing over real progress stings because it’s true. Too many products prioritize sizzle over substance, leaving us with tools that look futuristic but feel hollow. A sleek touchscreen isn’t inherently better than a well-worn keyboard if it doesn’t connect us to the task.

The follow-up piece, Responses to Some Rants, is Victor doubling down, and it’s a gem. He anticipates pushback—like my own about gestures—and counters with a deep dive into why our fingertips’ nerve density matters for cognitive engagement. It’s a compelling angle, rooted in biology, but I think it’s only half the story. Interaction design isn’t just about what feels good in the hand; it’s about what clicks in the mind. For someone who’s never held a pen, a digital stylus is a revelation. I remember my first encounter with a Wacom tablet—coming from a mouse, it felt like unlocking a superpower. Meanwhile, my cousin, raised on iPads, shrugs at styluses but loses her mind over VR hand-tracking. Progress is personal, shaped by what we’ve known and what we dream of.

Since Victor wrote this, the world’s moved fast. Neural interfaces, like Neuralink’s early trials, are starting to bypass physical input entirely, while projects like OpenAI’s multimodal AI (think ChatGPT’s vision capabilities) let us interact through natural language and images. These feel like steps toward a future Victor might not have imagined, where “tactile” expands beyond the physical. Still, his rant holds up as a gut-check: are we building tools that truly enhance human capability, or just chasing the next shiny thing? This piece left me wrestling with that question, and I’m eager to see how the answer evolves.

Week 10 – Production David & Ali noor

Musical Instrument

Introduction:

An instrument is something that helps us humans either measure the data or produce it. For this assignment, we were supposed to build a musical instrument in a group of two. Given my health challenges, I was unable to team up with anyone, and decided to pursue it on my own. I started off by asking the most fundamental question – “What is a musical instrument ? ” Something that plays sound when triggered? Or something that plays sound on its own? What kind of sound? How many sounds? It is when after pondering on the philosophy of a musical device, I questioned my surroundings. I don’t happen to have a dorm mate, and very less likely do I get to socialize with others around the campus. Sometimes the thoughts themselves get too loud that I start second guessing my self. This is where the eureka moment came in! A musical device that talks to me – interacts with my as if a roommate for instance would have done. To start off with basics, a greeting or a salutation would have had sufficed as well. There and then the idea of ‘Welcominator’ was born!

Concept and design:

After having decided upon what to make and accomplish, I got down to work. The basic logic of ‘Welcominator’ would involve use of a trigger mechanism which would recognize the moment I settle inside my room, and a speaker, alongside a digital and an analogue switch to trigger and adjust response.

For the digital sensor / switch I decided to use the FSR sensor (Force Sensing Resistor). This sensor reduces the resistance with greater pressure applied, and by logic qualifies for the analogue sensor. However, the basic concept to this instrument was me putting down my items such as my watch as soon as I enter and settle down inside my dorm. Thus, two FSR sensors were used for greater surface area, and the value read by these sensors was read using digitalRead. Therefore, acting as a switch, the value of 1 or 0 was only read.

As for the analogue sensor, the potentiometer was used. The potentiometer in this case was not used to adjust the volume, but rather to choose between the audio tracks. The code written would select which sound to play depending on the current fed in by the wiper toward the A0 pin on the Arduino. The schematic and sketch below show the connection and logical mapping of the ciruit:

 

The buzzer or the speaker takes voltage signals from pin 9, whilst the digitalRead performed on FSR sensor is sent to pin A1 and A2 respective of the sensor.  It is an active buzzer and hence can play different sound tunes.

//crucial file added to understand how pitches and notes work
#include "pitches.h"
#define BUZZER_PIN 9
#define POT_PIN A0
#define SENSOR1_PIN A1
#define SENSOR2_PIN A2
bool isPlaying = false;
// godfather
int godfather_melody[] = {
  NOTE_E4, NOTE_A4, NOTE_C5, NOTE_B4, NOTE_A4, NOTE_C5, NOTE_A4, NOTE_B4, NOTE_A4, NOTE_F4, NOTE_G4,
  NOTE_E4, NOTE_E4, NOTE_A4, NOTE_C5,
  NOTE_B4, NOTE_A4, NOTE_C5, NOTE_A4, NOTE_C5, NOTE_A4, NOTE_E4, NOTE_DS4,
  NOTE_D4, NOTE_D4, NOTE_F4, NOTE_GS4,
  NOTE_B4, NOTE_D4, NOTE_F4, NOTE_GS4,
  NOTE_A4, NOTE_C4, NOTE_C4, NOTE_G4,
  NOTE_F4, NOTE_E4, NOTE_G4, NOTE_F4, NOTE_F4, NOTE_E4, NOTE_E4, NOTE_GS4,
  NOTE_A4
};
int godfather_durations[] = {
  8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
  2, 8, 8, 8,
  8, 8, 8, 8, 8, 8, 8, 8,
  2, 8, 8, 8,
  2, 8, 8, 8,
  2, 8, 8, 8,
  8, 8, 8, 8, 8, 8, 8, 8,
  2
};
// nokia tune
int nokia_melody[] = {
  NOTE_E5, NOTE_D5, NOTE_FS4, NOTE_GS4, 
  NOTE_CS5, NOTE_B4, NOTE_D4, NOTE_E4,
  NOTE_B4, NOTE_A4, NOTE_CS4, NOTE_E4,
  NOTE_A4
};
int nokia_durations[] = {
  8, 8, 8, 8,
  8, 8, 8, 8,
  8, 8, 8, 8,
  8
};
void setup() {
  pinMode(BUZZER_PIN, OUTPUT);
  pinMode(SENSOR1_PIN, INPUT);
  pinMode(SENSOR2_PIN, INPUT);
  pinMode(POT_PIN, INPUT);
}
void loop() {
  int potValue = analogRead(POT_PIN);
  bool useGodfather = potValue < 512;  // Left side (low) pot = Godfather, Right (high) = Nokia
  int sensor1Value = digitalRead(SENSOR1_PIN);
  int sensor2Value = digitalRead(SENSOR2_PIN);
  bool sensorTriggered1 = sensor1Value == HIGH;
  bool sensorTriggered2 = sensor2Value == HIGH;
  if ((sensorTriggered1 || sensorTriggered2) && !isPlaying) { // checks if no music is playing and either of the sensor trigger is recorded for
    isPlaying = true;
    if (useGodfather) {
      playMelody(godfather_melody, godfather_durations, sizeof(godfather_melody) / sizeof(int));
    } else {
      playMelody(nokia_melody, nokia_durations, sizeof(nokia_melody) / sizeof(int));
    }
    isPlaying = false;
  }
}
//function for playing melody
void playMelody(int melody[], int durations[], int length) {
  for (int i = 0; i < length; i++) {
    int noteDuration = 1000 / durations[i];
    tone(BUZZER_PIN, melody[i], noteDuration);
    delay(noteDuration * 1.2); // time duration added betnween notes to make it seem buttery smooth.
    noTone(BUZZER_PIN);
  }
}
// pitches.h
#define REST 0
#define NOTE_B0 31
#define NOTE_C1 33
#define NOTE_CS1 35
#define NOTE_D1 37
#define NOTE_DS1 39
#define NOTE_E1 41
#define NOTE_F1 44
#define NOTE_FS1 46
#define NOTE_G1 49
#define NOTE_GS1 52
#define NOTE_A1 55
#define NOTE_AS1 58
#define NOTE_B1 62
#define NOTE_C2 65
#define NOTE_CS2 69
#define NOTE_D2 73
#define NOTE_DS2 78
#define NOTE_E2 82
#define NOTE_F2 87
#define NOTE_FS2 93
#define NOTE_G2 98
#define NOTE_GS2 104
#define NOTE_A2 110
#define NOTE_AS2 117
#define NOTE_B2 123
#define NOTE_C3 131
#define NOTE_CS3 139
#define NOTE_D3 147
#define NOTE_DS3 156
#define NOTE_E3 165
#define NOTE_F3 175
#define NOTE_FS3 185
#define NOTE_G3 196
#define NOTE_GS3 208
#define NOTE_A3 220
#define NOTE_AS3 233
#define NOTE_B3 247
#define NOTE_C4 262
#define NOTE_CS4 277
#define NOTE_D4 294
#define NOTE_DS4 311
#define NOTE_E4 330
#define NOTE_F4 349
#define NOTE_FS4 370
#define NOTE_G4 392
#define NOTE_GS4 415
#define NOTE_A4 440
#define NOTE_AS4 466
#define NOTE_B4 494
#define NOTE_C5 523
#define NOTE_CS5 554
#define NOTE_D5 587
#define NOTE_DS5 622
#define NOTE_E5 659
#define NOTE_F5 698
#define NOTE_FS5 740
#define NOTE_G5 784
#define NOTE_GS5 831
#define NOTE_A5 880
#define NOTE_AS5 932
#define NOTE_B5 988
#define NOTE_C6 1047
#define NOTE_CS6 1109
#define NOTE_D6 1175
#define NOTE_DS6 1245
#define NOTE_E6 1319
#define NOTE_F6 1397
#define NOTE_FS6 1480
#define NOTE_G6 1568
#define NOTE_GS6 1661
#define NOTE_A6 1760
#define NOTE_AS6 1865
#define NOTE_B6 1976
#define NOTE_C7 2093
#define NOTE_CS7 2217
#define NOTE_D7 2349
#define NOTE_DS7 2489
#define NOTE_E7 2637
#define NOTE_F7 2794
#define NOTE_FS7 2960
#define NOTE_G7 3136
#define NOTE_GS7 3322
#define NOTE_A7 3520
#define NOTE_AS7 3729
#define NOTE_B7 3951

The code above addresses the logic and the pitches.h is another file used for defining and storing the notes that are used by our program. Code for the pitches and the sounds for ‘Godfather theme’ and ‘Nokia ‘ tune were taken from Arduino Project HUB website .

Both FSR sensor trigger HIGH or LOW value, and if potentiometer is registering lower voltage, then Godfather theme plays, and when it registers higher voltage, the Nokia tune plays. Once it ends, it sets the isPlaying state to false. This is to avoid interruption. Last but not the least, chatgpt was used to order pitches.h file as coding it myself would have been impossible.

 

Challenges:

One and the only challenge I faced was the FSR registering high, even when there was no pressure being applied. This led me to do some researching. Turns out that sometimes inaccuracy and condition of the sensor renders false positives. Hence, I used a resistor connection with the sensor and ground to get rid of the  tingling current, and register high only when a solid threshold was provided. An led was attached before the buzzer, for visual aesthetics and for indication that current was going through the buzzer. Initially the buzzer would work as well. This led me to code and find the bug in wrong pin assignment

Demo:

Video Player

Future Revisions:

For future revision I intend to add multiple songs, FSR sensors, and an array of leds which can mimic the sound pattern.

Final Project: Repeat After Me

Finally, we reached the end of the semester, and with it came the submission of the final project. I had decided to make a Simon-Says style game, using lights and buzzers to interact with the user and test their recall skills. An interesting thing I’ve found throughout this course that I seem to really enjoy memory-style games, as with my midterm, and now this.

Like with my midterm, I began by creating a prototype to make sure I got the basic features down before I integrated any fancy features or graphics. Looking back on it now, the initial version worked, but it just looked, in simple terms, boring.

 

The initial gameplay didn’t feel like anything I would be excited to play at all.

The initial wiring setup didn’t inspire much confidence either.

But that’s the great part of a prototype. It didn’t need to look good, so long as it functioned well. I was able to nail down the game states with the prototype, then I began working on graphics.

I wanted to go for a retro style with my game, and so I tried to make the backgrounds and the board match a cohesive neon-arcade-esque vibe.

In the end, we arrived at the final submission. I ended up soldering many of the wires inside my box to hide them as much as possible, because attractive things work better (reading reference!).

After carrying out user-testing, I ended up integrating more features within my code, including an instructions screen, and more interactivity between the buttons (a shorter debounce delay, the buttons lighting up, etc).

And here we are. With the final submission! I had  both  a great  and  frustrating  experience  making  it,  but I’m  really  glad  with the  final result.

Schematic of my circuit (though I used arcade buttons)

// establishing variables
const int buttonPins[] = {2, 3, 4, 5}; // Yellow, green, blue, and red
const int ledPins[] = {8, 9, 10, 11}; // Yellow, green, blue, and red
const int buzzerPin = 6; // Buzzer set on pin 6

bool ledBlinking = false; // Checks whether the LEDs are blinking or not
unsigned long lastBlinkTime = 0; // Tracks when the LEDs were last blinked
bool blinkState = false; // Toggles between on and off when the LEDs are blinking

void setup() {
  // Setting up serial communication
  Serial.begin(9600); // Buad rate of 9600
  for (int i = 0; i < 4; i++) {
    // Setting the pin modes for the buttons, LEDs, and buzzer
    pinMode(buttonPins[i], INPUT_PULLUP);
    pinMode(ledPins[i], OUTPUT);
  }
  pinMode(buzzerPin, OUTPUT);
}

void loop() {
  // Handle blinking mode
  if (ledBlinking && millis() - lastBlinkTime > 500) { 
    blinkState = !blinkState; // Alternates between the LED being on and off every 500ms
    for (int i = 0; i < 4; i++) {
      if (blinkState) {
        digitalWrite(ledPins[i], HIGH); // Turn the LED on if blinkState is true
        } 
      else {
        digitalWrite(ledPins[i], LOW); // Turn the LED off if blinkState is false
        }
       }
    lastBlinkTime = millis();
  }

  // Check button presses
  for (int i = 0; i < 4; i++) {
    if (digitalRead(buttonPins[i]) == LOW) {
      Serial.println(buttonPins[i]); // Send button pin number to p5
      delay(100); // Debounce delay
    }
  }

  // Handle serial input from p5
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    command.trim();

    if (command.startsWith("ALL")) { // if the p5 command sends "ALL", all LEDs must be on
      int mode = command.substring(3).toInt();
      handleAllLEDs(mode);
    } 
    else if (command == "WRONG") { // if the p5 command sends "WRONG", play the sound
      tone(buzzerPin, 100, 500); // Wrong answer sound
    } 
    else {
      int pin = command.toInt(); // lights up the corresponding LED and plays the sound
      if (pin >= 8 && pin <= 11) {
        playColorFeedback(pin);
      }
    }
  }
}

// Turns on the LED corresponding to the button, and plays the sound
void playColorFeedback(int pin) {
  digitalWrite(pin, HIGH);
  playToneForPin(pin);
  delay(300);
  digitalWrite(pin, LOW);
  noTone(buzzerPin);
}

// Plays a specific tone based on the button pressed
void playToneForPin(int pin) {
  switch (pin) {
    case 8: tone(buzzerPin, 262); break; // Yellow is C4
    case 9: tone(buzzerPin, 330); break; // Green is E4
    case 10: tone(buzzerPin, 392); break; // Blue is G4
    case 11: tone(buzzerPin, 523); break; // Red is C5
  }
}

void handleAllLEDs(int mode) {
  ledBlinking = false;
  for (int i = 0; i < 4; i++) {
    digitalWrite(ledPins[i], LOW); // the LEDs are off
  }

  if (mode == 1) {
    for (int i = 0; i < 4; i++) {
      digitalWrite(ledPins[i], HIGH); // if the mode is 1, it turns on all the LEDs
    }
  } else if (mode == 2) { // if the mode is 2, it blinks the LEDs
    ledBlinking = true;
    lastBlinkTime = millis();
  }
}

My Arduino Code

Link to the full screen version

Thanks for a great semester!

Final Project Documentation

Concept

The Smart House System is an interactive physical computing project that simulates features of an intelligent home using Arduino UNO and p5.js. The system includes:

  • A smart parking assistant that detects cars entering and exiting, and updates available parking slots automatically.

  • A light automation system that turns on indoor lights when it gets dark, based on ambient light readings.

  • A real-time dashboard and voice announcer, implemented in p5.js, that visualizes the system state and speaks updates aloud using p5.speech.

This system provides a fun and engaging way to demonstrate real-world home automation, combining sensors, outputs, and visual/voice feedback for user interaction.

Interaction Demo

IMG_9078

How the Implementation Works

The system uses ultrasonic distance sensors to detect when a vehicle is near the entry or exit of the parking area. A servo motor simulates the gate that opens when a car arrives and parking is available.

A photoresistor (LDR) detects light levels to automatically turn on five LEDs that simulate indoor lighting when it gets dark.

All event messages from Arduino are sent to a p5.js sketch over web serial. The browser-based sketch then:

  • Displays the parking status

  • Shows light status

  • Uses p5.speech to speak real-time messages like “Parking is full!” or “Lights are now on!”

Interaction Design

The project is designed for simple, touchless interaction using real-world analog sensors:

  • Bringing your hand or an object close to the entry sensor simulates a car arriving. If space is available, the gate opens, the slot count is reduced, and a voice announces the update.

  • Moving your hand in front of the exit sensor simulates a car leaving, increasing the parking availability.

  • Covering the LDR sensor simulates nighttime — lights automatically turn on, and the system announces it.

  • The p5.js dashboard shows real-time status and acts as an interactive voice feedback system.

Arduino Code

The Arduino UNO is responsible for:

  • Reading two ultrasonic sensors for car entry/exit

  • Reading the photoresistor (LDR) for light level

  • Controlling a servo motor for the gate

  • Controlling 5 indoor LEDs

  • Sending status messages to the p5.js sketch over serial

Code Overview:

  • Starts with 3 available parking slots

  • Gate opens and slot count decreases when a car is detected at entry

  • Slot count increases when a car exits

  • Indoor lights turn on when light level drops below a threshold

  • Sends messages like car_entry, car_exit, parking_full, lights_on, lights_off, and parking_spots:X

#include <Servo.h>

// Ultrasonic sensor pins
#define trigEntry 2
#define echoEntry 3
#define trigExit 4
#define echoExit 5

// Servo motor pin
#define servoPin 6

// LED pins
int ledPins[] = {7, 8, 9, 10, 11};

// Light sensor pin
#define lightSensor A0

Servo gateServo;
int Slot = 3; // Initial parking spots

void setup() {
  Serial.begin(9600);

  // Ultrasonic sensors
  pinMode(trigEntry, OUTPUT);
  pinMode(echoEntry, INPUT);
  pinMode(trigExit, OUTPUT);
  pinMode(echoExit, INPUT);

  // LED pins
  for (int i = 0; i < 5; i++) {
    pinMode(ledPins[i], OUTPUT);
  }

  // LDR analog input
  pinMode(lightSensor, INPUT);

  // Servo
  gateServo.attach(servoPin);
  gateServo.write(100); // Gate closed
}

void loop() {
  int entryDistance = getDistance(trigEntry, echoEntry);
  int exitDistance  = getDistance(trigExit, echoExit);
  int lightValue    = analogRead(lightSensor); // 0 (dark) to 1023 (bright)

  Serial.print("Entry: "); Serial.print(entryDistance);
  Serial.print(" | Exit: "); Serial.print(exitDistance);
  Serial.print(" | Light: "); Serial.print(lightValue);
  Serial.print(" | Slots: "); Serial.println(Slot);

  // ===== Car Entry Logic =====
  if (entryDistance < 10 && Slot > 0) {
    openGate();
    Slot--;
    Serial.println("car_entry");
    Serial.print("parking_spots:");
    Serial.println(Slot);
    delay(2000);
    closeGate();
  }
  // ===== Parking Full Logic =====
  else if (entryDistance < 10 && Slot == 0) {
    Serial.println("parking_full");
    delay(1000); // Prevent spamming the message
  }

  // ===== Car Exit Logic =====
  if (exitDistance < 10 && Slot < 3) {
    openGate();
    Slot++;
    Serial.println("car_exit");
    Serial.print("parking_spots:");
    Serial.println(Slot);
    delay(2000);
    closeGate();
  }

  // ===== Light Control (5 LEDs) =====
  if (lightValue < 900) { // It's dark
    for (int i = 0; i < 5; i++) {
      digitalWrite(ledPins[i], HIGH);
    }
    Serial.println("lights_on");
  } else {
    for (int i = 0; i < 5; i++) {
      digitalWrite(ledPins[i], LOW);
    }
    Serial.println("lights_off");
  }

  delay(500);
}

// ===== Gate Functions =====
void openGate() {
  gateServo.write(0);
  delay(1000);
}

void closeGate() {
  gateServo.write(100);
  delay(1000);
}

// ===== Distance Sensor Function =====
int getDistance(int trigPin, int echoPin) {
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);
  long duration = pulseIn(echoPin, HIGH);
  int distance = duration * 0.034 / 2;
  return distance;
}

Circuit Schematic

p5.js Code and Dashboard

The p5.js sketch:

  • Uses the p5.webserial library to connect to Arduino

  • Uses p5.speech for voice announcements

  • Displays a dashboard showing the number of available parking slots

  • Shows the indoor light status using a colored circle

The voice announcements are fun and slightly humorous, e.g.:

“A wild car appears!”
“Uh-oh! Parking is full.”
“It’s getting spooky in here… turning the lights on!”

The sketch uses a say() wrapper function to safely trigger voice output in Chrome after the user clicks once.

Code Highlights:

  • Automatically resumes Chrome’s audio context

  • Waits for user interaction before enabling speech

  • Processes serial messages one line at a time

  • Provides a Connect/Disconnect button for user control

Arduino and p5.js Communication

The communication uses Web Serial API via p5.webserial:

  • Arduino sends messages like "car_entry\n", "lights_on\n", etc.

  • p5.js reads each line, processes it, updates the dashboard, and speaks it out loud

  • A connect button in the sketch allows users to select their Arduino port manually

  • All communication is unidirectional: Arduino → p5.js

What I’m Proud Of

  • Fully working sensor-triggered voice feedback via p5.js — makes the system feel alive

  • Smooth parking logic with entry and exit detection

  • Integration of multiple Arduino components (servo, LDR, LEDs, ultrasonic)

  • An intuitive UI that works both visually and with voice

  • Reliable browser-based connection using modern Web Serial

Areas for Future Improvement

  • Add a screen-based parking spot display (e.g., 7-segment or OLED)

  • Use non-blocking code in Arduino with millis() instead of delay()

  • Make a mobile-responsive version of the dashboard UI

  • Add a security camera feed or face detection in p5.js

  • Improve the servo animation to be smoother and time-synced

  • Add a buzzer or alert when parking is full

Final Project Documentation

Concept:
My final project is a bomb defusal game inspired by Keep Talking and Nobody Explodes. Just like in the original game, the player has to disarm several modules on the bomb in order to successfully defuse it. Currently it includes three types of modules: The first is Simon Says, using four LED arcade buttons. The second is an adaptation of cutting wires, where the player will have to either disconnect or rearrange the wires correctly. The last module requires the user to use a potentiometer as a tuning knob and try to hone in on the correct frequency. Once all three modules are disarmed, the bomb is defused and the game ends.Image, Video

 

Implementation:

Arduino:

  • Reads button presses, potentiometer values, and wire states.
  • Blinks arcade button LEDs to display the current Simon Says sequence, and activates the green LED on each module to indicate that they have been disarmed. 
  • Uses a piezo buzzer to audibly indicate how much time remains, which helps add tension.
  • The UNO and two breadboards are contained inside the cardboard shell, and the inputs are mounted on top.

p5.js:

  • Renders a representation of the physical bomb, including module status.
  • Displays the countdown timer since the LCD screen from the kit was not used.
  • Handles initializing each new round, including options for difficulty.
  • Randomly generates the solution for each module (e.g. color sequence for Simon Says, sweet spot for potentiometer).

 

Interaction Design:

The player is presented with three modules, which can be independently disarmed in any order. As the modules are disarmed, green status LEDs light up to indicate that the user has succeeded and can move on. Once all three modules are disarmed, p5.js will halt the timer and display a win screen. If the player fails to defuse the bomb in time, they will instead see a loss screen.

  • Simon Says: Flashes a sequence of colors on the arcade buttons, increasing in length with each successful input. If the player makes an incorrect input or fails to respond within a set amount of time, the sequence will repeat. The length of the sequence is determined by the difficulty selected.
  • Tuning: A value within the potentiometer’s viable range is randomly chosen. The player moves the knob, and once it comes within a certain range of the target value it begins a short countdown while displaying a progress bar. Both the current and target values are visualized using the sin function. The leniency range is also determined by difficulty.
  • Wires: The player must figure out the correct sequence to connect the four wires. They are not penalized for attempts in this adaptation, so they are free to use trial-and-error. A rendered visual helps guide them towards the correct configuration.

 

Schematic:

 

Arduino Code:

The Arduino code is fairly straightforward. It has a few functions used in the main loop to send/receive control messages, check the relevant inputs, and handle timing for the audiovisual components.

/*
Final Project (WIP)
By Matthias Kebede
*/





// // // Global Variables
const int valueCount = 9;
int active = 0;

// // Inputs
const int potPin = A4;
const int wirePins[4] = {A0, A1, A2, A3};
const int blueButtonIn = 6;
const int redButtonIn = 7;
const int yellowButtonIn = 8;
const int greenButtonIn = 9;

// // Input stuff
int inputs[valueCount] = {potPin, wirePins[0], wirePins[1], wirePins[2], wirePins[3], blueButtonIn, redButtonIn, yellowButtonIn, greenButtonIn};
int inputVals[valueCount] = {-1, -1, -1, -1, -1, -1, -1, -1, -1};
float smoothVals[5] = {0, 0, 0, 0, 0};
char headers[valueCount][13] = {
  {"TUNE:POT"}, {"WIRES:W1"}, {"WIRES:W2"}, {"WIRES:W3"}, {"WIRES:W4"},
  {"SIMON:BLUE"}, {"SIMON:RED"}, {"SIMON:YELLOW"}, {"SIMON:GREEN"}
};

// // Outputs
const int speakerPin = A5;
const int blueButtonOut = 2;
const int redButtonOut = 3;
const int yellowButtonOut = 4;
const int greenButtonOut = 5;
const int simonLED = 10;
const int wiresLED = 11;
const int tuneLED = 12;

// // Output Information
const int beepFreq = 2000;   // hz
const int beepDur = 50;   // ms
int beepInterval = 500;   // ms
int lastBeepTime = 0;   // ms
const int simonBlink = 350;   // ms

// // Misc.
// Keep time for Simon Says lights
struct Blink {
  int pin;
  bool lit;
  long offTime;
};
Blink simonLights[] = {
  {blueButtonOut, false, 0},
  {redButtonOut, false, 0},
  {yellowButtonOut, false, 0},
  {greenButtonOut, false, 0}
};
// Wire thresholds
const int TH0 = (1000 + 928) / 2;   // 964
const int TH1 = (928  + 512) / 2;   // 720
const int TH2 = (512  +  92) / 2;   // 302
// For analog smoothing
const float alpha = 0.2;
const int potDelta = 4;





// // // Main Processes
void setup() {
  Serial.begin(9600);

  // // Inputs and Outputs
  pinMode(potPin, INPUT);
  pinMode(blueButtonIn, INPUT);
  pinMode(redButtonIn, INPUT);
  pinMode(yellowButtonIn, INPUT);
  pinMode(greenButtonIn, INPUT);
  for (int i = 0; i < 4; i++) {
    pinMode(wirePins[i], INPUT);
  }
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(speakerPin, OUTPUT);
  pinMode(blueButtonOut, OUTPUT);
  pinMode(redButtonOut, OUTPUT);
  pinMode(yellowButtonOut, OUTPUT);
  pinMode(greenButtonOut, OUTPUT);
  pinMode(simonLED, OUTPUT);
  pinMode(wiresLED, OUTPUT);
  pinMode(tuneLED, OUTPUT);

  // // Check built-in LED
  digitalWrite(LED_BUILTIN, HIGH);
  delay(200);
  digitalWrite(LED_BUILTIN, LOW);

  // // Temp check
  digitalWrite(blueButtonOut, HIGH);
  digitalWrite(redButtonOut, HIGH);
  digitalWrite(yellowButtonOut, HIGH);
  digitalWrite(greenButtonOut, HIGH);
  delay(200);
  digitalWrite(blueButtonOut, LOW);
  digitalWrite(redButtonOut, LOW);
  digitalWrite(yellowButtonOut, LOW);
  digitalWrite(greenButtonOut, LOW);

  // // Start handshake w/ p5.js
  while (Serial.available() <= 0) {
    digitalWrite(LED_BUILTIN, HIGH);
    Serial.println("Waiting for data..."); // identifiable starting number
    delay(300);
    digitalWrite(LED_BUILTIN, LOW);
    delay(50);
  }
}

void loop() {
  // // Wait for p5.js
  while (Serial.available()) {
    digitalWrite(LED_BUILTIN, HIGH);

    String target = Serial.readStringUntil('=');
    int value = Serial.parseInt();
    if (Serial.read() == '\n') {
      writeTarget(target, value);
    }

    digitalWrite(LED_BUILTIN, LOW);
  }

  // // Send data to p5.js
  for (int i = 0; i < valueCount; i++) {
    checkValue(i);
    delay(1);
  }

  // // Clear Simon Says lights
  clearSimon();

  // // Play beeps
  if (active) timerSound();

  // // // Temp read wires
  // Serial.print(analogRead(A0)); Serial.print(',');
  // Serial.print(analogRead(A1)); Serial.print(',');
  // Serial.print(analogRead(A2)); Serial.print(',');
  // Serial.println(analogRead(A3));
}





// // // Helper Functions
// // Check current input values and compare to last known value
void checkValue(int index) {
  // // Check value // Wires: 100=1000, 1k=928, 100k=91, 10k=512   <-- W1, W2, W3, W4
  int checking;
  if (index < 1) { // < 5
    // // Add delay and smoothing for analog reads
    delay(1);
    checking = analogRead(inputs[index]);
    smoothVals[index] = alpha * checking + (1 - alpha) * smoothVals[index];
    checking = int(smoothVals[index]);
    // // Check if pot has significant change
    if (abs(checking - inputVals[index]) >= potDelta) {
      inputVals[index] = checking;
      Serial.print(headers[index]);
      Serial.print('=');
      // // Send pot value
      if (index == 0) {
        Serial.println(checking);
      }
      // // Send index of wire connection
      // else {
      //   Serial.println(identifyWire(checking));
      // }
    }
  }
  // else if (index < 5) {
  //   delay(1);
  //   checking = analogRead(inputs[index]);
  //   smoothVals[index] = alpha * checking + (1 - alpha) * smoothVals[index];
  //   checking = int(smoothVals[index]);
  //   int binaryVal = digitalWire(checking);
  //   if (abs(checking - inputVals[index]) >= potDelta && binaryVal != inputVals[index]) {
  //     inputVals[index] = binaryVal;
  //     Serial.print(headers[index]);
  //     Serial.print('=');
  //     Serial.println(binaryVal);
  //   }
  // }
  else {
    checking = digitalRead(inputs[index]);
    // // Compare
    if (checking != inputVals[index]) {
      inputVals[index] = checking;
      Serial.print(headers[index]);
      Serial.print('=');
      Serial.println(checking);
    }
  }
}

// // Handle writing to the target pin
void writeTarget(String target, int value) {
  if (target == "ACTIVE") {
    active = value;
  }
  else if (target == "BUILTIN") {
    digitalWrite(LED_BUILTIN, value);
    delay(150);
    digitalWrite(LED_BUILTIN, LOW);
  }
  // // Change beep interval based on p5.js timer
  else if (target == "BEEP") {
    beepInterval = value;
  }
  // // Simon Says
  else if (target == "SIMON") {
    digitalWrite(simonLED, value); // // Simon Says = defused
  }
  else if (target == "BLUE") {
    flashSimon(blueButtonOut);
  }
  else if (target == "RED") {
    flashSimon(redButtonOut);
  }
  else if (target == "YELLOW") {
    flashSimon(yellowButtonOut);
  }
  else if (target == "GREEN") {
    flashSimon(greenButtonOut);
  }
  // // Wires
  else if (target == "WIRES") {
    digitalWrite(wiresLED, value);
  }
  // // Tune
  else if (target == "TUNE") {
    digitalWrite(tuneLED, value);
  }
}

// // Play beeping noise
void timerSound() {
  if (lastBeepTime > beepInterval) {
    // // Reset
    lastBeepTime = 0;
    noTone(speakerPin);
    // // Play
    tone(speakerPin, beepFreq, beepDur);
  }
  else {
    lastBeepTime++;
  }
}

// // Non-blocking flash for Simon Says
void flashSimon(int pin) {
  for (auto &btn : simonLights) {
    if (btn.pin == pin) {
      digitalWrite(pin, HIGH);
      btn.lit  = true;
      btn.offTime = millis() + simonBlink;
      break;
    }
  }
}
void clearSimon() {
  long now = millis();
  for (auto &btn : simonLights) {
    if (btn.lit && now >= btn.offTime) {
      digitalWrite(btn.pin, LOW);
      btn.lit = false;
    }
  }
}

// // // Determine wire connections
// int identifyWire(int val) {
//   // if (val < 25) return -1;   // unplugged or other issue
//   if (val > TH0) return 0;   // 100 ohm
//   else if (val > TH1) return 1;   // 1k ohm
//   else if (val > TH2) return 3;   // 10k ohm
//   else return 2;   // 100k ohm // remember bottom-up order is 100, 1k, 100k, 10k
// }
// int digitalWire(int val) { // 92, 512, 928, 1000
//   if (val < 50) return 0;
//   if (val <  110 && val > 70) return 1;
//   if (val < 530 && val > 480) return 1;
//   if (val < 950 && val > 905) return 1;
//   if (val < 1024 && val > 975) return 1;
//   return 0;
// }

 

p5.js Code:

p5.js handles the actual game logic, and generates unique solutions for the modules every time a game starts. The Game class contains an array of modules, and continuously calls their update methods. Each module has its own child class extending the Module class, and contains the code for its own specific mechanics. The user can select a difficulty level from the main menu, and start a game.

 

Serial Communication:

The protocol used here follows the basic idea from the in-class examples, reading up until it reaches a newline character. In order to avoid sending the state of every input device to p5.js with every message, I broke things down into messages of the format `HEADER=value`. I mainly used this to indicate which module was sending input data, and combined it with a switch statement to separate things out on the p5.js side. In terms of transmitting to Arduino, I followed a similar idea but only had to send messages to disarm modules (e.g. `SIMON=0`) or defuse the bomb itself to stop the beeping. I also used this to have p5.js increase the frequency of the beeping when its timer reached certain breakpoints.

 

What I’m Proud Of:

I was happy with a number of decisions I made. For one, I was able to cleanly use OOP to separate out the p5.js logic for my modules while including overlapping functionality like disarming. I was also proud of the protocol I came up with for serial communication. It gave me a lot of trouble at first, so it was very fulfilling to end up with a lightweight method where I could direct messages exactly where they needed to go. Lastly, I was proud of my Simon Says module in particular. I spent a lot of time on it early on since it was the first to be implemented, and I feel like it came out the best. I had to figure out how the arcade buttons work and soldered all the necessary wires, but it was worth it since it is probably the most engaging module.

 

Areas for Improvement:

In contrast to Simon Says, I was really disappointed by my Wires module. It was originally the one I was most excited about, since this gave me a chance to actualize the experience of the inspiring game in a unique way. However, I tried a number of different ways to implement it that all failed in the end. My first attempt was to use resistors of different values and use analogRead() to determine which wire was plugged in where. However, the floating values when the wires were unplugged threw things off too much.

Another area for improvement would be the design of the bomb. Using cardboard turned out just fine, but laser cutting a wooden box might have looked more professional. I put a lot of time and effort into the initial construction, especially since I cut the cardboard down by hand, but after that I became far too busy to add any decoration and finishing touches. The p5.js visuals also suffered a bit for the same reason. There was one choice I made that I’m still on the fence about, which was to omit an explosion sound when the player loses a round. It would have been a nice touch, but I already drove myself crazy listening to the beeping sound, and I felt that having the beep frequency pick up speed was sufficient by itself.