#!/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()