307 lines
10 KiB
Python
307 lines
10 KiB
Python
#!/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()
|