From 8ee1bb1c4bf99e4d23aaebf94b4d04f0e2238360 Mon Sep 17 00:00:00 2001 From: Ty Date: Sat, 20 Jun 2026 17:34:27 +0000 Subject: [PATCH] Add complete Asteroids 1979 clone implementation in asteroids.py --- asteroids.py | 306 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 asteroids.py diff --git a/asteroids.py b/asteroids.py new file mode 100644 index 0000000..9307e86 --- /dev/null +++ b/asteroids.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +""" +Asteroids (1979 Clone) +A Python + Pygame reimplementation of the classic 1979 Atari Asteroids arcade game. + +Controls: + Left/Right Arrow - Rotate + Up Arrow - Thrust + Space - Fire + H - Hyperspace + P - Pause + R - Restart after game over + ESC - Quit +""" + +import pygame +import math +import random +from pygame.math import Vector2 + +# Screen settings +WIDTH, HEIGHT = 800, 600 +FPS = 60 + +# Colors +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) + +class Ship: + def __init__(self): + self.pos = Vector2(WIDTH // 2, HEIGHT // 2) + self.vel = Vector2(0, 0) + self.angle = 0 # degrees, 0 = up + self.size = 12 + self.thrust = 0.2 + self.rotation_speed = 5 + self.invincible = 0 # frames of invincibility + + def rotate(self, direction): + self.angle += direction * self.rotation_speed + + def thrust_forward(self): + rad = math.radians(self.angle - 90) # adjust for pygame angle + self.vel.x += math.cos(rad) * self.thrust + self.vel.y += math.sin(rad) * self.thrust + + def shoot(self): + rad = math.radians(self.angle - 90) + bullet_vel = Vector2(math.cos(rad), math.sin(rad)) * 8 + return Bullet(self.pos + bullet_vel * 2, bullet_vel) + + def hyperspace(self): + self.pos = Vector2(random.randint(50, WIDTH-50), random.randint(50, HEIGHT-50)) + self.vel = Vector2(0, 0) + self.invincible = 90 # ~1.5 seconds + + def update(self): + self.pos += self.vel + # Wrap around screen + self.pos.x %= WIDTH + self.pos.y %= HEIGHT + # Apply slight drag (classic had almost none, but helps control) + self.vel *= 0.99 + if self.invincible > 0: + self.invincible -= 1 + + def draw(self, screen): + # Ship as triangle + rad = math.radians(self.angle - 90) + points = [] + for offset in [0, 140, 220]: + a = math.radians(self.angle - 90 + offset) + points.append((self.pos.x + math.cos(a) * self.size, + self.pos.y + math.sin(a) * self.size)) + pygame.draw.polygon(screen, WHITE, points, 1) + + # Thrust flame + if pygame.key.get_pressed()[pygame.K_UP]: + flame_points = [ + (self.pos.x + math.cos(rad) * self.size * 0.7, self.pos.y + math.sin(rad) * self.size * 0.7), + (self.pos.x + math.cos(rad + 0.3) * self.size * 1.8, self.pos.y + math.sin(rad + 0.3) * self.size * 1.8), + (self.pos.x + math.cos(rad - 0.3) * self.size * 1.8, self.pos.y + math.sin(rad - 0.3) * self.size * 1.8), + ] + pygame.draw.polygon(screen, WHITE, flame_points, 1) + + # Invincibility flash + if self.invincible > 0 and (self.invincible // 5) % 2 == 0: + pygame.draw.circle(screen, (100, 100, 255), (int(self.pos.x), int(self.pos.y)), self.size + 4, 1) + +class Asteroid: + def __init__(self, pos=None, size=3, vel=None): + self.size = size # 3=large, 2=medium, 1=small + self.radius = size * 12 + if pos is None: + self.pos = Vector2(random.randint(0, WIDTH), random.randint(0, HEIGHT)) + else: + self.pos = Vector2(pos) + if vel is None: + angle = random.uniform(0, 360) + speed = random.uniform(0.5, 1.5) * (4 - size) + self.vel = Vector2(math.cos(math.radians(angle)), math.sin(math.radians(angle))) * speed + else: + self.vel = Vector2(vel) + self.rotation = random.uniform(-1, 1) + self.angle = random.uniform(0, 360) + # Generate jagged polygon points + self.points = [] + for i in range(8): + a = math.radians(i * 45 + random.uniform(-15, 15)) + r = self.radius * random.uniform(0.7, 1.3) + self.points.append((math.cos(a) * r, math.sin(a) * r)) + + def update(self): + self.pos += self.vel + self.pos.x %= WIDTH + self.pos.y %= HEIGHT + self.angle += self.rotation + + def draw(self, screen): + points = [] + for px, py in self.points: + a = math.radians(self.angle) + x = self.pos.x + px * math.cos(a) - py * math.sin(a) + y = self.pos.y + px * math.sin(a) + py * math.cos(a) + points.append((x, y)) + pygame.draw.polygon(screen, WHITE, points, 1) + + def split(self): + if self.size > 1: + return [Asteroid(self.pos, self.size - 1, self.vel + Vector2(random.uniform(-1,1), random.uniform(-1,1))), + Asteroid(self.pos, self.size - 1, self.vel + Vector2(random.uniform(-1,1), random.uniform(-1,1)))] + return [] + +class Bullet: + def __init__(self, pos, vel): + self.pos = Vector2(pos) + self.vel = Vector2(vel) + self.lifetime = 45 # frames + + def update(self): + self.pos += self.vel + self.pos.x %= WIDTH + self.pos.y %= HEIGHT + self.lifetime -= 1 + + def draw(self, screen): + pygame.draw.circle(screen, WHITE, (int(self.pos.x), int(self.pos.y)), 2) + +def wrap_position(pos): + return Vector2(pos.x % WIDTH, pos.y % HEIGHT) + +def main(): + pygame.init() + screen = pygame.display.set_mode((WIDTH, HEIGHT)) + pygame.display.set_caption("Asteroids 1979 Clone") + clock = pygame.time.Clock() + font = pygame.font.SysFont(None, 36) + small_font = pygame.font.SysFont(None, 24) + + ship = Ship() + asteroids = [] + bullets = [] + score = 0 + lives = 3 + level = 1 + game_over = False + paused = False + + def spawn_asteroids(count): + asteroids.clear() + for _ in range(count): + # Avoid spawning too close to ship + while True: + a = Asteroid(size=3) + if a.pos.distance_to(ship.pos) > 150: + asteroids.append(a) + break + + spawn_asteroids(4) # Start with 4 large asteroids + + running = True + while running: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + running = False + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + running = False + if game_over and event.key == pygame.K_r: + # Restart + ship = Ship() + asteroids.clear() + bullets.clear() + score = 0 + lives = 3 + level = 1 + game_over = False + spawn_asteroids(4) + if not game_over and event.key == pygame.K_p: + paused = not paused + if not game_over and not paused and event.key == pygame.K_SPACE: + if len(bullets) < 6: # Limit bullets + bullets.append(ship.shoot()) + if not game_over and not paused and event.key == pygame.K_h: + ship.hyperspace() + + if game_over or paused: + # Draw paused or game over screen + screen.fill(BLACK) + if game_over: + text = font.render("GAME OVER", True, WHITE) + screen.blit(text, (WIDTH//2 - text.get_width()//2, HEIGHT//2 - 60)) + text2 = small_font.render("Press R to restart", True, WHITE) + screen.blit(text2, (WIDTH//2 - text2.get_width()//2, HEIGHT//2)) + else: + text = font.render("PAUSED", True, WHITE) + screen.blit(text, (WIDTH//2 - text.get_width()//2, HEIGHT//2)) + pygame.display.flip() + clock.tick(FPS) + continue + + keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: + ship.rotate(-1) + if keys[pygame.K_RIGHT]: + ship.rotate(1) + if keys[pygame.K_UP]: + ship.thrust_forward() + + # Update + ship.update() + for a in asteroids: + a.update() + for b in bullets[: ]: + b.update() + if b.lifetime <= 0: + bullets.remove(b) + + # Bullet - Asteroid collisions + for b in bullets[: ]: + hit = False + for a in asteroids[: ]: + if b.pos.distance_to(a.pos) < a.radius: + bullets.remove(b) + asteroids.remove(a) + new_asteroids = a.split() + asteroids.extend(new_asteroids) + # Scoring + if a.size == 3: + score += 20 + elif a.size == 2: + score += 50 + else: + score += 100 + hit = True + break + if hit: + break + + # Ship - Asteroid collisions + if ship.invincible <= 0: + for a in asteroids[: ]: + if ship.pos.distance_to(a.pos) < a.radius + ship.size * 0.7: + lives -= 1 + asteroids.remove(a) + new_asteroids = a.split() + asteroids.extend(new_asteroids) + if lives <= 0: + game_over = True + else: + ship = Ship() # Respawn + ship.invincible = 120 # 2 seconds + break + + # Level progression + if not asteroids: + level += 1 + spawn_asteroids(min(4 + level, 10)) + # Give player a short break + ship.invincible = 60 + + # Draw everything + screen.fill(BLACK) + + for a in asteroids: + a.draw(screen) + for b in bullets: + b.draw(screen) + if not game_over: + ship.draw(screen) + + # UI + score_text = font.render(f"Score: {score}", True, WHITE) + screen.blit(score_text, (20, 20)) + lives_text = small_font.render(f"Lives: {lives}", True, WHITE) + screen.blit(lives_text, (20, 60)) + level_text = small_font.render(f"Level: {level}", True, WHITE) + screen.blit(level_text, (WIDTH - 120, 20)) + + pygame.display.flip() + clock.tick(FPS) + + pygame.quit() + +if __name__ == "__main__": + main()