Adafruit_Learning_System_Gu.../Computer_Space/Computer_Space.ino
2025-04-30 10:21:18 -07:00

1120 lines
No EOL
34 KiB
C++

// SPDX-FileCopyrightText: 2025 John Park and Claude
//
// SPDX-License-Identifier: MIT
// Computer Space simulation for Arduino
// For Adafruit Feather M4 with OLED display
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1305.h>
// 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);
}
}
}
}
}