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.

Midterm Project – Oracle Lady

Oracle Cards – Midterm Project Report

Oracle Cards is an interactive web-based application I developed, inspired by the mystical allure of tarot and oracle card readings. For this project, I utilized the p5.js library to create a visually engaging and user-friendly experience that allows users to select a category and receive either advice or a reflection question. My goal was to craft an accessible and contemplative digital experience, drawing from my interest in mindfulness and self-reflection. This project leverages p5.js for rendering graphics, handling user input, and incorporating audio to enhance immersion.

About

Gameplay Instructions:
To play Oracle Cards, click the game link above to open the application in a new window. Ensure your browser supports p5.js and that you have a stable internet connection for loading assets (images and audio). The game is controlled entirely via mouse clicks, making it intuitive and accessible. Upon loading, you’ll see a welcome screen with a background image and category buttons. Click a category to proceed, then choose between receiving advice or a reflection question. The result is displayed on a card, accompanied by ambient music you can control with on-screen buttons. All images (e.g., backgrounds, cards) were sourced from royalty-free libraries, and the music tracks are licensed for non-commercial use. I do not claim ownership of these assets but have customized their integration to suit the game’s aesthetic.

How the Game Works

Oracle Cards simulates a digital card-reading experience, drawing inspiration from oracle decks used for introspection and guidance. The game progresses through three states:

  1. Start: Users select a category (“Yourself,” “Friends & Family,” or “Transcendence”).

  2. Choose Type: Users pick between “Advice” or a “Reflection Question.”

  3. Result: A randomly selected response is displayed on a card, based on the chosen category and type.

The game uses a desk-themed interface with a lady in the background (inspired by a fortune-teller aesthetic) to create an immersive atmosphere. Users can control background music via play/pause, next, and previous buttons, enhancing the reflective mood. The sequence of events is as follows:

  • The game loads with a welcome screen and category buttons.

  • Clicking a category transitions to the choice screen, displaying a card with two options.

  • Selecting a choice reveals the result on a card, with a prompt to restart by clicking anywhere.

The design emphasizes simplicity and emotional resonance, encouraging users to pause and reflect.

Game Controls

The controls are straightforward:

  • Mouse Click: Click on category buttons (e.g., “Yourself”) to select a category, choice buttons (e.g., “Advice”) to pick a type, or anywhere on the result screen to restart.

  • Music Controls: Click the previous, play/pause, or next buttons in the top-right corner to manage background music.

This minimal input method ensures accessibility for users of all skill levels.

Proud Moment

I’m particularly proud of implementing the state management system using p5.js, which seamlessly transitions between the start, chooseType, and result states without glitches. Debugging the button click detection was challenging, especially ensuring accurate hitboxes for the buttons, but I resolved this by carefully calculating their positions relative to the canvas. Additionally, integrating audio functionality with p5.js’s loadSound and managing playback states (play/pause, next/previous) was a significant achievement. The randomization of responses, tailored to each category and choice, adds replayability and depth, which I fine-tuned to ensure a balanced and meaningful user experience.

The Code

Reusability and p5.js

The code is designed with modularity in mind, separating concerns like state management, rendering, and user input. Key p5.js functions (preload, setup, draw) are organized to handle asset loading, canvas initialization, and continuous rendering efficiently. For example, the drawButtons function is reusable for rendering both category and choice buttons:

function drawButtons(options, yStart) {
  for (let i = 0; i < options.length; i++) {
    let x = width / 2;
    let y = yStart + i * 50;
    fill(200, 100, 100); // Button color (red)
    rect(x - 100, y - 20, 200, 40, 10); // Draw button with rounded corners
    fill(255); // White text
    textAlign(CENTER, CENTER);
    text(options[i], x, y); // Display button label
  }
}

This function is called in both the start and chooseType states, reducing code duplication. The preload function ensures all images and sounds are loaded before rendering, with error handling via callbacks:

deskLadyImage = loadImage('assets/desk_lady.png', 
  () => console.log("Desk lady image loaded successfully"), 
  (err) => console.error("Failed to load desk lady image:", err)
);

Object-Oriented Code

While the project primarily uses functional programming, the responses object is structured hierarchically to store advice and reflection questions for each category, enabling easy access and randomization:

let responses = {
  "Yourself": {
    "Advice": [
      "Take a deep breath and trust yourself.",
      // ... other advice
    ],
    "Reflection Question": [
      "What is one thing you truly love about yourself?",
      // ... other questions
    ]
  },
  // ... other categories
};

The mousePressed function handles state transitions and music controls, using conditional logic to detect clicks within button boundaries:

if (gameState === "start") {
  for (let i = 0; i < categories.length; i++) {
    if (mouseX > width / 2 - 100 && mouseX < width / 2 + 100 &&
        mouseY > height / 2 + 150 + i * 50 - 20 && mouseY < height / 2 + 150 + i * 50 + 20) {
      selectedCategory = categories[i];
      gameState = "chooseType";
    }
  }
}

This modular approach ensures the code is maintainable and extensible.

Training the Model

No machine learning models were used in this project, as the focus was on user interaction and randomization within p5.js. However, the randomization logic for responses mimics a lightweight decision model. I curated the responses object by researching mindfulness and self-help literature, ensuring each piece of advice or question is concise yet impactful. The random function in p5.js was used to select responses, tested extensively to confirm uniform distribution across options.

Areas of Improvement

I’m delighted with Oracle Cards as a reflective and engaging experience that aligns with my vision of digital mindfulness. However, there are opportunities for enhancement:

  • Additional Features: I’d like to add animations for card transitions and a settings menu to adjust music volume or toggle visuals (e.g., enabling/disabling the crystal ball image).

  • Visual Polish: Incorporating hover effects for buttons and more varied card designs could elevate the aesthetic.

  • Content Expansion: Adding more categories or allowing users to input custom questions would increase replayability.

  • Accessibility: Implementing keyboard controls alongside mouse clicks would make the game more inclusive.

As for the game logic, I plan to explore subtle physics-based animations (e.g., card flipping) using p5.js to enhance interactivity.

Conclusion

Overall, I’m proud of Oracle Cards and how it blends creativity, technical skill, and emotional resonance. It’s a meaningful step in my journey with interactive design, and I look forward to refining it further to share its calming experience with others.

Below is the Game:

Week 4 response

Introduction:

When I was reading the book I noticed how my thoughts that I had internally were said outside. Maybe I didn’t know how to word it as well but the idea was just as clear. The way he describes the process and the way the designers think of it vs how we as the users think of the items is different and that kind of makes it uncomfortable to use. There are things specifically on campus that drive me crazy sometimes because of how it is made and not efficient.

Reading Response:

Based on the reading something that annoys me would be the doors on campus, specifically the doors at Dining Hall 2. They don’t have sensors to enter and require you to pull but then open themselves automatically. The harder you pull the more opposing force it uses to prevent you from opening it manually. I prefer doors that can just open from a push either way. But how do I use the frustration of these inefficient made doors in interactive media? Well, I would make it so that it’s user-friendly. Something that is so easy to use that intrinsic to use people, something that’s almost close to common sense for the everyday layperson. Not focusing on creating something that’s easy as a coder or an artist but something thats easy for my audience.

 

Data visualization:

Below is a data visualization that’s very simple and not advanced at all but just very basic to show data that’s all.

let users = [];
let colors = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#00ffff', '#ffa500', '#800080', '#008080', '#ffd700'];
let timeAwakeInput, timeWorkingInput, submitButton, newUserButton, userSelect;
let currentUser = 0;
let barWidth = 20;

function setup() {
  createCanvas(800, 500);
  background(240);
  drawAxes();

  timeAwakeInput = createSelect();
  for (let i = 2; i <= 24; i += 2) {
    timeAwakeInput.option(i);
  }
  timeAwakeInput.position(10, 10);

  timeWorkingInput = createSelect();
  for (let i = 0.5; i <= 24; i += 0.5) {
    timeWorkingInput.option(i);
  }
  timeWorkingInput.position(10, 40);

  submitButton = createButton('Submit Entry');
  submitButton.position(10, 70);
  submitButton.mousePressed(addEntry);

  newUserButton = createButton('New User');
  newUserButton.position(110, 70);
  newUserButton.mousePressed(createNewUser);

  userSelect = createSelect();
  userSelect.position(10, 100);
  userSelect.changed(changeUser);

  createNewUser();

  textAlign(LEFT, CENTER);
  text('Time Awake (hours):', 150, 25);
  text('Time Working (hours):', 150, 55);
}

function createNewUser() {
  let newUser = {
    id: users.length,
    color: colors[users.length % colors.length],
    entries: []
  };
  users.push(newUser);
  userSelect.option('User ' + (newUser.id + 1), newUser.id);
  userSelect.selected(newUser.id);
  currentUser = newUser.id;
}

function changeUser() {
  currentUser = parseInt(userSelect.value());
}

function addEntry() {
  let timeAwake = parseFloat(timeAwakeInput.value());
  let timeWorking = parseFloat(timeWorkingInput.value());

  if (timeWorking > timeAwake) {
    alert("Time working cannot exceed time awake!");
    return;
  }

  users[currentUser].entries.push({
    timeAwake: timeAwake,
    timeWorking: timeWorking
  });

  updateGraph();
}

function drawAxes() {
  stroke(0);
  line(50, height - 50, width - 50, height - 50); // X-axis
  line(50, height - 50, 50, 50); // Y-axis
  
  textAlign(CENTER);
  text("Time Awake (hours)", width/2, height - 10);
  
  push();
  translate(15, height/2);
  rotate(-HALF_PI);
  text("Time Working (hours)", 0, 0);
  pop();
  
  for (let i = 0; i <= 24; i += 4) {
    let x = map(i, 0, 24, 50, width - 50);
    line(x, height - 50, x, height - 45);
    textAlign(CENTER);
    text(i, x, height - 35);
  }
  
  for (let i = 0; i <= 24; i += 4) {
    let y = map(i, 0, 24, height - 50, 50);
    line(45, y, 50, y);
    textAlign(RIGHT);
    text(i, 40, y);
  }
}

function updateGraph() {
  background(240);
  drawAxes();
  
  let groupWidth = barWidth * users.length;
  
  for (let i = 0; i < users.length; i++) {
    let user = users[i];
    for (let entry of user.entries) {
      let x = map(entry.timeAwake, 0, 24, 50, width - 50);
      let barHeight = map(entry.timeWorking, 0, 24, 0, height - 100);
      
      // Adjust x position based on user index
      let adjustedX = x - groupWidth/2 + i * barWidth + barWidth/2;
      
      fill(user.color);
      rect(adjustedX - barWidth/2, height - 50 - barHeight, barWidth, barHeight);
      
      push();
      fill(0);
      textAlign(CENTER);
      textSize(10);
      text(entry.timeWorking + "h", adjustedX, height - 55 - barHeight);
      pop();
    }
  }
}

 

Week 3 – Assignment

Introduction: Inspiration and Thought Process

When approaching this project, I wanted to create something that felt organic, dynamic, and visually engaging while staying true to my interest in systems, patterns, and movement. I’ve always been fascinated by the hidden structures that govern the natural world—whether it’s the way air currents shape cloud formations, how magnetic fields interact with charged particles, or how fluid dynamics influence ocean currents. These invisible forces dictate movement and structure on both microscopic and massive scales, yet they often go unnoticed in daily life.

This fascination led me to explore generative art as a way to reveal these hidden forces. By using a flow field generated through Perlin noise, I aimed to simulate an abstract yet realistic movement of particles through a force-driven system. The goal was to make the movement feel both unpredictable and structured—like a balance between chaos and order, mirroring how natural systems operate.

Another reason this concept resonated with me is my interest in interactive media and computational design. I see creative coding as a bridge between logic and aesthetics—using algorithms not just to solve problems but to create emotionally engaging experiences. This project became a way to explore how simple rules (vector fields and movement constraints) can lead to complex, emergent behaviors.

How I Decided on the Approach

Initially, I considered different ways of visualizing movement:

  • Cellular automata (which follows a discrete rule set)
  • Particle systems with basic physics (which simulate real-world gravity and collisions)
  • Algorithmic drawing techniques (such as recursive fractals)

However, I specifically wanted smooth, flowing movement, where particles appear to drift through an unseen force field rather than follow rigid, predictable patterns. This led me to research flow fields, a technique often used in generative art to create dynamic motion based on vector fields derived from Perlin noise. The key insight was that by giving each particle a force vector at every point in space, I could create an artwork where movement itself became the visual expression.

Connecting to My Interests

This project ties into my broader interest in interactive systems and generative design. In the future, I could see expanding this into an interactive piece, where users could manipulate the flow field in real-time, changing the behavior of the particles with gestures or sound inputs. Additionally, this exploration of emergent patterns aligns with my curiosity about how simple rules can create complexity, something that applies not just to visual art but also to fields like engineering, physics, and artificial intelligence.

Ultimately, this artwork serves as both a study of motion and structure and a reflection of how natural forces shape our world in ways we don’t always perceive.

My code:

let particles = [];
let flowField;
let cols, rows;
let scl = 20; // Scale of the grid
let zOff = 0; // Noise offset for animation

function setup() {
  createCanvas(600, 600);
  cols = floor(width / scl);
  rows = floor(height / scl);
  flowField = new Array(cols * rows);

  // Create particles
  for (let i = 0; i < 500; i++) {
    particles.push(new Particle());
  }
}

function draw() {
  background(0, 10); // Faint trail effect

  // Generate the flow field using Perlin noise
  let yOff = 0;
  for (let y = 0; y < rows; y++) {
    let xOff = 0;
    for (let x = 0; x < cols; x++) {
      let index = x + y * cols;
      let angle = noise(xOff, yOff, zOff) * TWO_PI * 4;
      let v = p5.Vector.fromAngle(angle);
      flowField[index] = v;
      xOff += 0.1;
    }
    yOff += 0.1;
  }
  zOff += 0.01;

  // Update and display particles
  for (let particle of particles) {
    particle.follow(flowField);
    particle.update();
    particle.edges();
    particle.show();
  }
}

// Particle class
class Particle {
  constructor() {
    this.pos = createVector(random(width), random(height));
    this.vel = createVector(0, 0);
    this.acc = createVector(0, 0);
    this.maxSpeed = 2;
    this.color = color(random(255), random(255), random(255), 100);
  }

  follow(vectors) {
    let x = floor(this.pos.x / scl);
    let y = floor(this.pos.y / scl);
    let index = x + y * cols;
    let force = vectors[index];
    this.applyForce(force);
  }

  applyForce(force) {
    this.acc.add(force);
  }

  update() {
    this.vel.add(this.acc);
    this.vel.limit(this.maxSpeed);
    this.pos.add(this.vel);
    this.acc.mult(0);
  }

  edges() {
    if (this.pos.x > width) this.pos.x = 0;
    if (this.pos.x < 0) this.pos.x = width;
    if (this.pos.y > height) this.pos.y = 0;
    if (this.pos.y < 0) this.pos.y = height;
  }

  show() {
    stroke(this.color);
    strokeWeight(2);
    point(this.pos.x, this.pos.y);
  }
}

 

Week 2 – Reading response

How are you planning to incorporate random elements into your work?

Well, I myself have am not used to the idea of producing something random, although there is beauty in the idea of randomness it’s not as alluring as the idea of systematic approach. When it’s random, it’s likely only to be produced once like that and twice if you can beat the odds. Although Casey, with in the video showed how these random elements can prove to not only provide an unique appearance, he also presents its limitations on how once done can’t be done agin, the idea of having to repeat a process to hopefully create something similar can be infuriating, but also fascinating as the random elements could go beyond expectation or never reach the expectations sent. I intend to incorporate this kind of thinking into future works, I might not be happy in the beginning due to my rigid thinking but over time I could change and will change in how to balance the randomness that can only produced once and the control of the scope in which it happens.

Where do you feel is the optimum balance between total randomness and complete control?

I think that when theres a start of a systematic approach too randomness. All randomness has to be born with in a confined space, whether by the laws of science that govern the way things move or how systems have an algorithm that will produce a random sequence based off the a equation thats been written before hand. You can’t escape, it but you can confine it with in a space. Like a science experiment you might have the tools, and test it out and then an outcome that works. You have your controlled variables and uncontrolled variables. Sometimes we are the uncontrolled variables but our way of thinking is the controlled variable. The way we move and do things, the laws of the universe these are controlled, set in stone in that moment, but once we release it without being able to calculate or speculate the outcome it then becomes random.

Week 2 Assignment

Introduction:

While looking through the same art, I thought that its actually far from what I could create at least at this point. I wanted it to simple yet something that reminds me of home. I ended up then thinking about what makes home, home? Was it the people that I missed? Was it the seas and mountains? Was it the seasons? Well, it was a bit of everything, the way the mountain changed its vegetation across the seasons, the way I’d travel to those mountains with those very precious people. The way the seasons or the feel of the seasons could not be replicated here with in the UAE.

Concept:

My concept for the post had to do with mountains and seasons. The reflection on something not season as clearly or with in the country as abundantly as my own country. I didn’t really use any references from the document but it’s essence rather gave me something I’d rather implement.

 

Production and Completion:

Although I had to go through many trials, like getting the color scheme, the clouds, the sun and moon, the mountains, as well as the time switch to show the seasons. I wasn’t satisfied, because I had a dream of how it should have made me felt. I hadn’t gotten close but I had gotten somewhere, starting with what can represent the seasons? the time? The way things change. If you look at the code you will notice that the frame rate changes at about every 3 seconds like the months. The sun changes into the after 1.6 seconds and the moon to the sun at 0.8 seconds, match 16 hours of sun and 8 hours of moon.

 

let sunAngle = 0;
let showMoon = false;
let seasonProgress = 0;
let currentSeason = "spring";
let mountainProgress = 0;
let seasons = ["spring", "summer", "autumn", "winter"];
let brushColors;
let icons = [];
let clouds = [];

function setup() {
    createCanvas(800, 600);
    brushColors = [
        color(139, 69, 19),  // Brown (Autumn)
        color(34, 139, 34),  // Green (Spring)
        color(255, 165, 0),  // Yellow-orange (Summer)
        color(173, 216, 230) // **Distinctive icy blue for Winter**
    ];
    frameRate(30);
    generateIcons();
    generateClouds();
}

function draw() {
    if (currentSeason === "spring") {
        background(144, 238, 144);
    } else if (currentSeason === "summer") {
        background(255, 223, 186);
    } else if (currentSeason === "autumn") {
        background(255, 165, 0);
    } else if (currentSeason === "winter") {
        background(100, 150, 200); // **Deeper Frost Blue for Winter**
    }

    drawMountain();
    drawClouds();
    if (!showMoon) {
        drawSun();
    } else {
        drawMoon();
    }
    displayIcons();
    updateIcons();
    updateClouds();

    if (frameCount % 90 === 0) {
        changeSeason();
        generateIcons();
    }
}

function drawMountain() {
    let layerHeight = 50;
    let numLayers = 6;
    for (let i = 0; i < numLayers; i++) {
        let layerColor = getMountainColor(i);
        stroke(layerColor);
        strokeWeight(15 + (sin(mountainProgress + i * 0.5) * 5));
        noFill();
        beginShape();
        for (let x = 0; x < width; x++) {
            let y = sin(x * 0.02 + mountainProgress + i * 0.5) * (100 + i * layerHeight) + 400;
            vertex(x, y);
        }
        endShape();
    }
    mountainProgress += 0.02;
}

function getMountainColor(layerIndex) {
    let seasonIndex = seasons.indexOf(currentSeason);
    if (currentSeason === "spring") {
        return lerpColor(brushColors[1], color(255, 255, 255), layerIndex * 0.1);
    } else if (currentSeason === "summer") {
        return lerpColor(brushColors[2], color(255, 255, 255), layerIndex * 0.1);
    } else if (currentSeason === "autumn") {
        return lerpColor(brushColors[0], color(255, 255, 255), layerIndex * 0.1);
    } else if (currentSeason === "winter") {
        return lerpColor(brushColors[3], color(255, 255, 255), layerIndex * 0.2); // **Stronger white contrast for winter**
    }
}

function drawSun() {
    fill(255, 204, 0, 150);
    noStroke();
    let size = 100 + sin(sunAngle) * 30;
    ellipse(650, 100, size, size);
    sunAngle += 0.02;

    if (sunAngle > TWO_PI) {
        sunAngle = 0;
        showMoon = true;
    }
}

function drawMoon() {
    let moonSize = map(sin(frameCount * 0.1), -1, 1, 50, 100);
    fill(255, 255, 255, 150);
    noStroke();
    ellipse(650, 100, moonSize, moonSize);
    if (frameCount % 24 === 0) {
        showMoon = false;
    }
}

function changeSeason() {
    let nextSeasonIndex = (seasons.indexOf(currentSeason) + 1) % seasons.length;
    currentSeason = seasons[nextSeasonIndex];
    generateClouds();
}

function generateIcons() {
    icons = [];
    let iconSymbol = "";
    if (currentSeason === "spring") {
        iconSymbol = "🌸";
    } else if (currentSeason === "summer") {
        iconSymbol = "☀️";
    } else if (currentSeason === "autumn") {
        iconSymbol = "🍂";
    } else if (currentSeason === "winter") {
        iconSymbol = "❄️";
    }
    for (let i = 0; i < 5; i++) {
        icons.push({
            x: random(width),
            y: random(100, 300),
            speed: random(0.2, 0.5),
            symbol: iconSymbol
        });
    }
}

function updateIcons() {
    for (let icon of icons) {
        icon.y += icon.speed;
        if (icon.y > height) {
            icon.y = random(100, 300);
        }
    }
}

function displayIcons() {
    textSize(32);
    textAlign(CENTER, CENTER);
    for (let icon of icons) {
        text(icon.symbol, icon.x, icon.y);
    }
}

function generateClouds() {
    clouds = [];
    for (let i = 0; i < 6; i++) {
        clouds.push({
            x: random(width),
            y: random(50, 200),
            size: random(60, 100),
            opacity: random(100, 200),
            speed: random(0.5, 1.5)
        });
    }
}

function drawClouds() {
    for (let cloud of clouds) {
        fill(255, 255, 255, cloud.opacity);
        noStroke();
        ellipse(cloud.x, cloud.y, cloud.size, cloud.size * 0.6);
        ellipse(cloud.x + 30, cloud.y, cloud.size * 0.8, cloud.size * 0.5);
        ellipse(cloud.x - 30, cloud.y, cloud.size * 0.9, cloud.size * 0.6);
    }
}

function updateClouds() {
    for (let cloud of clouds) {
        cloud.x += cloud.speed;
        cloud.size += sin(frameCount * 0.01) * 0.5;
        cloud.opacity = map(sin(frameCount * 0.01), -1, 1, 100, 200);
        if (cloud.x > width + 50) {
            cloud.x = -50;
            cloud.y = random(50, 200);
        }
    }
}

Sketch:

Improvements:

I think I can improve a lot with more dynamic visuals and smoother transitions. I will try to include more user interaction that can allow them to do more than just have a ‘watch’ experience. They should be able to have ‘active’ experience as a participant. I could include sounds that mimic the ambiance achieved in  places in South African to make it further immersive.