From d44d654d39fe351fc17ab11a898081863a5e9782 Mon Sep 17 00:00:00 2001 From: jedgarpark Date: Wed, 30 Apr 2025 10:21:18 -0700 Subject: [PATCH 1/2] first commit Computer Space attract --- Computer_Space/.feather_m4_express.test.only | 0 Computer_Space/Computer_Space.ino | 1120 ++++++++++++++++++ 2 files changed, 1120 insertions(+) create mode 100644 Computer_Space/.feather_m4_express.test.only create mode 100644 Computer_Space/Computer_Space.ino 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 From 198469c7f6c88a78b5f6ff66adb0dea0e5725a85 Mon Sep 17 00:00:00 2001 From: jedgarpark Date: Wed, 30 Apr 2025 10:50:37 -0700 Subject: [PATCH 2/2] added SSD1305 library to library.deps --- library.deps | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library.deps b/library.deps index 7e2ca5528..7668ac9b4 100644 --- a/library.deps +++ b/library.deps @@ -1 +1 @@ -depends=Adafruit ILI9341, Adafruit BusIO, SD, Adafruit NeoPixel, Adafruit VS1053 Library, Adafruit BluefruitLE nRF51, Adafruit seesaw Library, Ethernet, Stepper, Adafruit IO Arduino, FastLED, Adafruit LiquidCrystal, Adafruit SoftServo, TinyWireM, Adafruit AM radio library, WaveHC, Adafruit LED Backpack Library, MAX31850 OneWire, Adafruit VC0706 Serial Camera Library, RTClib, Adafruit SleepyDog Library, Adafruit Thermal Printer Library, Adafruit Zero I2S Library, Adafruit EPD, Adafruit SSD1351 library, Adafruit FONA Library, Adafruit Motor Shield V2 Library, Adafruit NeoMatrix, Adafruit Soundboard library, Adafruit Circuit Playground, ArduinoJson, Adafruit TCS34725, Adafruit Pixie, Adafruit GPS Library, TinyGPS, WiFi101, Adafruit DotStar, Adafruit Si7021 Library, Adafruit WS2801 Library, Mouse, Keyboard, Time, IRremote, Adafruit LSM9DS0 Library, Adafruit Arcada Library, MIDIUSB, PubSubClient, Adafruit LIS2MDL, Adafruit NeoPXL8, Adafruit MCP23017 Arduino Library, Adafruit MLX90640, LiquidCrystal, Adafruit NeoTrellis M4 Library, RGB matrix Panel, Adafruit MLX90614 Library, Adafruit RGB LCD Shield Library, MAX6675 library, Adafruit MP3, Adafruit Keypad, Adafruit Arcada GifDecoder, Keypad, Neosegment, Encoder, Adafruit TiCoServo, Adafruit Trellis Library, FauxmoESP, Adafruit LSM303 Accel, Adafruit LSM303DLH Mag, Adafruit LSM303DLHC, CapacitiveSensor, Adafruit Zero PDM Library, Adafruit DMA neopixel library, elapsedMillis, DST RTC, Adafruit SHARP Memory Display, Adafruit SPIFlash, BSEC Software Library, WiiChuck, Adafruit DPS310, Adafruit AHTX0, RotaryEncoder, Adafruit MCP9808 Library, LSM303, Adafruit Protomatter, Adafruit IS31FL3741 Library, Sensirion I2C SCD4x, Adafruit TestBed, Bounce2, Adafruit AHRS, Adafruit DRV2605 Library, STM32duino VL53L4CD, PicoDVI - Adafruit Fork, Adafruit MMA8451 Library, Adafruit TSC2007, GFX Library for Arduino, Adafruit PyCamera Library, Adafruit ADG72x, Adafruit BNO055, Adafruit SHT4x Library, Adafruit VCNL4200 Library, Adafruit GC9A01A, Adafruit DVI HSTX, Adafruit TLV320 I2S +depends=Adafruit SSD1305, Adafruit ILI9341, Adafruit BusIO, SD, Adafruit NeoPixel, Adafruit VS1053 Library, Adafruit BluefruitLE nRF51, Adafruit seesaw Library, Ethernet, Stepper, Adafruit IO Arduino, FastLED, Adafruit LiquidCrystal, Adafruit SoftServo, TinyWireM, Adafruit AM radio library, WaveHC, Adafruit LED Backpack Library, MAX31850 OneWire, Adafruit VC0706 Serial Camera Library, RTClib, Adafruit SleepyDog Library, Adafruit Thermal Printer Library, Adafruit Zero I2S Library, Adafruit EPD, Adafruit SSD1351 library, Adafruit FONA Library, Adafruit Motor Shield V2 Library, Adafruit NeoMatrix, Adafruit Soundboard library, Adafruit Circuit Playground, ArduinoJson, Adafruit TCS34725, Adafruit Pixie, Adafruit GPS Library, TinyGPS, WiFi101, Adafruit DotStar, Adafruit Si7021 Library, Adafruit WS2801 Library, Mouse, Keyboard, Time, IRremote, Adafruit LSM9DS0 Library, Adafruit Arcada Library, MIDIUSB, PubSubClient, Adafruit LIS2MDL, Adafruit NeoPXL8, Adafruit MCP23017 Arduino Library, Adafruit MLX90640, LiquidCrystal, Adafruit NeoTrellis M4 Library, RGB matrix Panel, Adafruit MLX90614 Library, Adafruit RGB LCD Shield Library, MAX6675 library, Adafruit MP3, Adafruit Keypad, Adafruit Arcada GifDecoder, Keypad, Neosegment, Encoder, Adafruit TiCoServo, Adafruit Trellis Library, FauxmoESP, Adafruit LSM303 Accel, Adafruit LSM303DLH Mag, Adafruit LSM303DLHC, CapacitiveSensor, Adafruit Zero PDM Library, Adafruit DMA neopixel library, elapsedMillis, DST RTC, Adafruit SHARP Memory Display, Adafruit SPIFlash, BSEC Software Library, WiiChuck, Adafruit DPS310, Adafruit AHTX0, RotaryEncoder, Adafruit MCP9808 Library, LSM303, Adafruit Protomatter, Adafruit IS31FL3741 Library, Sensirion I2C SCD4x, Adafruit TestBed, Bounce2, Adafruit AHRS, Adafruit DRV2605 Library, STM32duino VL53L4CD, PicoDVI - Adafruit Fork, Adafruit MMA8451 Library, Adafruit TSC2007, GFX Library for Arduino, Adafruit PyCamera Library, Adafruit ADG72x, Adafruit BNO055, Adafruit SHT4x Library, Adafruit VCNL4200 Library, Adafruit GC9A01A, Adafruit DVI HSTX, Adafruit TLV320 I2S