diff --git a/Computer_Space/.feather_m4_express.test.only b/Computer_Space/.feather_m4_express.test.only new file mode 100644 index 000000000..e69de29bb diff --git a/Computer_Space/Computer_Space.ino b/Computer_Space/Computer_Space.ino new file mode 100644 index 000000000..6218dd724 --- /dev/null +++ b/Computer_Space/Computer_Space.ino @@ -0,0 +1,1120 @@ +// SPDX-FileCopyrightText: 2025 John Park and Claude +// +// SPDX-License-Identifier: MIT +// Computer Space simulation for Arduino + +// For Adafruit Feather M4 with OLED display + +#include +#include +#include +#include + +// Display setup +#define OLED_CS 5 +#define OLED_DC 6 +#define OLED_RESET 9 +Adafruit_SSD1305 display(128, 64, &SPI, OLED_DC, OLED_RESET, OLED_CS, 7000000UL); + +// Game constants +#define GAME_WIDTH 85 // Game area width - for 4:3 aspect ratio +#define GAME_HEIGHT 64 // Game area height +#define SCREEN_WIDTH 128 // Physical display width +#define SCREEN_HEIGHT 64 // Physical display height +#define GAME_X_OFFSET ((SCREEN_WIDTH - GAME_WIDTH) / 2) // Center the game area +#define GAME_Y_OFFSET 0 +#define WHITE 1 +#define BLACK 0 + +// Score positions - all inside game area +#define SCORE_Y_TOP 18 // Player score +#define SCORE_Y_MIDDLE 32 // Saucer score +#define SCORE_Y_BOTTOM 46 // Timer + +// Score fonts are 3x5 pixels +#define DIGIT_WIDTH 3 +#define DIGIT_HEIGHT 5 +#define DIGIT_SPACING 5 // Total width including spacing + +// Scores +int player_score = 0; // Starting scores at 0 as requested +int saucer_score = 0; +unsigned long game_timer = 0; // Starting from 00 and counting up to 99 +const unsigned long GAME_DURATION = 99000; // 99 seconds + +// Number of stars (similar to PDP-1 Spacewar!) +#define NUM_STARS 40 + +// Ship and saucer states +#define ALIVE 0 +#define EXPLODING 1 +#define RESPAWNING 2 +#define EXPLOSION_FRAMES 12 // Number of frames for explosion animation + +// Screen flash effect +bool screen_flash = false; +int flash_frames = 0; +#define FLASH_DURATION 3 // How long to flash the screen + +// Game objects +// Ship +float ship_x, ship_y; +float ship_vx, ship_vy; +float ship_rotation = 0; +float target_rotation = 0; +const float ship_thrust = 0.13; // 33% slower than original 0.2 +bool ship_thrusting = false; +int ship_state = ALIVE; +int ship_explosion_frame = 0; +unsigned long ship_respawn_time = 0; + +// Saucers (moving in formation) +float saucer1_x, saucer1_y; +float saucer2_x, saucer2_y; +float saucer_vertical_distance; // Distance between saucers (maintained) +unsigned long direction_change_time = 0; +int saucer1_state = ALIVE; +int saucer2_state = ALIVE; +int saucer1_explosion_frame = 0; +int saucer2_explosion_frame = 0; +unsigned long saucer1_respawn_time = 0; +unsigned long saucer2_respawn_time = 0; + +// Diagonal movement table +const int8_t MOVEMENT_TABLE[][2] = { + { 1, 0}, // Right + {-1, 0}, // Left + { 0, -1}, // Up + { 0, 1}, // Down + { 1, 1}, // Down-Right + {-1, -1}, // Up-Left + {-1, 1}, // Down-Left + { 1, -1} // Up-Right +}; +uint8_t current_movement = 0; + +// Bullets +bool player_bullet_active = false; +float player_bullet_x, player_bullet_y; +float player_bullet_vx, player_bullet_vy; +unsigned long player_bullet_expire = 0; +float player_bullet_tracking_factor = 0.08; // How much the bullet tracks ship rotation + +bool saucer1_bullet_active = false; +float saucer1_bullet_x, saucer1_bullet_y; +float saucer1_bullet_vx, saucer1_bullet_vy; +unsigned long saucer1_bullet_expire = 0; + +bool saucer2_bullet_active = false; +float saucer2_bullet_x, saucer2_bullet_y; +float saucer2_bullet_vx, saucer2_bullet_vy; +unsigned long saucer2_bullet_expire = 0; + +// Shooting cooldowns +unsigned long player_fire_cooldown = 0; +unsigned long saucer_fire_cooldown = 0; +const unsigned long PLAYER_COOLDOWN = 700; // milliseconds +const unsigned long SAUCER_COOLDOWN = 1500; // milliseconds +const unsigned long BULLET_LIFETIME = 2000; // 2 seconds as requested +const float BULLET_SPEED = 42.0; // Much faster than ship movement + +// Respawn timing +const unsigned long RESPAWN_DELAY = 2000; // 2 seconds + +// Game timing +unsigned long last_time = 0; +unsigned long auto_rotation_time = 0; +unsigned long auto_thrust_time = 0; +unsigned long game_start_time = 0; +unsigned long timer_update_time = 0; + +// Star coordinates +uint8_t stars[NUM_STARS][2]; + +// Background counter for star flicker (PDP-1 style) +uint8_t bg_counter = 0; + +// Digit patterns for 0-9 (3x5 pixels, 1 for pixel on, 0 for pixel off) +const uint8_t DIGITS[10][DIGIT_HEIGHT][DIGIT_WIDTH] = { + // 0 + { + {1, 1, 1}, + {1, 0, 1}, + {1, 0, 1}, + {1, 0, 1}, + {1, 1, 1} + }, + // 1 + { + {0, 1, 0}, + {0, 1, 0}, + {0, 1, 0}, + {0, 1, 0}, + {0, 1, 0} + }, + // 2 + { + {1, 1, 1}, + {0, 0, 1}, + {1, 1, 1}, + {1, 0, 0}, + {1, 1, 1} + }, + // 3 + { + {1, 1, 1}, + {0, 0, 1}, + {1, 1, 1}, + {0, 0, 1}, + {1, 1, 1} + }, + // 4 + { + {1, 0, 1}, + {1, 0, 1}, + {1, 1, 1}, + {0, 0, 1}, + {0, 0, 1} + }, + // 5 + { + {1, 1, 1}, + {1, 0, 0}, + {1, 1, 1}, + {0, 0, 1}, + {1, 1, 1} + }, + // 6 + { + {1, 1, 1}, + {1, 0, 0}, + {1, 1, 1}, + {1, 0, 1}, + {1, 1, 1} + }, + // 7 + { + {1, 1, 1}, + {0, 0, 1}, + {0, 0, 1}, + {0, 0, 1}, + {0, 0, 1} + }, + // 8 + { + {1, 1, 1}, + {1, 0, 1}, + {1, 1, 1}, + {1, 0, 1}, + {1, 1, 1} + }, + // 9 + { + {1, 1, 1}, + {1, 0, 1}, + {1, 1, 1}, + {0, 0, 1}, + {1, 1, 1} + } +}; + +// Explosion pattern data - expanding circle animation +const uint8_t EXPLOSION_RADIUS[] = {1, 2, 3, 4, 5, 6, 6, 5, 4, 3, 2, 1}; +const uint8_t EXPLOSION_POINTS = 8; // Points to draw in the circle + +// Function prototypes +void drawDigit(int digit, int x, int y); +void drawScore(int score, int x, int y); +void drawScores(); +void clearScoreArea(); +void drawStars(); +void clearShip(); +void clearSaucer(float x, float y); +bool checkCollision(float x1, float y1, float x2, float y2, float radius); +bool isAimedAtSaucer(float ship_x, float ship_y, float ship_rotation, float saucer_x, float saucer_y, float tolerance = 0.6); +void drawShip(float x, float y, float rotation); +void drawSaucer(float x, float y); +void updateSaucerBullet(bool &active, float &x, float &y, float &vx, float &vy, unsigned long &expire, float dt, unsigned long current_time); +float normalizeAngle(float angle); +float getAngleDifference(float a1, float a2); +float getAngleToTarget(float x1, float y1, float x2, float y2); +void updateSaucers(float dt, unsigned long current_time); +void updateTimer(); +void drawExplosion(float x, float y, int frame); +void respawnShip(); +void respawnSaucer(int saucer_num); +void triggerScreenFlash(); + +void setup() { + Serial.begin(9600); + + // Initialize display + if (!display.begin()) { + Serial.println("SSD1305 allocation failed"); + while (1); + } + + // Show intro text + display.clearDisplay(); + display.setTextSize(1); + display.setTextColor(WHITE); + display.setCursor(10, 10); + display.println("Computer Space"); + display.setCursor(32, 30); + display.println("PDP-1 Style"); + display.setCursor(32, 50); + display.println("Demo Mode"); + display.display(); + delay(2000); + + display.clearDisplay(); + + // Initialize stars (PDP-1 style - fewer stars) + randomSeed(analogRead(0)); + for (int i = 0; i < NUM_STARS; i++) { + stars[i][0] = GAME_X_OFFSET + random(0, GAME_WIDTH); + stars[i][1] = GAME_Y_OFFSET + random(0, GAME_HEIGHT); + } + + // Initialize game objects + respawnShip(); + + // Initialize saucers in formation (one above the other) + respawnSaucer(1); + respawnSaucer(2); + saucer_vertical_distance = saucer2_y - saucer1_y; + + direction_change_time = millis() + random(2000, 5000); + game_start_time = millis(); + timer_update_time = millis(); + + // Draw the stars and initial score display + drawStars(); + drawScores(); + display.display(); + + last_time = millis(); +} + +void loop() { + unsigned long current_time = millis(); + float dt = (current_time - last_time) / 1000.0; // Convert to seconds + last_time = current_time; + + // Cap dt to prevent large jumps + if (dt > 0.1) dt = 0.1; + + // Update timer + updateTimer(); + + // Handle screen flash effect + if (screen_flash) { + // Flash the entire screen white + if (flash_frames == 0) { + display.fillRect(GAME_X_OFFSET, GAME_Y_OFFSET, GAME_WIDTH, GAME_HEIGHT, WHITE); + display.display(); + delay(50); // Short delay to make flash visible + } + + flash_frames++; + if (flash_frames >= FLASH_DURATION) { + screen_flash = false; + flash_frames = 0; + // Clear screen after flash + display.fillRect(GAME_X_OFFSET, GAME_Y_OFFSET, GAME_WIDTH, GAME_HEIGHT, BLACK); + } + } + + // Clear previous objects + clearShip(); + clearSaucer(saucer1_x, saucer1_y); + clearSaucer(saucer2_x, saucer2_y); + + if (player_bullet_active) { + display.drawPixel(player_bullet_x, player_bullet_y, BLACK); + } + if (saucer1_bullet_active) { + display.drawPixel(saucer1_bullet_x, saucer1_bullet_y, BLACK); + } + if (saucer2_bullet_active) { + display.drawPixel(saucer2_bullet_x, saucer2_bullet_y, BLACK); + } + + // Process ship based on its state + if (ship_state == ALIVE) { + // Simulate AI decisions for the player ship (PDP-1 style) + if (current_time > auto_rotation_time) { + // Choose a target to aim at (50% chance for each saucer if they're alive) + if (saucer1_state == ALIVE && saucer2_state == ALIVE) { + if (random(100) > 50) { + target_rotation = getAngleToTarget(ship_x, ship_y, saucer1_x, saucer1_y); + } else { + target_rotation = getAngleToTarget(ship_x, ship_y, saucer2_x, saucer2_y); + } + } else if (saucer1_state == ALIVE) { + target_rotation = getAngleToTarget(ship_x, ship_y, saucer1_x, saucer1_y); + } else if (saucer2_state == ALIVE) { + target_rotation = getAngleToTarget(ship_x, ship_y, saucer2_x, saucer2_y); + } else { + // Both saucers exploding/respawning, just pick a random direction + target_rotation = random(0, 628) / 100.0; // Random angle between 0 and 2*PI + } + + // Add some randomness to make it less precise (PDP-1 style) + target_rotation += random(-30, 30) * 0.01; + target_rotation = normalizeAngle(target_rotation); + + auto_rotation_time = current_time + random(1000, 3000); + } + + if (current_time > auto_thrust_time) { + // Thrusting decision based on aim + float angle_diff = abs(getAngleDifference(ship_rotation, target_rotation)); + if (angle_diff < 0.2) { + // If aimed correctly, thrust for a while + ship_thrusting = true; + auto_thrust_time = current_time + random(300, 800); + } else { + // If not aimed correctly, don't thrust + ship_thrusting = false; + auto_thrust_time = current_time + random(100, 300); + } + } + + // Update ship rotation with smoother movement (PDP-1 style) + float angle_diff = getAngleDifference(ship_rotation, target_rotation); + if (abs(angle_diff) > 0.05) { + // Rotate at a maximum of 0.05 radians per frame for smoother movement + ship_rotation += (angle_diff > 0 ? 1 : -1) * min(abs(angle_diff), 0.05f); + ship_rotation = normalizeAngle(ship_rotation); + } + + // Apply thrust to ship ONLY in the direction it's pointing + if (ship_thrusting) { + // Since the rocket points up (negative y) when rotation=0, + // we need to adjust the thrust direction by 90 degrees + ship_vx += cos(ship_rotation - PI/2) * ship_thrust; + ship_vy += sin(ship_rotation - PI/2) * ship_thrust; + } + + // Update position based on velocity (inertia - PDP-1 style physics) + ship_x += ship_vx * dt; + ship_y += ship_vy * dt; + + // Apply very slight drag (just to prevent perpetual motion) + ship_vx *= 0.995; + ship_vy *= 0.995; + + // Wrap ship around game area + if (ship_x < GAME_X_OFFSET) { + ship_x = GAME_X_OFFSET + GAME_WIDTH - 1; + } else if (ship_x >= GAME_X_OFFSET + GAME_WIDTH) { + ship_x = GAME_X_OFFSET; + } + + if (ship_y < GAME_Y_OFFSET) { + ship_y = GAME_Y_OFFSET + GAME_HEIGHT - 1; + } else if (ship_y >= GAME_Y_OFFSET + GAME_HEIGHT) { + ship_y = GAME_Y_OFFSET; + } + + // Check for collision with saucers + if (saucer1_state == ALIVE && checkCollision(ship_x, ship_y, saucer1_x, saucer1_y, 8)) { + // Collided with saucer 1 + ship_state = EXPLODING; + ship_explosion_frame = 0; + saucer1_state = EXPLODING; + saucer1_explosion_frame = 0; + + // Increment saucer score + saucer_score++; + if (saucer_score > 9) saucer_score = 9; // Cap at 9 + + // Trigger screen flash + triggerScreenFlash(); + } + else if (saucer2_state == ALIVE && checkCollision(ship_x, ship_y, saucer2_x, saucer2_y, 8)) { + // Collided with saucer 2 + ship_state = EXPLODING; + ship_explosion_frame = 0; + saucer2_state = EXPLODING; + saucer2_explosion_frame = 0; + + // Increment saucer score + saucer_score++; + if (saucer_score > 9) saucer_score = 9; // Cap at 9 + + // Trigger screen flash + triggerScreenFlash(); + } + + // Player shooting - only when aimed at a saucer + if (!player_bullet_active && current_time > player_fire_cooldown && random(100) > 90) { + // Check if aimed at any saucer + bool can_fire = false; + + if (saucer1_state == ALIVE && isAimedAtSaucer(ship_x, ship_y, ship_rotation, saucer1_x, saucer1_y)) { + can_fire = true; + } else if (saucer2_state == ALIVE && isAimedAtSaucer(ship_x, ship_y, ship_rotation, saucer2_x, saucer2_y)) { + can_fire = true; + } + + if (can_fire) { + player_bullet_active = true; + player_bullet_x = ship_x + cos(ship_rotation - PI/2) * 6; + player_bullet_y = ship_y + sin(ship_rotation - PI/2) * 6; + player_bullet_vx = cos(ship_rotation - PI/2) * BULLET_SPEED; + player_bullet_vy = sin(ship_rotation - PI/2) * BULLET_SPEED; + player_bullet_expire = current_time + BULLET_LIFETIME; + player_fire_cooldown = current_time + PLAYER_COOLDOWN; + } + } + } + else if (ship_state == EXPLODING) { + // Ship is exploding, update animation + ship_explosion_frame++; + if (ship_explosion_frame >= EXPLOSION_FRAMES) { + ship_state = RESPAWNING; + ship_respawn_time = current_time + RESPAWN_DELAY; + } + } + else if (ship_state == RESPAWNING) { + // Check if it's time to respawn + if (current_time > ship_respawn_time) { + respawnShip(); + } + } + + // Update saucers based on state + if (saucer1_state == ALIVE && saucer2_state == ALIVE) { + // Update saucers (in formation, PDP-1 style) + updateSaucers(dt, current_time); + } + else if (saucer1_state == EXPLODING) { + // Saucer 1 is exploding, update animation + saucer1_explosion_frame++; + if (saucer1_explosion_frame >= EXPLOSION_FRAMES) { + saucer1_state = RESPAWNING; + saucer1_respawn_time = current_time + RESPAWN_DELAY; + } + } + else if (saucer1_state == RESPAWNING) { + // Check if it's time to respawn saucer 1 + if (current_time > saucer1_respawn_time) { + respawnSaucer(1); + + // If saucer 2 is also active, update the formation distance + if (saucer2_state == ALIVE) { + saucer_vertical_distance = saucer2_y - saucer1_y; + } + } + } + + if (saucer2_state == EXPLODING) { + // Saucer 2 is exploding, update animation + saucer2_explosion_frame++; + if (saucer2_explosion_frame >= EXPLOSION_FRAMES) { + saucer2_state = RESPAWNING; + saucer2_respawn_time = current_time + RESPAWN_DELAY; + } + } + else if (saucer2_state == RESPAWNING) { + // Check if it's time to respawn saucer 2 + if (current_time > saucer2_respawn_time) { + respawnSaucer(2); + + // If saucer 1 is also active, update the formation distance + if (saucer1_state == ALIVE) { + saucer_vertical_distance = saucer2_y - saucer1_y; + } + } + } + + // Update player bullet with tracking behavior + if (player_bullet_active) { + // If ship is alive, update bullet direction to track ship's rotation + if (ship_state == ALIVE) { + // Calculate the target velocity based on current ship rotation + float target_vx = cos(ship_rotation - PI/2) * BULLET_SPEED; + float target_vy = sin(ship_rotation - PI/2) * BULLET_SPEED; + + // Gradually adjust bullet velocity to track the ship's rotation + player_bullet_vx += (target_vx - player_bullet_vx) * player_bullet_tracking_factor; + player_bullet_vy += (target_vy - player_bullet_vy) * player_bullet_tracking_factor; + + // Normalize velocity to maintain constant speed + float speed = sqrt(player_bullet_vx * player_bullet_vx + player_bullet_vy * player_bullet_vy); + if (speed > 0) { + player_bullet_vx = (player_bullet_vx / speed) * BULLET_SPEED; + player_bullet_vy = (player_bullet_vy / speed) * BULLET_SPEED; + } + } + + // Update position + player_bullet_x += player_bullet_vx * dt; + player_bullet_y += player_bullet_vy * dt; + + // Wrap bullet within game area + if (player_bullet_x < GAME_X_OFFSET) { + player_bullet_x = GAME_X_OFFSET + GAME_WIDTH - 1; + } else if (player_bullet_x >= GAME_X_OFFSET + GAME_WIDTH) { + player_bullet_x = GAME_X_OFFSET; + } + + if (player_bullet_y < GAME_Y_OFFSET) { + player_bullet_y = GAME_Y_OFFSET + GAME_HEIGHT - 1; + } else if (player_bullet_y >= GAME_Y_OFFSET + GAME_HEIGHT) { + player_bullet_y = GAME_Y_OFFSET; + } + + // Check collisions with saucers + if (saucer1_state == ALIVE && checkCollision(player_bullet_x, player_bullet_y, saucer1_x, saucer1_y, 4)) { + player_bullet_active = false; + saucer1_state = EXPLODING; + saucer1_explosion_frame = 0; + + // Increment player score + player_score++; + if (player_score > 9) player_score = 9; // Cap at 9 + + // Trigger screen flash + triggerScreenFlash(); + } + else if (saucer2_state == ALIVE && checkCollision(player_bullet_x, player_bullet_y, saucer2_x, saucer2_y, 4)) { + player_bullet_active = false; + saucer2_state = EXPLODING; + saucer2_explosion_frame = 0; + + // Increment player score + player_score++; + if (player_score > 9) player_score = 9; // Cap at 9 + + // Trigger screen flash + triggerScreenFlash(); + } + + // Bullet lifetime + if (current_time > player_bullet_expire) { + player_bullet_active = false; + } + } + + // Update saucer bullets + if (saucer1_state == ALIVE) { + updateSaucerBullet(saucer1_bullet_active, saucer1_bullet_x, saucer1_bullet_y, + saucer1_bullet_vx, saucer1_bullet_vy, saucer1_bullet_expire, dt, current_time); + } + + if (saucer2_state == ALIVE) { + updateSaucerBullet(saucer2_bullet_active, saucer2_bullet_x, saucer2_bullet_y, + saucer2_bullet_vx, saucer2_bullet_vy, saucer2_bullet_expire, dt, current_time); + } + + // Saucer shooting (PDP-1 style random timing) + if (!saucer1_bullet_active && !saucer2_bullet_active && current_time > saucer_fire_cooldown) { + if (random(100) > 50 && saucer1_state == ALIVE && ship_state == ALIVE) { + // Saucer 1 shoots + saucer1_bullet_active = true; + saucer1_bullet_x = saucer1_x; + saucer1_bullet_y = saucer1_y; + + // Aim towards player with some randomness (PDP-1 style accuracy) + float angle = atan2(ship_y - saucer1_y, ship_x - saucer1_x) + + (random(-50, 50) / 100.0); + saucer1_bullet_vx = cos(angle) * BULLET_SPEED * 0.7; + saucer1_bullet_vy = sin(angle) * BULLET_SPEED * 0.7; + saucer1_bullet_expire = current_time + BULLET_LIFETIME; + } else if (saucer2_state == ALIVE && ship_state == ALIVE) { + // Saucer 2 shoots + saucer2_bullet_active = true; + saucer2_bullet_x = saucer2_x; + saucer2_bullet_y = saucer2_y; + + // Aim towards player with some randomness + float angle = atan2(ship_y - saucer2_y, ship_x - saucer2_x) + + (random(-50, 50) / 100.0); + saucer2_bullet_vx = cos(angle) * BULLET_SPEED * 0.7; + saucer2_bullet_vy = sin(angle) * BULLET_SPEED * 0.7; + saucer2_bullet_expire = current_time + BULLET_LIFETIME; + } + saucer_fire_cooldown = current_time + SAUCER_COOLDOWN; + } + + // Update background counter for star flicker effect (PDP-1 style) + bg_counter++; + + // Draw stars only on certain frames (PDP-1 style) + if (bg_counter % 2 == 0) { + drawStars(); + } + + // Only draw game objects if not in a screen flash + if (!screen_flash) { + // Draw game objects based on their state + if (ship_state == ALIVE) { + drawShip(ship_x, ship_y, ship_rotation); + } else if (ship_state == EXPLODING) { + drawExplosion(ship_x, ship_y, ship_explosion_frame); + } + + if (saucer1_state == ALIVE) { + drawSaucer(saucer1_x, saucer1_y); + } else if (saucer1_state == EXPLODING) { + drawExplosion(saucer1_x, saucer1_y, saucer1_explosion_frame); + } + + if (saucer2_state == ALIVE) { + drawSaucer(saucer2_x, saucer2_y); + } else if (saucer2_state == EXPLODING) { + drawExplosion(saucer2_x, saucer2_y, saucer2_explosion_frame); + } + + // Draw bullets + if (player_bullet_active) { + display.drawPixel(player_bullet_x, player_bullet_y, WHITE); + } + if (saucer1_bullet_active) { + display.drawPixel(saucer1_bullet_x, saucer1_bullet_y, WHITE); + } + if (saucer2_bullet_active) { + display.drawPixel(saucer2_bullet_x, saucer2_bullet_y, WHITE); + } + + // Clear score area and draw scores + clearScoreArea(); + drawScores(); + } + + // Update display + display.display(); + + // Small delay for performance + delay(20); // ~50 FPS - Similar to PDP-1 refresh rate +} + +// Trigger a white screen flash effect +void triggerScreenFlash() { + screen_flash = true; + flash_frames = 0; +} + +// Draw a PDP-1 style explosion animation +void drawExplosion(float x, float y, int frame) { + int radius = EXPLOSION_RADIUS[frame]; + + // Draw expanding circle with points + for (int i = 0; i < EXPLOSION_POINTS; i++) { + float angle = i * (2.0 * PI / EXPLOSION_POINTS); + int px = x + radius * cos(angle); + int py = y + radius * sin(angle); + + // Only draw if within game area + if (px >= GAME_X_OFFSET && px < GAME_X_OFFSET + GAME_WIDTH && + py >= GAME_Y_OFFSET && py < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(px, py, WHITE); + } + + // Add some randomly placed debris particles + if (frame > 2 && frame < 10) { + float debris_angle = angle + random(-30, 30) * 0.01; + float debris_dist = random(1, radius + 2); + int dx = x + debris_dist * cos(debris_angle); + int dy = y + debris_dist * sin(debris_angle); + + if (dx >= GAME_X_OFFSET && dx < GAME_X_OFFSET + GAME_WIDTH && + dy >= GAME_Y_OFFSET && dy < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(dx, dy, WHITE); + } + } + } +} + +// Respawn ship at a random position +void respawnShip() { + ship_x = GAME_X_OFFSET + random(GAME_WIDTH/4, 3*GAME_WIDTH/4); + ship_y = GAME_Y_OFFSET + random(GAME_HEIGHT/4, 3*GAME_HEIGHT/4); + ship_vx = ship_vy = 0; + ship_state = ALIVE; +} + +// Respawn saucer at a new position +void respawnSaucer(int saucer_num) { + if (saucer_num == 1) { + saucer1_x = GAME_X_OFFSET + random(10, GAME_WIDTH - 10); + saucer1_y = GAME_Y_OFFSET + random(10, GAME_HEIGHT/2 - 10); + saucer1_state = ALIVE; + } else { + saucer2_x = GAME_X_OFFSET + random(10, GAME_WIDTH - 10); + saucer2_y = GAME_Y_OFFSET + random(GAME_HEIGHT/2 + 10, GAME_HEIGHT - 10); + saucer2_state = ALIVE; + } +} + +// Draw a single digit using our custom font +void drawDigit(int digit, int x, int y) { + if (digit < 0 || digit > 9) return; // Only support 0-9 + + // Draw the digit pixel by pixel + for (int row = 0; row < DIGIT_HEIGHT; row++) { + for (int col = 0; col < DIGIT_WIDTH; col++) { + if (DIGITS[digit][row][col] == 1) { + display.drawPixel(x + col, y + row, WHITE); + } + } + } +} + +// Draw a score with multiple digits +void drawScore(int score, int x, int y) { + // Handle single and double digit scores + if (score < 10) { + // Single digit - add leading zero for timer + if (y == SCORE_Y_BOTTOM) { + // This is the timer, draw leading zero + drawDigit(0, x - DIGIT_SPACING, y); + drawDigit(score, x, y); + } else { + // Single digit score + drawDigit(score, x, y); + } + } else if (score < 100) { + // Double digits + int tens = score / 10; + int ones = score % 10; + drawDigit(tens, x - DIGIT_SPACING, y); + drawDigit(ones, x, y); + } +} + +// Update game timer - always counts up from 00 to 99 +void updateTimer() { + unsigned long current_time = millis(); + + // Update timer only once per second + if (current_time - timer_update_time >= 1000) { + timer_update_time = current_time; + + // Always increment timer + game_timer++; + if (game_timer >= 100) { + game_timer = 0; // Reset to 00 when reaching 100 + } + } +} + +// Clear the score area more thoroughly +void clearScoreArea() { + // Define score display areas + for (int i = 0; i < 3; i++) { + int y_pos; + switch (i) { + case 0: y_pos = SCORE_Y_TOP; break; + case 1: y_pos = SCORE_Y_MIDDLE; break; + case 2: y_pos = SCORE_Y_BOTTOM; break; + } + + // Clear area for double-digit score (including spacing) + for (int y = y_pos; y < y_pos + DIGIT_HEIGHT; y++) { + for (int x = GAME_X_OFFSET + GAME_WIDTH - 12; x < GAME_X_OFFSET + GAME_WIDTH; x++) { + display.drawPixel(x, y, BLACK); + } + } + } +} + +// Draw scores with custom font, positioned at edge of game area +void drawScores() { + // Position scores a few pixels from the right edge of game area + int score_x = GAME_X_OFFSET + GAME_WIDTH - 5; + + // Player score at top position + drawScore(player_score, score_x, SCORE_Y_TOP); + + // Saucer score at middle position + drawScore(saucer_score, score_x, SCORE_Y_MIDDLE); + + // Timer at bottom position (always show as 2 digits) + drawScore(game_timer, score_x, SCORE_Y_BOTTOM); +} + +// Update saucers using the PDP-1 style zig-zag formation movement +void updateSaucers(float dt, unsigned long current_time) { + // Check if it's time to change direction + if (current_time > direction_change_time) { + // Select a new movement pattern from the table + current_movement = random(0, 8); // 8 possible movement directions + + // Set next direction change time + direction_change_time = current_time + random(1500, 3000); + } + + // Apply current movement pattern + float speed_factor = 25.0 * dt; + float dx = MOVEMENT_TABLE[current_movement][0] * speed_factor; + float dy = MOVEMENT_TABLE[current_movement][1] * speed_factor; + + // Update saucer positions, maintaining their formation + saucer1_x += dx; + saucer1_y += dy; + + // Always keep saucer2 at the same position relative to saucer1 + saucer2_x = saucer1_x; + saucer2_y = saucer1_y + saucer_vertical_distance; + + // Wrap saucers around the game area + if (saucer1_x < GAME_X_OFFSET) { + saucer1_x = GAME_X_OFFSET + GAME_WIDTH - 1; + saucer2_x = saucer1_x; + } else if (saucer1_x >= GAME_X_OFFSET + GAME_WIDTH) { + saucer1_x = GAME_X_OFFSET; + saucer2_x = saucer1_x; + } + + if (saucer1_y < GAME_Y_OFFSET) { + saucer1_y = GAME_Y_OFFSET + GAME_HEIGHT - 1; + saucer2_y = saucer1_y + saucer_vertical_distance; + } else if (saucer1_y >= GAME_Y_OFFSET + GAME_HEIGHT) { + saucer1_y = GAME_Y_OFFSET; + saucer2_y = saucer1_y + saucer_vertical_distance; + } + + // Also wrap saucer2 if it goes off screen + if (saucer2_y >= GAME_Y_OFFSET + GAME_HEIGHT) { + saucer2_y = GAME_Y_OFFSET + (saucer2_y - (GAME_Y_OFFSET + GAME_HEIGHT)); + saucer1_y = saucer2_y - saucer_vertical_distance; + } else if (saucer2_y < GAME_Y_OFFSET) { + saucer2_y = GAME_Y_OFFSET + GAME_HEIGHT - (GAME_Y_OFFSET - saucer2_y); + saucer1_y = saucer2_y - saucer_vertical_distance; + } +} + +// Normalize angle to [0, 2π] +float normalizeAngle(float angle) { + while (angle < 0) angle += 2 * PI; + while (angle >= 2 * PI) angle -= 2 * PI; + return angle; +} + +// Get the shortest angle difference between two angles +float getAngleDifference(float a1, float a2) { + float diff = normalizeAngle(a2 - a1); + if (diff > PI) diff -= 2 * PI; + return diff; +} + +// Get angle from point 1 to point 2 +float getAngleToTarget(float x1, float y1, float x2, float y2) { + return atan2(y2 - y1, x2 - x1); +} + +// Helper function to update saucer bullets +void updateSaucerBullet(bool &active, float &x, float &y, float &vx, float &vy, + unsigned long &expire, float dt, unsigned long current_time) { + if (active) { + x += vx * dt; + y += vy * dt; + + // Wrap bullet within game area + if (x < GAME_X_OFFSET) { + x = GAME_X_OFFSET + GAME_WIDTH - 1; + } else if (x >= GAME_X_OFFSET + GAME_WIDTH) { + x = GAME_X_OFFSET; + } + + if (y < GAME_Y_OFFSET) { + y = GAME_Y_OFFSET + GAME_HEIGHT - 1; + } else if (y >= GAME_Y_OFFSET + GAME_HEIGHT) { + y = GAME_Y_OFFSET; + } + + // Check collision with player + if (ship_state == ALIVE && checkCollision(x, y, ship_x, ship_y, 4)) { + active = false; + ship_state = EXPLODING; + ship_explosion_frame = 0; + + // Increment saucer score + saucer_score++; + if (saucer_score > 9) saucer_score = 9; // Cap at 9 + + // Trigger screen flash + triggerScreenFlash(); + } + + // Bullet lifetime + if (current_time > expire) { + active = false; + } + } +} + +// Draw stars +void drawStars() { + for (int i = 0; i < NUM_STARS; i++) { + // Only draw stars within the game area + if (stars[i][0] >= GAME_X_OFFSET && stars[i][0] < GAME_X_OFFSET + GAME_WIDTH && + stars[i][1] >= GAME_Y_OFFSET && stars[i][1] < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(stars[i][0], stars[i][1], WHITE); + } + } +} + +// Improved collision detection using distance formula +bool checkCollision(float x1, float y1, float x2, float y2, float radius) { + return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)) < radius; +} + +// Check if ship is aimed at a saucer +bool isAimedAtSaucer(float ship_x, float ship_y, float ship_rotation, + float saucer_x, float saucer_y, float tolerance) { + // Calculate angle to saucer + float angle_to_saucer = atan2(saucer_y - ship_y, saucer_x - ship_x); + + // Calculate the difference between angles + float angle_diff = getAngleDifference(ship_rotation - PI/2, angle_to_saucer); + + // Check if angle difference is within tolerance + return abs(angle_diff) < tolerance; +} + +// Draw player ship as a dot pattern (PDP-1 style) +void drawShip(float x, float y, float rotation) { + int orig_x = (int)x; + int orig_y = (int)y; + + // Define the rocket shape points in its local coordinate system (PDP-1 style) + const int NUM_SHIP_POINTS = 15; + const int8_t points[][2] = { + {0, -6}, // Top point + {-2, -4}, {2, -4}, // Upper row + {-3, -2}, {3, -2}, // Mid-upper row + {-3, 0}, {3, 0}, // Middle row + {-2, 1}, {2, 1}, // Mid-lower row + {-4, 2}, {0, 2}, {4, 2}, // Lower body row + {-3, 4}, {3, 4} // Bottom fins + }; + + // Draw each point after rotation (PDP-1 style) + for (int i = 0; i < NUM_SHIP_POINTS; i++) { + // Rotate point + float rx = points[i][0] * cos(rotation) - points[i][1] * sin(rotation); + float ry = points[i][0] * sin(rotation) + points[i][1] * cos(rotation); + + // Calculate pixel position + int px = orig_x + (int)rx; + int py = orig_y + (int)ry; + + // Only draw if within game area + if (px >= GAME_X_OFFSET && px < GAME_X_OFFSET + GAME_WIDTH && + py >= GAME_Y_OFFSET && py < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(px, py, WHITE); + } + } + + // Add exhaust flame if thrusting - pointing from bottom of rocket + if (ship_thrusting) { + // Calculate bottom center position of the rocket + float bottom_x = 0 * cos(rotation) - 4 * sin(rotation); + float bottom_y = 0 * sin(rotation) + 4 * cos(rotation); + + // Add flame a bit below the bottom center + float flame_x = bottom_x + 2 * sin(rotation); // Perpendicular to rotation + float flame_y = bottom_y - 2 * cos(rotation); // Perpendicular to rotation + + int px = orig_x + (int)flame_x; + int py = orig_y + (int)flame_y; + + // Only draw if within game area + if (px >= GAME_X_OFFSET && px < GAME_X_OFFSET + GAME_WIDTH && + py >= GAME_Y_OFFSET && py < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(px, py, WHITE); + } + } +} + +// Helper to clear ship (draws in black) +void clearShip() { + // We'll use a larger area to ensure we cover the ship in any rotation + for (int dy = -8; dy <= 8; dy++) { + for (int dx = -8; dx <= 8; dx++) { + int x = ship_x + dx; + int y = ship_y + dy; + + // Only clear points within the game area + if (x >= GAME_X_OFFSET && x < GAME_X_OFFSET + GAME_WIDTH && + y >= GAME_Y_OFFSET && y < GAME_Y_OFFSET + GAME_HEIGHT) { + // Don't erase stars + bool is_star = false; + for (int i = 0; i < NUM_STARS; i++) { + if (stars[i][0] == x && stars[i][1] == y) { + is_star = true; + break; + } + } + if (!is_star) { + display.drawPixel(x, y, BLACK); + } + } + } + } +} + +// Draw saucer as a dot pattern (PDP-1 style) +void drawSaucer(float x, float y) { + int orig_x = (int)x; + int orig_y = (int)y; + + // Define saucer points (PDP-1 style dot pattern) + const int NUM_SAUCER_POINTS = 18; + const int8_t points[][2] = { + // Top dome + {-1, -2}, {1, -2}, + // Mid-top row + {-4, -1}, {-3, -1}, {3, -1}, {4, -1}, + // Middle row (widest) + {-5, 0}, {-2, 0}, {-1, 0}, {1, 0}, {2, 0}, {5, 0}, + // Mid-bottom row + {-4, 1}, {-3, 1}, {3, 1}, {4, 1}, + // Bottom + {-1, 2}, {1, 2} + }; + + // Draw each point + for (int i = 0; i < NUM_SAUCER_POINTS; i++) { + int px = orig_x + points[i][0]; + int py = orig_y + points[i][1]; + + // Only draw if within game area + if (px >= GAME_X_OFFSET && px < GAME_X_OFFSET + GAME_WIDTH && + py >= GAME_Y_OFFSET && py < GAME_Y_OFFSET + GAME_HEIGHT) { + display.drawPixel(px, py, WHITE); + } + } +} + +// Helper to clear saucer +void clearSaucer(float x, float y) { + // Clear a rectangle around the saucer + for (int dy = -6; dy <= 6; dy++) { + for (int dx = -6; dx <= 6; dx++) { + int px = x + dx; + int py = y + dy; + + // Only clear points within the game area + if (px >= GAME_X_OFFSET && px < GAME_X_OFFSET + GAME_WIDTH && + py >= GAME_Y_OFFSET && py < GAME_Y_OFFSET + GAME_HEIGHT) { + // Don't erase stars + bool is_star = false; + for (int i = 0; i < NUM_STARS; i++) { + if (stars[i][0] == px && stars[i][1] == py) { + is_star = true; + break; + } + } + if (!is_star) { + display.drawPixel(px, py, BLACK); + } + } + } + } +} \ No newline at end of file