Add complete Asteroids 1979 clone implementation in asteroids.py

This commit is contained in:
Ty
2026-06-20 17:34:27 +00:00
parent 2643abef23
commit 8ee1bb1c4b
+306
View File
@@ -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()