diff --git a/examples/mascot-jump-game/README.md b/examples/mascot-jump-game/README.md new file mode 100644 index 0000000..1599378 --- /dev/null +++ b/examples/mascot-jump-game/README.md @@ -0,0 +1,431 @@ +# Mascot Jump Game + +An endless runner game inspired by the classic browser dinosaur game, where you control an LED character jumping over electronic components. Features progressively increasing difficulty, score tracking, one-button gameplay, and synchronized LED matrix animations on the UNO Q. + +![Mascot Jump Game Example](assets/docs_assets/thumbnail.png) + +## Description + +The App uses the `web_ui` Brick to create a browser-based game with real-time communication between the UNO Q and a web interface. The backend manages game physics, collision detection, and scoring at 60 FPS, while the frontend renders the LED character using PNG images for different animations. + +![Mascot Jump Game - LED Character](assets/docs_assets/led_character_animation.png) + +Key features include: + +- LED character with six animation states (4 running patterns, jump, game over) +- Electronic component obstacles: resistors, transistors, and microchips +- Synchronized LED matrix display mirroring game state +- Progressive difficulty scaling with score +- Keyboard and mouse control +- Session high score tracking + +## Bricks Used + +The mascot jump game example uses the following Bricks: + +- `web_ui`: Brick to create a web interface with real-time communication between the browser and Arduino board with game state updates, input handling, and rendering synchronization. + +## Hardware and Software Requirements + +### Hardware + +- Arduino UNO Q (x1) +- USB-C® cable (for power and programming) (x1) + +### Software + +- Arduino App Lab + +**Note:** You can also run this example using your Arduino UNO Q as a Single Board Computer (SBC) using a [USB-C hub](https://store.arduino.cc/products/usb-c-to-hdmi-multiport-adapter-with-ethernet-and-usb-hub) with a mouse, keyboard and display attached. + +## How to Use the Example + +1. **Run the App** + +![Arduino App Lab - Run App](assets/docs_assets/launch-app.png) + +2. **Access the Web Interface** + +The App should open automatically in the web browser. You can also open it manually via `.local:7000`. The `WebUI` brick establishes a WebSocket connection for real-time communication between browser and UNO Q. + +3. **Wait for Game Initialization** + +The game loads and displays the LED character in idle state. The `GameState` class initializes with default parameters, while the Arduino sketch begins polling game state through `Bridge.call("get_led_state").result(gameState)`. + +4. **Start Playing** + +Press **SPACE** or **UP ARROW** to jump over obstacles. The keypress triggers a `player_action` WebSocket message to the backend, which validates and applies the jump physics. Use **R** to restart after game over. + +![Gameplay Example](assets/docs_assets/game_play_state.gif) + +5. **Avoid Obstacles** + +Jump over three types of electronic components: *resistors* (small), *transistors* (medium), and *microchips* (large). The backend's `spawn_obstacle()` creates new obstacles at random intervals, while the game loop moves them across the screen. Your score increases continuously based on survival time. + +6. **Game Over** + +When you hit an obstacle, `check_collisions()` detects the hit and triggers game over. Your final score and session high score are displayed. The LED character shows a fallen animation. Press **SPACE** to call `game.reset()` and restart. + +![Game Over Screen](assets/docs_assets/game_over_state.gif) + +7. **LED Matrix Synchronization** + +The LED matrix on your UNO Q mirrors the game state. The Arduino sketch calls `Bridge.call("get_led_state").result(gameState)` every 50 ms to get the current state (*running*, *jumping*, *game_over*, or *idle*), then displays the matching LED frame from `game_frames.h`. For more information about the LED matrix, see the [LED Matrix setion from the UNO Q user manual](https://docs.arduino.cc/tutorials/uno-q/user-manual/#led-matrix). + +![LED Matrix Frames](assets/docs_assets/led_matrix_frames.png) + +8. **Progressive Difficulty** + +The game speed increases as your score grows using `BASE_SPEED + (score / 1500.0)`. The `game_loop()` runs at 60 FPS, updating physics, moving obstacles, checking collisions, and broadcasting state to all connected clients. + +## How it Works + +Once the App is running, it performs the following operations: + +- **Managing game state and physics calculations on the backend.** + +The backend maintains the complete game state and physics engine: + +```python +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI +import time +import random +import threading +import json +... +class GameState: + def __init__(self): + self.reset() + self.high_score = 0 + + def reset(self): + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + self.obstacles = [] + self.score = 0 + self.game_over = False + self.speed = BASE_SPEED + self.last_spawn_time = time.time() + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def update_physics(self, dt): + if not self.on_ground: + self.velocity_y += GRAVITY * dt * 60 # Scale for 60 FPS base + self.mascot_y += self.velocity_y * dt * 60 + + # Ground collision + if self.mascot_y >= GROUND_Y - MASCOT_HEIGHT: + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True +... +game = GameState() +``` + +The physics engine calculates gravity effects, jump trajectories, and collision boundaries at a fixed timestep for consistent gameplay. + +- **Providing LED matrix state through Bridge communication.** + +The LED Matrix on the UNO Q displays the game state in real-time with a simplified mascot design: + +```python +def get_led_state(): + global game_started + + if game.game_over: + return "game_over" + elif not game_started and game.score == 0: + return "idle" + elif not game.on_ground: + return "jumping" + else: + return "running" + +... +# Provide function to Arduino sketch +Bridge.provide("get_led_state", get_led_state) +``` + +The LED matrix shows different animations: + +- **Running State:** 4-frame animation cycling through leg positions +- **Jumping State:** Mascot in mid-air with arms spread +- **Idle State:** Standing mascot waiting to start +- **Game Over State:** Fallen mascot rotated 45 degrees with extended arms + +The Arduino sketch processes these states using the `Arduino_LED_Matrix` library: + +```cpp +#include +#include +#include "game_frames.h" + +Arduino_LED_Matrix matrix; + +int animationFrame = 0; +unsigned long lastFrameTime = 0; +const unsigned long ANIMATION_DELAY = 200; + +void setup() { + matrix.begin(); + matrix.setGrayscaleBits(3); // 3-bit grayscale (0-7 brightness levels) + Bridge.begin(); +} + +void loop() { + String gameState; + bool ok = Bridge.call("get_led_state").result(gameState); + + if (ok) { + if (gameState == "running") { + // Animate between four running frames + unsigned long currentTime = millis(); + if (currentTime - lastFrameTime > ANIMATION_DELAY) { + animationFrame = (animationFrame + 1) % 4; + lastFrameTime = currentTime; + } + + switch(animationFrame) { + case 0: matrix.draw(running_frame1); break; + case 1: matrix.draw(running_frame2); break; + case 2: matrix.draw(running_frame3); break; + case 3: matrix.draw(running_frame4); break; + } + } else if (gameState == "jumping") { + matrix.draw(jumping); + animationFrame = 0; + } else if (gameState == "game_over") { + matrix.draw(game_over); + animationFrame = 0; + } else if (gameState == "idle") { + matrix.draw(idle); + animationFrame = 0; + } else { + matrix.draw(idle); + } + } else { + matrix.draw(idle); + } + + delay(50); // Update at ~20 FPS +} +``` + +- **Processing user input through WebSocket events.** + +Input handling uses event-based communication: + +```python +def on_player_action(client_id, data): + global game_started + action = data.get('action') + + if action == 'jump': + game_started = True + if game.jump(): + ui.send_message('jump_confirmed', {'success': True}) + elif action == 'restart': + game.reset() + game_started = True # Game restarts + ui.send_message('game_reset', {'state': game.to_dict()}) + +ui = WebUI() +... +ui.on_message('player_action', on_player_action) +``` + +The backend validates inputs to prevent invalid actions, such as jumping while airborne or during the game-over state. + +- **Running the main game loop with fixed timestep updates.** + +The game loop runs at 60 FPS intervals: + +```python +def game_loop(): + global game_running, game_started + last_update = time.time() + + while game_running: + current_time = time.time() + dt = current_time - last_update + + if not game.game_over: + game.update_physics(dt) + game.update_obstacles(dt) + game.check_collisions() + game.score += int(60 * dt) + game.speed = BASE_SPEED + (game.score / 1500.0) + + ui.send_message('game_update', game.to_dict()) + + last_update = current_time + sleep_time = max(0, (1/FPS) - (time.time() - current_time)) + time.sleep(sleep_time) +``` + +- **Handling obstacle generation and collision detection.** + +The system manages three types of electronic component obstacles: + +```python +OBSTACLE_TYPES = [ + {'name': 'resistor', 'height': 28},    # Small + {'name': 'transistor', 'height': 38},  # Medium + {'name': 'microchip', 'height': 48}    # Large +] + +def spawn_obstacle(self): + obstacle_type = random.choice(OBSTACLE_TYPES) + height = obstacle_type['height'] + + obstacle = { + 'x': GAME_WIDTH + 30, + 'y': GROUND_Y - height, + 'width': OBSTACLE_WIDTH, + 'height': height, + 'type': obstacle_type['name'] + } + self.obstacles.append(obstacle) +``` + +- **Synchronizing game state with frontend rendering.** + +The frontend maintains rendering with PNG images for the LED character: + +```javascript +function loadLEDImages() { +   const imagesToLoad = [ + { key: 'move1', src: 'img/ledcharacter_move1.png' }, + { key: 'move2', src: 'img/ledcharacter_move2.png' }, + { key: 'move3', src: 'img/ledcharacter_move3.png' }, + { key: 'move4', src: 'img/ledcharacter_move4.png' }, + { key: 'jump', src: 'img/ledcharacter_jump.png' }, + { key: 'gameover', src: 'img/ledcharacter_gameover.png' } + ]; + ... +} + +// Cycle through movement patterns on each jump +socket.on('jump_confirmed', (data) => { +   if (data.success) { +       currentMovePattern = (currentMovePattern % 4) + 1; + } +}); + +function drawMascot() { + if (!gameConfig || !gameState || !imagesLoaded) return; + + const x = gameConfig.mascot_x; + const y = Math.round(gameState.mascot_y); + + let imageToUse = null; + + // Select appropriate image based on game state + if (gameState.game_over) { + imageToUse = ledImages.gameover; + } else if (!gameState.on_ground) { + imageToUse = ledImages.jump; + } else { + // Use current movement pattern + switch(currentMovePattern) { + case 1: imageToUse = ledImages.move1; break; + case 2: imageToUse = ledImages.move2; break; + case 3: imageToUse = ledImages.move3; break; + case 4: imageToUse = ledImages.move4; break; + default: imageToUse = ledImages.move1; + } + } + + ... +} +``` + +The high-level data flow looks like this: + +1. **User Input**: Player presses SPACE/UP or clicks to jump +2. **WebSocket**: Input is sent to backend +3. **Backend Processing**: Validates action and updates game state +4. **Game Loop (60 FPS)**: +- Physics update (such as gravity, velocity, and position) +- Collision detection +- State broadcast to clients +5. **Parallel Rendering**: +- Frontend: Canvas draws mascot and obstacles +- LED matrix update: UNO Q displays synchronized LED animations based on game state +6. **Visual Feedback**: Updated display on browser and LED matrix + +## Understanding the Code + +Here is a brief explanation of the App components: + +### 🔧 Backend (`main.py`) + +The Python® component manages all game logic and state. + +- **Game state management**: Tracks the LED character's position, velocity, obstacle locations, score, and game status +- **Physics engine**: Simulates gravity and jump mechanics with frame-independent movement at 60 FPS +- **Obstacle system**: Randomly spawns three types of electronic components (resistors, transistors, microchips) at intervals between 900-1500 ms, moves them across the screen, and removes them when off-screen +- **Collision detection**: Checks if the LED character intersects with any obstacles each frame and triggers game over on collision +- **Bridge communication**: Provides game state to the Arduino LED matrix through the `get_led_state` function +- **Game loop**: Updates physics, obstacles, and score 60 times per second, then broadcasts the game state to the web interface + +### 🔧 Frontend (`app.js` + `index.html`) + +The web interface renders the game using HTML5 Canvas and PNG images. + +- **Canvas rendering**: Displays the LED character using 6 PNG sprites, cycles through 4 running patterns with each jump, and renders electronic component obstacles at 60 FPS +- **Input handling**: Captures keyboard controls (**SPACE/UP** to jump, **R** to restart) and sends actions to the backend via WebSocket +- **Obstacle rendering**: Draws resistors with color bands (red, yellow, green), transistors with *TO-92* package and three pins, and microchips labeled IC555 +- **WebSocket communication**: Connects to the backend on page load, sends player actions, and receives real-time game state updates +- **Score display**: Shows current score and session high score with zero-padded formatting, updating in real-time + +### 🔧 Arduino Component (`sketch.ino` + `game_frames.h`) + +The Arduino sketch displays synchronized LED matrix animations. + +- **Bridge integration**: Retrieves the current game state from the Python® backend via Bridge communication +- **Animation system**: Plays different LED patterns based on game state (running, jumping, game over, or idle) +- **LED patterns**: Each frame is an 8x13 matrix (104 values) stored in `game_frames.h`: + +```cpp +// Example: Running frame 1 +uint8_t running_frame1[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, // Row 0: Head + 0,0,0,7,7,7,7,0,0,0,0,0,0, // Row 1: Body + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, // Row 5: Body/legs + 0,0,7,0,0,0,0,7,0,0,0,0,0, // Row 6: Legs animated + 7,7,7,7,7,7,7,7,7,7,7,7,7 // Row 7: Ground line +}; +``` + +### 👾 Customizing LED Matrix Frames + +The LED matrix frames can be easily customized in `game_frames.h`. Each frame is 8 rows × 13 columns (104 values): + +- **Brightness values**: 0 (off), 1-3 (dim), 4-5 (medium), 6-7 (bright) +- **Row 7**: Always the ground line (all 7s) +- **Animation**: Only row 6 changes between running frames (leg positions) + +To create custom frames: + +1. Design your pattern on an 8×13 grid +2. Use values 0-7 for different brightness levels +3. Replace the array values in `game_frames.h` +4. Upload the sketch to see your custom mascot + +### 🕹️ Game Configuration + +Key constants that define the gameplay, found in `main.py` and can be modified: + +- **Physics**: Gravity (0.65), jump velocity (-12.5), ground position (240px) +- **Canvas**: 800x300px with LED character size of 44x48px +- **Obstacles**: Resistor (28px), Transistor (38px), Microchip (48px), width (18px) +- **Timing**: Base speed (6.0), spawn intervals (900-1500 ms), target 60 FPS +- **Difficulty**: Speed increases with score (score/1500 rate) + +You can adjust these values at the top of `main.py` to customize gameplay difficulty, physics, and visual layout. LED matrix frames can be customized in `game_frames.h` by modifying the 8x13 arrays. \ No newline at end of file diff --git a/examples/mascot-jump-game/app.yaml b/examples/mascot-jump-game/app.yaml new file mode 100644 index 0000000..da022dd --- /dev/null +++ b/examples/mascot-jump-game/app.yaml @@ -0,0 +1,6 @@ +name: Mascot Jump Game +icon: 🏃 +description: An endless runner game where you jump over electronic components with the LED character + +bricks: + - arduino:web_ui diff --git a/examples/mascot-jump-game/assets/app.js b/examples/mascot-jump-game/assets/app.js new file mode 100644 index 0000000..d55668a --- /dev/null +++ b/examples/mascot-jump-game/assets/app.js @@ -0,0 +1,507 @@ +// SPDX-FileCopyrightText: Copyright (C) 2025 +// SPDX-License-Identifier: MPL-2.0 + +// Game configuration received from backend +let gameConfig = null; +let gameState = null; +let socket = null; + +// Canvas setup +let canvas = null; +let ctx = null; + +// Animation state +let currentMovePattern = 1; // Track movement pattern (1-4) +let blinkState = true; +let lastBlinkTime = Date.now(); +let animationId = null; + +// LED Character Images +let ledImages = { + move1: null, + move2: null, + move3: null, + move4: null, + jump: null, + gameover: null +}; + +// Track which images are loaded +let imagesLoaded = false; + +// Colors +const BG_COLOR = '#f5f5f5'; +const FG_COLOR = '#282828'; +const ACCENT_COLOR = '#3c3c3c'; + +document.addEventListener('DOMContentLoaded', () => { + loadLEDImages(); + initCanvas(); + initSocketIO(); + initInputHandlers(); + startGameLoop(); +}); + +function loadLEDImages() { + const imagesToLoad = [ + { key: 'move1', src: 'img/ledcharacter_move1.png' }, + { key: 'move2', src: 'img/ledcharacter_move2.png' }, + { key: 'move3', src: 'img/ledcharacter_move3.png' }, + { key: 'move4', src: 'img/ledcharacter_move4.png' }, + { key: 'jump', src: 'img/ledcharacter_jump.png' }, + { key: 'gameover', src: 'img/ledcharacter_gameover.png' } + ]; + + let loadedCount = 0; + + imagesToLoad.forEach(({ key, src }) => { + const img = new Image(); + img.onload = () => { + ledImages[key] = img; + loadedCount++; + if (loadedCount === imagesToLoad.length) { + imagesLoaded = true; + console.log('All LED character images loaded from img/ folder'); + } + }; + img.onerror = () => { + console.error(`Failed to load image: ${src}`); + // Try loading from root directory as fallback + const filename = src.split('/').pop(); + console.log(`Trying fallback path: ${filename}`); + img.src = filename; + }; + img.src = src; + }); +} + +function initCanvas() { + canvas = document.getElementById('gameCanvas'); + ctx = canvas.getContext('2d'); + + // Set canvas properties for pixels + ctx.imageSmoothingEnabled = false; + + // Handle window resize + window.addEventListener('resize', handleResize); + handleResize(); +} + +function handleResize() { + // Scale canvas to fit window while maintaining aspect ratio + const maxWidth = window.innerWidth - 40; + const maxHeight = window.innerHeight - 150; + const scale = Math.min(maxWidth / 800, maxHeight / 300, 1); + + if (scale < 1) { + canvas.style.width = `${800 * scale}px`; + canvas.style.height = `${300 * scale}px`; + } +} + +function initSocketIO() { + socket = io(`http://${window.location.host}`); + + socket.on('connect', () => { + console.log('Connected to game server'); + updateConnectionStatus(true); + socket.emit('client_connected', {}); + }); + + socket.on('disconnect', () => { + console.log('Disconnected from game server'); + updateConnectionStatus(false); + }); + + socket.on('game_init', (data) => { + console.log('Received game initialization:', data); + gameConfig = data.config; + gameState = data.state; + updateScoreDisplay(); + }); + + socket.on('game_update', (data) => { + gameState = data; + updateScoreDisplay(); + }); + + socket.on('game_reset', (data) => { + console.log('Game reset'); + gameState = data.state; + updateScoreDisplay(); + // Reset animation states + currentMovePattern = 1; + blinkState = true; + }); + + socket.on('jump_confirmed', (data) => { + if (data.success) { + console.log('⬆Jump confirmed'); + // Cycle to next movement pattern (1->2->3->4->1) + currentMovePattern = (currentMovePattern % 4) + 1; + } + }); + + socket.on('error', (error) => { + console.error('Socket error:', error); + showError('Connection error: ' + error); + }); +} + +function initInputHandlers() { + // Keyboard controls + document.addEventListener('keydown', handleKeyPress); + + // Touch/click controls for mobile + canvas.addEventListener('click', handleCanvasClick); + canvas.addEventListener('touchstart', handleCanvasTouch); + + // Prevent default touch behaviors + canvas.addEventListener('touchmove', (e) => e.preventDefault()); + canvas.addEventListener('touchend', (e) => e.preventDefault()); +} + +function handleKeyPress(e) { + switch(e.code) { + case 'Space': + case 'ArrowUp': + e.preventDefault(); + performAction(); + break; + case 'KeyR': + e.preventDefault(); + restartGame(); + break; + } +} + +function handleCanvasClick(e) { + e.preventDefault(); + performAction(); +} + +function handleCanvasTouch(e) { + e.preventDefault(); + performAction(); +} + +function performAction() { + if (!gameState) return; + + if (gameState.game_over) { + restartGame(); + } else { + jump(); + } +} + +function jump() { + if (socket && socket.connected) { + socket.emit('player_action', { action: 'jump' }); + } +} + +function restartGame() { + if (socket && socket.connected) { + socket.emit('player_action', { action: 'restart' }); + } +} + +function updateConnectionStatus(connected) { + const statusElement = document.getElementById('connectionStatus'); + if (statusElement) { + statusElement.className = `connection-status ${connected ? 'connected' : 'disconnected'}`; + statusElement.textContent = connected ? 'Connected' : 'Disconnected'; + } +} + +function updateScoreDisplay() { + if (!gameState) return; + + const scoreElement = document.getElementById('score'); + const highScoreElement = document.getElementById('highScore'); + + if (scoreElement) { + scoreElement.textContent = String(Math.floor(gameState.score)).padStart(5, '0'); + } + + if (highScoreElement) { + highScoreElement.textContent = String(Math.floor(gameState.high_score)).padStart(5, '0'); + } +} + +function showError(message) { + console.error(message); + + const errorContainer = document.getElementById('errorContainer'); + if (errorContainer) { + errorContainer.textContent = message; + errorContainer.style.display = 'block'; + setTimeout(() => { + errorContainer.style.display = 'none'; + }, 5000); + } +} + +// Drawing functions +function clearCanvas() { + ctx.fillStyle = BG_COLOR; + ctx.fillRect(0, 0, canvas.width, canvas.height); +} + +function drawGround() { + if (!gameConfig) return; + + // Ground line + ctx.strokeStyle = ACCENT_COLOR; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(0, gameConfig.ground_y + 1); + ctx.lineTo(canvas.width, gameConfig.ground_y + 1); + ctx.stroke(); + + // Ground texture dots + ctx.fillStyle = ACCENT_COLOR; + for (let x = 0; x < canvas.width; x += 14) { + ctx.fillRect(x, gameConfig.ground_y + 3, 1, 1); + } +} + +function drawMascot() { + if (!gameConfig || !gameState || !imagesLoaded) return; + + const x = gameConfig.mascot_x; + const y = Math.round(gameState.mascot_y); + + let imageToUse = null; + + // Select appropriate image based on game state + if (gameState.game_over) { + imageToUse = ledImages.gameover; + } else if (!gameState.on_ground) { + imageToUse = ledImages.jump; + } else { + // Use current movement pattern + switch(currentMovePattern) { + case 1: + imageToUse = ledImages.move1; + break; + case 2: + imageToUse = ledImages.move2; + break; + case 3: + imageToUse = ledImages.move3; + break; + case 4: + imageToUse = ledImages.move4; + break; + default: + imageToUse = ledImages.move1; + } + } + + // Draw the LED character image if available + if (imageToUse) { + // Draw image at original size or scale if needed + // Assuming the PNGs are sized appropriately for the mascot + ctx.drawImage(imageToUse, x, y, gameConfig.mascot_width, gameConfig.mascot_height); + } else { + // Fallback: draw a simple rectangle if image not loaded + ctx.fillStyle = FG_COLOR; + ctx.fillRect(x, y, gameConfig.mascot_width, gameConfig.mascot_height); + + // Simple face + ctx.fillStyle = BG_COLOR; + ctx.fillRect(x + 10, y + 10, 5, 5); + ctx.fillRect(x + 29, y + 10, 5, 5); + } +} + +function drawObstacles() { + if (!gameState || !gameState.obstacles) return; + + for (const obstacle of gameState.obstacles) { + const x = Math.round(obstacle.x); + const y = Math.round(obstacle.y); + const h = obstacle.height; + + // Determine obstacle type based on height + if (h <= 32) { + // Small: Resistor + drawResistor(x, y - 10); + } else if (h <= 42) { + // Medium: Transistor + drawTransistor(x, y - 8); + } else { + // Large: Microchip + drawMicrochip(x, y); + } + } +} + +function drawResistor(x, y) { + ctx.fillStyle = '#8B4513'; // Brown color for resistor body + ctx.fillRect(x, y + 8, 20, 14); + + // Resistor bands + ctx.fillStyle = '#FF0000'; // Red band + ctx.fillRect(x + 3, y + 8, 3, 14); + ctx.fillStyle = '#FFFF00'; // Yellow band + ctx.fillRect(x + 9, y + 8, 3, 14); + ctx.fillStyle = '#00FF00'; // Green band + ctx.fillRect(x + 15, y + 8, 3, 14); + + // Wires + ctx.fillStyle = '#606060'; + ctx.fillRect(x - 3, y + 13, 5, 3); + ctx.fillRect(x + 18, y + 13, 5, 3); + + // Add vertical wires + ctx.fillRect(x - 1, y + 3, 2, 10); + ctx.fillRect(x + 19, y + 3, 2, 10); +} + +function drawTransistor(x, y) { + // Pixel art transistor (medium obstacle) + ctx.fillStyle = FG_COLOR; + + // Main body (TO-92 package style) + ctx.fillRect(x + 2, y + 2, 16, 24); + + // Rounded top + ctx.fillRect(x + 4, y, 12, 3); + ctx.fillRect(x + 6, y - 1, 8, 1); + + // Three legs + ctx.fillStyle = '#606060'; + ctx.fillRect(x + 4, y + 26, 3, 12); + ctx.fillRect(x + 9, y + 26, 3, 12); + ctx.fillRect(x + 14, y + 26, 3, 12); + + // Label + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(x + 5, y + 8, 10, 10); + ctx.fillStyle = FG_COLOR; + ctx.font = '12px monospace'; + ctx.fillText('T', x + 8, y + 16); +} + +function drawMicrochip(x, y) { + // Pixel art microchip/IC (large obstacle) + ctx.fillStyle = FG_COLOR; + + // Main IC body + ctx.fillRect(x + 2, y + 10, 14, 20); + + // Notch at top + ctx.fillStyle = BG_COLOR; + ctx.fillRect(x + 7, y + 10, 4, 3); + + // IC pins + ctx.fillStyle = '#606060'; + for (let i = 0; i < 4; i++) { + // Left side pins + ctx.fillRect(x - 2, y + 14 + i*4, 4, 2); + // Right side pins + ctx.fillRect(x + 14, y + 14 + i*4, 4, 2); + } + + // Label on IC + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(x + 4, y + 16, 10, 8); + ctx.fillStyle = FG_COLOR; + ctx.font = '6px monospace'; + ctx.fillText('IC', x + 6, y + 21); + ctx.fillText('555', x + 5, y + 23); +} + +function drawGameOver() { + if (!gameState || !gameState.game_over) return; + + // Semi-transparent overlay + ctx.fillStyle = 'rgba(245, 245, 245, 0.8)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Game Over text + ctx.fillStyle = FG_COLOR; + ctx.font = 'bold 32px Consolas, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2 - 30); + + // Score display + ctx.font = '20px Consolas, monospace'; + ctx.fillText(`Score: ${Math.floor(gameState.score)}`, canvas.width/2, canvas.height/2); + + // Blinking restart prompt + const currentTime = Date.now(); + if (currentTime - lastBlinkTime > 500) { + blinkState = !blinkState; + lastBlinkTime = currentTime; + } + + if (blinkState) { + ctx.font = '16px Consolas, monospace'; + ctx.fillStyle = ACCENT_COLOR; + ctx.fillText('Press SPACE to restart', canvas.width/2, canvas.height/2 + 35); + } +} + +function drawDebugInfo() { + // Optional: Display debug information + if (!gameState || !gameConfig) return; + + ctx.fillStyle = ACCENT_COLOR; + ctx.font = '10px monospace'; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + + const debugInfo = [ + `FPS: ${(1000 / 16).toFixed(0)}`, + `Speed: ${gameState.speed.toFixed(1)}`, + `Obstacles: ${gameState.obstacles.length}`, + `Y: ${gameState.mascot_y.toFixed(0)}`, + `Vel: ${gameState.velocity_y.toFixed(1)}`, + `Pattern: ${currentMovePattern}`, + `Images: ${imagesLoaded ? 'Loaded' : 'Loading...'}` + ]; + + debugInfo.forEach((info, i) => { + ctx.fillText(info, 10, 10 + i * 12); + }); +} + +// Main game rendering loop +function render() { + clearCanvas(); + drawGround(); + drawObstacles(); + drawMascot(); + drawGameOver(); + + // Uncomment for debug info + //drawDebugInfo(); +} + +function startGameLoop() { + function loop() { + render(); + animationId = requestAnimationFrame(loop); + } + loop(); +} + +function stopGameLoop() { + if (animationId) { + cancelAnimationFrame(animationId); + animationId = null; + } +} + +// Clean up on page unload +window.addEventListener('beforeunload', () => { + stopGameLoop(); + if (socket) { + socket.disconnect(); + } +}); \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif b/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif new file mode 100644 index 0000000..c36b1d5 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/game_over_state.gif differ diff --git a/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif b/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif new file mode 100644 index 0000000..e1e8ac9 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/game_play_state.gif differ diff --git a/examples/mascot-jump-game/assets/docs_assets/launch-app.png b/examples/mascot-jump-game/assets/docs_assets/launch-app.png new file mode 100644 index 0000000..5e18786 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/launch-app.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png b/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png new file mode 100644 index 0000000..5e48f19 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/led_character_animation.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png b/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png new file mode 100644 index 0000000..5470ca2 Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/led_matrix_frames.png differ diff --git a/examples/mascot-jump-game/assets/docs_assets/thumbnail.png b/examples/mascot-jump-game/assets/docs_assets/thumbnail.png new file mode 100644 index 0000000..285edbf Binary files /dev/null and b/examples/mascot-jump-game/assets/docs_assets/thumbnail.png differ diff --git a/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt b/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt new file mode 100644 index 0000000..a5ec031 --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/Open Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf b/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..548c15f Binary files /dev/null and b/examples/mascot-jump-game/assets/fonts/Open Sans/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt b/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt new file mode 100644 index 0000000..38d9750 --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/Roboto/OFL.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf b/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000..3a2d704 Binary files /dev/null and b/examples/mascot-jump-game/assets/fonts/Roboto/RobotoMono-VariableFont_wght.ttf differ diff --git a/examples/mascot-jump-game/assets/fonts/fonts.css b/examples/mascot-jump-game/assets/fonts/fonts.css new file mode 100644 index 0000000..19fab0d --- /dev/null +++ b/examples/mascot-jump-game/assets/fonts/fonts.css @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA + * + * SPDX-License-Identifier: MPL-2.0 + */ + +@font-face { + font-family: 'Roboto Mono'; + font-style: normal; + font-display: swap; + src: url('Roboto/RobotoMono-VariableFont_wght.ttf') format('truetype'); +} + +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + src: url('Open Sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype'); +} \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg b/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg new file mode 100644 index 0000000..c942003 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/RGB-Arduino-Logo_Color Inline Loop.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/favicon.png b/examples/mascot-jump-game/assets/img/favicon.png new file mode 100644 index 0000000..019a8cf Binary files /dev/null and b/examples/mascot-jump-game/assets/img/favicon.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png b/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png new file mode 100644 index 0000000..1ee456f Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_gameover.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_jump.png b/examples/mascot-jump-game/assets/img/ledcharacter_jump.png new file mode 100644 index 0000000..499df74 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_jump.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move1.png b/examples/mascot-jump-game/assets/img/ledcharacter_move1.png new file mode 100644 index 0000000..c140792 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move1.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move2.png b/examples/mascot-jump-game/assets/img/ledcharacter_move2.png new file mode 100644 index 0000000..ca4ce96 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move2.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move3.png b/examples/mascot-jump-game/assets/img/ledcharacter_move3.png new file mode 100644 index 0000000..1187ff4 Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move3.png differ diff --git a/examples/mascot-jump-game/assets/img/ledcharacter_move4.png b/examples/mascot-jump-game/assets/img/ledcharacter_move4.png new file mode 100644 index 0000000..c3dde6e Binary files /dev/null and b/examples/mascot-jump-game/assets/img/ledcharacter_move4.png differ diff --git a/examples/mascot-jump-game/assets/img/logo.svg b/examples/mascot-jump-game/assets/img/logo.svg new file mode 100644 index 0000000..d23ae68 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/logo.svg @@ -0,0 +1,19 @@ + + \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/img/logo.svg.license b/examples/mascot-jump-game/assets/img/logo.svg.license new file mode 100644 index 0000000..4b04544 --- /dev/null +++ b/examples/mascot-jump-game/assets/img/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA + +SPDX-License-Identifier: MPL-2.0 diff --git a/examples/mascot-jump-game/assets/index.html b/examples/mascot-jump-game/assets/index.html new file mode 100644 index 0000000..fa82627 --- /dev/null +++ b/examples/mascot-jump-game/assets/index.html @@ -0,0 +1,54 @@ + + + + + + + + Mascot Jumpe Game + + + + +
Connecting...
+ +
+
+

Mascot Jump Game

+ +
+ +
+ + +
+
+ Score: + 00000 + HI: + 00000 +
+ +
+ + SPACE or Jump + + + + R Restart + +
+
+ + +
+
+ + + + + diff --git a/examples/mascot-jump-game/assets/libs/socket.io.min.js b/examples/mascot-jump-game/assets/libs/socket.io.min.js new file mode 100644 index 0000000..530b185 --- /dev/null +++ b/examples/mascot-jump-game/assets/libs/socket.io.min.js @@ -0,0 +1,6 @@ +/*! + * Socket.IO v4.8.1 + * (c) 2014-2024 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); \ No newline at end of file diff --git a/examples/mascot-jump-game/assets/style.css b/examples/mascot-jump-game/assets/style.css new file mode 100644 index 0000000..f8a6855 --- /dev/null +++ b/examples/mascot-jump-game/assets/style.css @@ -0,0 +1,352 @@ +/* +SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA + +SPDX-License-Identifier: MPL-2.0 +*/ + +@import url('fonts/fonts.css'); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Consolas', 'Courier New', monospace; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + min-height: 100vh; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + user-select: none; + -webkit-user-select: none; +} + +.container { + width: 100%; + max-width: 900px; + padding: 20px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 0 10px; +} + +.arduino-text { + color: #008184; + font-family: "Roboto Mono", monospace; + font-size: 20px; + font-weight: 700; + margin: 0; + font-style: normal; + line-height: 170%; + letter-spacing: 2.4px; +} + +.arduino-logo { + height: 24px; + width: auto; +} + +/* Game container */ +.game-container { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1); + position: relative; +} + +/* Canvas */ +#gameCanvas { + display: block; + margin: 0 auto; + border: 2px solid #282828; + background: #f5f5f5; + border-radius: 8px; + cursor: pointer; + image-rendering: pixelated; + image-rendering: crisp-edges; + image-rendering: -moz-crisp-edges; + image-rendering: -webkit-crisp-edges; + transition: transform 0.2s ease; +} + +#gameCanvas:hover { + transform: scale(1.005); +} + +#gameCanvas:active { + transform: scale(0.995); +} + +/* Game info section */ +.game-info { + margin-top: 20px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 10px; +} + +/* Score display */ +.score-display { + display: flex; + align-items: baseline; + gap: 15px; + font-size: 24px; + color: #282828; +} + +.score-label, +.high-score-label { + font-weight: 500; + opacity: 0.7; + font-size: 20px; +} + +.score-value, +.high-score-value { + font-weight: 700; + font-variant-numeric: tabular-nums; + letter-spacing: 2px; +} + +.high-score-label { + margin-left: 10px; +} + +.high-score-value { + color: #008184; +} + +/* Controls info */ +.controls-info { + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #666; +} + +.control-item { + display: flex; + align-items: center; + gap: 5px; +} + +.key-icon { + display: inline-block; + padding: 3px 8px; + background: #f0f0f0; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + color: #282828; + box-shadow: 0 2px 0 #d0d0d0; + position: relative; + top: -1px; +} + +.control-divider { + color: #ccc; + font-size: 16px; +} + +/* Connection status */ +.connection-status { + position: fixed; + top: 20px; + right: 20px; + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + transition: all 0.3s ease; + z-index: 1000; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +.connection-status.connected { + background: #4caf50; + color: white; + animation: pulse 2s ease-out; +} + +.connection-status.disconnected { + background: #f44336; + color: white; + animation: blink 1s infinite; +} + +/* Error message */ +.error-message { + margin-top: 15px; + padding: 10px 15px; + background: #fff3cd; + border: 1px solid #ffc107; + border-radius: 6px; + color: #856404; + font-size: 14px; + text-align: center; + animation: slideIn 0.3s ease; +} + +/* Animations */ +@keyframes pulse { + 0% { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } + 50% { + box-shadow: 0 2px 20px rgba(76, 175, 80, 0.4); + } + 100% { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + } +} + +@keyframes blink { + 0%, 50%, 100% { + opacity: 1; + } + 25%, 75% { + opacity: 0.5; + } +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 768px) { + .container { + padding: 10px; + } + + .header { + margin-bottom: 15px; + } + + .arduino-text { + font-size: 24px; + } + + .game-container { + padding: 15px; + } + + #gameCanvas { + max-width: 100%; + height: auto; + } + + .game-info { + flex-direction: column; + gap: 15px; + align-items: center; + text-align: center; + } + + .score-display { + font-size: 20px; + } + + .controls-info { + flex-wrap: wrap; + justify-content: center; + } + + .connection-status { + top: 10px; + right: 10px; + padding: 6px 12px; + font-size: 10px; + } +} + +@media (max-width: 480px) { + .arduino-text { + font-size: 20px; + } + + .arduino-logo { + height: 24px; + } + + .score-display { + font-size: 18px; + gap: 10px; + } + + .score-label, + .high-score-label { + font-size: 16px; + } + + .controls-info { + font-size: 12px; + } + + .key-icon { + padding: 2px 6px; + font-size: 10px; + } +} + +@media (hover: none) and (pointer: coarse) { + #gameCanvas { + cursor: default; + } + + #gameCanvas:hover { + transform: none; + } + + #gameCanvas:active { + transform: scale(0.98); + } + + .key-icon { + display: none; + } + + .control-item:first-child::before { + content: "Tap to "; + } + + .control-divider, + .control-item:last-child { + display: none; + } +} + +@media print { + body { + background: white; + } + + .connection-status { + display: none; + } + + .game-container { + box-shadow: none; + border: 1px solid #ddd; + } +} diff --git a/examples/mascot-jump-game/python/main.py b/examples/mascot-jump-game/python/main.py new file mode 100644 index 0000000..0c26439 --- /dev/null +++ b/examples/mascot-jump-game/python/main.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +# +# SPDX-License-Identifier: MPL-2.0 + +from arduino.app_utils import * +from arduino.app_bricks.web_ui import WebUI +import time +import random +import threading +import json + +# Game Constants +GAME_WIDTH = 800 +GAME_HEIGHT = 300 +GROUND_Y = 240 +FPS = 60 + +MASCOT_WIDTH = 44 +MASCOT_HEIGHT = 48 +MASCOT_X = 80 + +OBSTACLE_WIDTH = 18 +MIN_OBSTACLE_HEIGHT = 28 # Resistor height +MID_OBSTACLE_HEIGHT = 38 # Transistor height +MAX_OBSTACLE_HEIGHT = 48 # Microchip height + +# Obstacle types with their specific heights +OBSTACLE_TYPES = [ + {'name': 'resistor', 'height': 28}, # Small + {'name': 'transistor', 'height': 38}, # Medium + {'name': 'microchip', 'height': 48} # Large +] + +JUMP_VELOCITY = -12.5 +GRAVITY = 0.65 +BASE_SPEED = 6.0 + +SPAWN_MIN_MS = 900 +SPAWN_MAX_MS = 1500 + +class GameState: + """Manages the complete game state""" + def __init__(self): + self.reset() + self.high_score = 0 + + def reset(self): + """Reset game to initial state""" + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + self.obstacles = [] + self.score = 0 + self.game_over = False + self.speed = BASE_SPEED + self.last_spawn_time = time.time() + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def update_physics(self, dt): + """Update mascot physics""" + if not self.on_ground: + self.velocity_y += GRAVITY * dt * 60 # Scale for 60 FPS base + self.mascot_y += self.velocity_y * dt * 60 + + # Ground collision + if self.mascot_y >= GROUND_Y - MASCOT_HEIGHT: + self.mascot_y = GROUND_Y - MASCOT_HEIGHT + self.velocity_y = 0.0 + self.on_ground = True + + def update_obstacles(self, dt): + """Update obstacle positions and spawn new ones""" + current_time = time.time() + + # Move existing obstacles + for obstacle in self.obstacles: + obstacle['x'] -= self.speed * dt * 60 + + # Remove offscreen obstacles + self.obstacles = [obs for obs in self.obstacles if obs['x'] > -OBSTACLE_WIDTH - 10] + + # Spawn new obstacles + if current_time - self.last_spawn_time >= self.next_spawn_delay: + self.spawn_obstacle() + self.last_spawn_time = current_time + self.next_spawn_delay = random.uniform(SPAWN_MIN_MS/1000, SPAWN_MAX_MS/1000) + + def spawn_obstacle(self): + """Create a new obstacle""" + # Randomly select an obstacle type + obstacle_type = random.choice(OBSTACLE_TYPES) + height = obstacle_type['height'] + + obstacle = { + 'x': GAME_WIDTH + 30, + 'y': GROUND_Y - height, + 'width': OBSTACLE_WIDTH, + 'height': height, + 'type': obstacle_type['name'] + } + self.obstacles.append(obstacle) + + def check_collisions(self): + """Check for mascot-obstacle collisions""" + mascot_rect = { + 'x': MASCOT_X, + 'y': self.mascot_y, + 'width': MASCOT_WIDTH, + 'height': MASCOT_HEIGHT + } + + for obstacle in self.obstacles: + if self.rectangles_intersect(mascot_rect, obstacle): + self.game_over = True + self.high_score = max(self.high_score, self.score) + return True + return False + + def rectangles_intersect(self, rect1, rect2): + """Check if two rectangles intersect""" + return not (rect1['x'] + rect1['width'] < rect2['x'] or + rect2['x'] + rect2['width'] < rect1['x'] or + rect1['y'] + rect1['height'] < rect2['y'] or + rect2['y'] + rect2['height'] < rect1['y']) + + def jump(self): + """Make the mascot jump if on ground""" + if self.on_ground and not self.game_over: + self.velocity_y = JUMP_VELOCITY + self.on_ground = False + return True + return False + + def to_dict(self): + """Serialize game state for transmission""" + return { + 'mascot_y': self.mascot_y, + 'velocity_y': self.velocity_y, + 'on_ground': self.on_ground, + 'obstacles': self.obstacles, + 'score': self.score, + 'high_score': self.high_score, + 'game_over': self.game_over, + 'speed': self.speed + } + +# Initialize game and UI +game = GameState() +ui = WebUI() + +# Game loop control +game_running = True +game_thread = None +game_started = False # Track if game has started + +def get_led_state(): + """Return current LED state for the LED matrix display""" + global game_started + + if game.game_over: + return "game_over" + elif not game_started and game.score == 0: + return "idle" + elif not game.on_ground: + return "jumping" + else: + return "running" + +def game_loop(): + """Main game loop running at ~60 FPS""" + global game_running, game_started + last_update = time.time() + + while game_running: + current_time = time.time() + dt = current_time - last_update + + if not game.game_over: + # Update game logic + game.update_physics(dt) + game.update_obstacles(dt) + game.check_collisions() + + # Update score (approximately 1 point per frame at 60 FPS) + game.score += int(60 * dt) + + # Increase difficulty + game.speed = BASE_SPEED + (game.score / 1500.0) + + # Send game state to all connected clients + ui.send_message('game_update', game.to_dict()) + + last_update = current_time + + # Target 60 FPS + sleep_time = max(0, (1/FPS) - (time.time() - current_time)) + time.sleep(sleep_time) + +def on_player_action(client_id, data): + """Handle player input actions""" + global game_started + action = data.get('action') + + if action == 'jump': + game_started = True # Game starts on first jump + if game.jump(): + ui.send_message('jump_confirmed', {'success': True}) + elif action == 'restart': + game.reset() + game_started = True # Game restarts + ui.send_message('game_reset', {'state': game.to_dict()}) + +def on_client_connected(client_id, data): + """Send initial game state when client connects""" + ui.send_message('game_init', { + 'state': game.to_dict(), + 'config': { + 'width': GAME_WIDTH, + 'height': GAME_HEIGHT, + 'ground_y': GROUND_Y, + 'mascot_x': MASCOT_X, + 'mascot_width': MASCOT_WIDTH, + 'mascot_height': MASCOT_HEIGHT + } + }) + +# Register WebSocket event handlers +ui.on_message('player_action', on_player_action) +ui.on_message('client_connected', on_client_connected) + +# Provide the LED state function to the Arduino sketch +Bridge.provide("get_led_state", get_led_state) + +# Start game loop in separate thread +game_thread = threading.Thread(target=game_loop, daemon=True) +game_thread.start() + +# Run the app +try: + App.run() +except KeyboardInterrupt: + game_running = False + if game_thread: + game_thread.join() \ No newline at end of file diff --git a/examples/mascot-jump-game/python/requirements.txt b/examples/mascot-jump-game/python/requirements.txt new file mode 100644 index 0000000..5873083 --- /dev/null +++ b/examples/mascot-jump-game/python/requirements.txt @@ -0,0 +1 @@ +pygame==2.6.1 diff --git a/examples/mascot-jump-game/sketch/game_frames.h b/examples/mascot-jump-game/sketch/game_frames.h new file mode 100644 index 0000000..f155c76 --- /dev/null +++ b/examples/mascot-jump-game/sketch/game_frames.h @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA + * + * SPDX-License-Identifier: MPL-2.0 + */ + +// LED Matrix frames for Mascot Jump Game +// 8x13 LED matrix patterns (104 values) +// Simplified mascot design with animated legs +// Values: 0 (off), 5 (medium), 7 (brightest) for 3-bit grayscale + +// Running animation frame 1 +uint8_t running_frame1[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,7,0,0,0,0,7,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 // Ground line +}; + +// Running animation frame 2 +uint8_t running_frame2[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Running animation frame 3 +uint8_t running_frame3[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,0,7,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Running animation frame 4 +uint8_t running_frame4[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 0,0,0,7,7,7,7,0,0,0,0,0,0, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,0,7,7,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Jumping frame - mascot in air with arms spread +uint8_t jumping[104] = { + 0,0,0,0,7,7,0,0,0,0,0,0,5, + 0,0,0,7,3,3,7,0,5,0,0,0,5, + 0,0,5,7,7,7,7,5,0,0,0,0,5, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,5, + 0,5,7,0,0,5,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Game over frame with X pattern +uint8_t game_over[104] = { + 0,0,7,7,7,0,0,0,0,0,4,0,4, + 0,7,7,3,7,7,0,7,0,0,0,4,0, + 0,7,3,7,7,7,7,0,0,0,4,0,4, + 0,7,7,7,7,7,7,0,0,0,0,0,0, + 0,0,7,7,7,7,7,7,7,0,0,0,0, + 0,7,0,0,7,0,0,0,0,0,0,0,0, + 0,0,0,0,7,0,0,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; + +// Idle/waiting frame same as running frame with neutral legs +uint8_t idle[104] = { + 0,0,0,0,7,7,0,0,0,0,4,0,0, + 0,0,0,7,7,7,7,0,0,0,4,4,4, + 0,0,0,7,3,3,7,0,0,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,5,7,7,7,7,7,7,5,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 0,0,0,7,0,0,7,0,0,0,0,0,0, + 7,7,7,7,7,7,7,7,7,7,7,7,7 +}; \ No newline at end of file diff --git a/examples/mascot-jump-game/sketch/sketch.ino b/examples/mascot-jump-game/sketch/sketch.ino new file mode 100644 index 0000000..80a729e --- /dev/null +++ b/examples/mascot-jump-game/sketch/sketch.ino @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright (C) 2025 ARDUINO SA +// +// SPDX-License-Identifier: MPL-2.0 + +#include +#include +#include "game_frames.h" + +Arduino_LED_Matrix matrix; + +// Animation state tracking +int animationFrame = 0; +unsigned long lastFrameTime = 0; +const unsigned long ANIMATION_DELAY = 200; // milliseconds between frames + +void setup() { + matrix.begin(); + matrix.setGrayscaleBits(3); // Use 3-bit grayscale (0-7 levels) + Bridge.begin(); +} + +void loop() { + String gameState; + bool ok = Bridge.call("get_led_state").result(gameState); + + if (ok) { + if (gameState == "running") { + // Animate between four running frames for leg movement + unsigned long currentTime = millis(); + if (currentTime - lastFrameTime > ANIMATION_DELAY) { + animationFrame = (animationFrame + 1) % 4; + lastFrameTime = currentTime; + } + + switch(animationFrame) { + case 0: + matrix.draw(running_frame1); + break; + case 1: + matrix.draw(running_frame2); + break; + case 2: + matrix.draw(running_frame3); + break; + case 3: + matrix.draw(running_frame4); + break; + } + + } else if (gameState == "jumping") { + // Show jumping frame when mascot is in the air + matrix.draw(jumping); + animationFrame = 0; // Reset animation frame + + } else if (gameState == "game_over") { + // Show game over pattern + matrix.draw(game_over); + animationFrame = 0; + + } else if (gameState == "idle") { + // Show idle frame when game has not started + matrix.draw(idle); + animationFrame = 0; + + } else { + // Default to idle if state is unknown + matrix.draw(idle); + } + } else { + // If communication fails, show idle + matrix.draw(idle); + } + + delay(50); // Update LED matrix at around 20 FPS +} \ No newline at end of file diff --git a/examples/mascot-jump-game/sketch/sketch.yaml b/examples/mascot-jump-game/sketch/sketch.yaml new file mode 100644 index 0000000..cb644bb --- /dev/null +++ b/examples/mascot-jump-game/sketch/sketch.yaml @@ -0,0 +1,11 @@ +profiles: + default: + fqbn: arduino:zephyr:unoq + platforms: + - platform: arduino:zephyr + libraries: + - MsgPack (0.4.2) + - DebugLog (0.8.4) + - ArxContainer (0.7.0) + - ArxTypeTraits (0.3.1) +default_profile: default \ No newline at end of file