feat: complete the game

This commit is contained in:
Stevan Freeborn
2026-05-22 09:34:56 -05:00
parent 6f9672beb5
commit 2ad2dfb6f8
9 changed files with 317 additions and 2 deletions
+38
View File
@@ -0,0 +1,38 @@
import random
import pygame
from circleshape import CircleShape
from constants import ASTEROID_MIN_RADIUS, LINE_WIDTH
from logger import log_event
class Asteroid(CircleShape):
def __init__(self, x: float, y: float, radius: float) -> None:
super().__init__(x, y, radius)
def draw(self, screen: pygame.Surface) -> None:
pygame.draw.circle(screen, "white", self.position, self.radius, LINE_WIDTH)
def update(self, dt: float) -> None:
self.position += self.velocity * dt
def split(self) -> None:
self.kill()
if self.radius <= ASTEROID_MIN_RADIUS:
return
log_event("asteroid_split")
angle = random.uniform(20, 50)
v1 = self.velocity.rotate(angle)
v2 = self.velocity.rotate(-angle)
r = self.radius - ASTEROID_MIN_RADIUS
a1 = Asteroid(self.position.x, self.position.y, r)
a1.velocity = v1 * 1.2
a2 = Asteroid(self.position.x, self.position.y, r)
a2.velocity = v2 * 1.2
+59
View File
@@ -0,0 +1,59 @@
import random
from collections.abc import Callable
import pygame
from asteroid import Asteroid
from constants import *
Edge = tuple[pygame.Vector2, Callable[[float], pygame.Vector2]]
class AsteroidField(pygame.sprite.Sprite):
containers: pygame.sprite.Group
edges: list[Edge] = [
(
pygame.Vector2(1, 0),
lambda y: pygame.Vector2(-ASTEROID_MAX_RADIUS, y * SCREEN_HEIGHT),
),
(
pygame.Vector2(-1, 0),
lambda y: pygame.Vector2(
SCREEN_WIDTH + ASTEROID_MAX_RADIUS, y * SCREEN_HEIGHT
),
),
(
pygame.Vector2(0, 1),
lambda x: pygame.Vector2(x * SCREEN_WIDTH, -ASTEROID_MAX_RADIUS),
),
(
pygame.Vector2(0, -1),
lambda x: pygame.Vector2(
x * SCREEN_WIDTH, SCREEN_HEIGHT + ASTEROID_MAX_RADIUS
),
),
]
def __init__(self) -> None:
pygame.sprite.Sprite.__init__(self, self.containers)
self.spawn_timer = 0.0
def spawn(
self, radius: float, position: pygame.Vector2, velocity: pygame.Vector2
) -> None:
asteroid = Asteroid(position.x, position.y, radius)
asteroid.velocity = velocity
def update(self, dt: float) -> None:
self.spawn_timer += dt
if self.spawn_timer > ASTEROID_SPAWN_RATE_SECONDS:
self.spawn_timer = 0
edge = random.choice(self.edges)
speed = random.randint(40, 100)
velocity = edge[0] * speed
velocity = velocity.rotate(random.randint(-30, 30))
position = edge[1](random.uniform(0, 1))
kind = random.randint(1, ASTEROID_KINDS)
self.spawn(ASTEROID_MIN_RADIUS * kind, position, velocity)
+28
View File
@@ -0,0 +1,28 @@
from typing import Self
import pygame
class CircleShape(pygame.sprite.Sprite):
containers: tuple[pygame.sprite.Group, ...]
def __init__(self, x: float, y: float, radius: float) -> None:
if hasattr(self, "containers"):
super().__init__(*self.containers)
else:
super().__init__()
self.position: pygame.Vector2 = pygame.Vector2(x, y)
self.velocity = pygame.Vector2(0, 0)
self.radius = radius
def draw(self, screen: pygame.Surface) -> None:
pass
def update(self, dt: float) -> None:
pass
def collides_with(self, other: Self) -> bool:
d = self.position.distance_to(other.position)
r = self.radius + other.radius
return d < r
+11 -1
View File
@@ -1,3 +1,13 @@
SCREEN_WIDTH = 1280 SCREEN_WIDTH = 1280
SCREEN_HEIGHT = 720 SCREEN_HEIGHT = 720
PLAYER_RADIUS = 20
LINE_WIDTH = 2
PLAYER_TURN_SPEED = 300
PLAYER_SPEED = 200
ASTEROID_MIN_RADIUS = 20
ASTEROID_KINDS = 3
ASTEROID_SPAWN_RATE_SECONDS = 0.8
ASTEROID_MAX_RADIUS = ASTEROID_MIN_RADIUS * ASTEROID_KINDS
SHOT_RADIUS = 5
PLAYER_SHOOT_SPEED = 500
PLAYER_SHOOT_COOLDOWN_SECONDS = 0.3
+56
View File
@@ -0,0 +1,56 @@
{"timestamp": "09:34:03.094", "elapsed_s": 4, "frame": 265, "type": "asteroid_shot"}
{"timestamp": "09:34:03.094", "elapsed_s": 4, "frame": 265, "type": "asteroid_split"}
{"timestamp": "09:34:05.593", "elapsed_s": 6, "frame": 421, "type": "asteroid_shot"}
{"timestamp": "09:34:08.949", "elapsed_s": 10, "frame": 632, "type": "asteroid_shot"}
{"timestamp": "09:34:08.949", "elapsed_s": 10, "frame": 632, "type": "asteroid_split"}
{"timestamp": "09:34:09.298", "elapsed_s": 10, "frame": 654, "type": "asteroid_shot"}
{"timestamp": "09:34:09.298", "elapsed_s": 10, "frame": 654, "type": "asteroid_split"}
{"timestamp": "09:34:09.667", "elapsed_s": 10, "frame": 677, "type": "asteroid_shot"}
{"timestamp": "09:34:10.815", "elapsed_s": 11, "frame": 749, "type": "asteroid_shot"}
{"timestamp": "09:34:10.816", "elapsed_s": 11, "frame": 749, "type": "asteroid_split"}
{"timestamp": "09:34:10.816", "elapsed_s": 11, "frame": 749, "type": "asteroid_shot"}
{"timestamp": "09:34:11.967", "elapsed_s": 13, "frame": 821, "type": "asteroid_shot"}
{"timestamp": "09:34:11.967", "elapsed_s": 13, "frame": 821, "type": "asteroid_split"}
{"timestamp": "09:34:13.025", "elapsed_s": 14, "frame": 887, "type": "asteroid_shot"}
{"timestamp": "09:34:13.025", "elapsed_s": 14, "frame": 887, "type": "asteroid_split"}
{"timestamp": "09:34:13.330", "elapsed_s": 14, "frame": 906, "type": "asteroid_shot"}
{"timestamp": "09:34:14.554", "elapsed_s": 15, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:15.018", "elapsed_s": 16, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:15.018", "elapsed_s": 16, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:15.354", "elapsed_s": 16, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:16.613", "elapsed_s": 17, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:17.905", "elapsed_s": 19, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:17.905", "elapsed_s": 19, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:18.729", "elapsed_s": 19, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:18.760", "elapsed_s": 19, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:18.760", "elapsed_s": 19, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:19.825", "elapsed_s": 20, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:19.841", "elapsed_s": 20, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:19.841", "elapsed_s": 20, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:20.112", "elapsed_s": 21, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:20.112", "elapsed_s": 21, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:20.144", "elapsed_s": 21, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:20.144", "elapsed_s": 21, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:20.527", "elapsed_s": 21, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:20.527", "elapsed_s": 21, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:21.234", "elapsed_s": 22, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:21.234", "elapsed_s": 22, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:21.314", "elapsed_s": 22, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:22.701", "elapsed_s": 23, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:22.701", "elapsed_s": 23, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:22.972", "elapsed_s": 24, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:22.972", "elapsed_s": 24, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:23.261", "elapsed_s": 24, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:23.647", "elapsed_s": 24, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:23.647", "elapsed_s": 24, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:24.028", "elapsed_s": 25, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:24.333", "elapsed_s": 25, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:24.813", "elapsed_s": 25, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:24.814", "elapsed_s": 25, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:25.435", "elapsed_s": 26, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:25.435", "elapsed_s": 26, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:25.916", "elapsed_s": 27, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:25.916", "elapsed_s": 27, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:25.995", "elapsed_s": 27, "frame": 961, "type": "asteroid_shot"}
{"timestamp": "09:34:25.995", "elapsed_s": 27, "frame": 961, "type": "asteroid_split"}
{"timestamp": "09:34:26.312", "elapsed_s": 27, "frame": 961, "type": "player_hit"}
+47 -1
View File
@@ -1,18 +1,43 @@
"""Entrypoint for asteroids game""" """Entrypoint for asteroids game"""
import sys
import pygame import pygame
from asteroid import Asteroid
from asteroidfield import AsteroidField
from constants import SCREEN_HEIGHT, SCREEN_WIDTH from constants import SCREEN_HEIGHT, SCREEN_WIDTH
from logger import log_state from logger import log_event, log_state
from player import Player
from shot import Shot
def main(): def main():
"""Main method for asteroids game""" """Main method for asteroids game"""
print(f"Starting Asteroids with pygame version: {pygame.version.ver}") print(f"Starting Asteroids with pygame version: {pygame.version.ver}")
print(f"Screen width: {SCREEN_WIDTH}") print(f"Screen width: {SCREEN_WIDTH}")
print(f"Screen height: {SCREEN_HEIGHT}") print(f"Screen height: {SCREEN_HEIGHT}")
pygame.init() pygame.init()
clock = pygame.time.Clock()
dt = 0.0
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
asteroids = pygame.sprite.Group()
updatable = pygame.sprite.Group()
drawable = pygame.sprite.Group()
shots = pygame.sprite.Group()
Player.containers = (updatable, drawable)
Asteroid.containers = (asteroids, updatable, drawable)
AsteroidField.containers = (updatable)
Shot.containers = (updatable, drawable, shots)
player = Player(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2)
AsteroidField()
while True: while True:
log_state() log_state()
@@ -20,8 +45,29 @@ def main():
if event.type == pygame.QUIT: if event.type == pygame.QUIT:
return return
for u in updatable:
u.update(dt)
for a in asteroids:
if a.collides_with(player):
log_event("player_hit")
print("Game over!")
sys.exit()
for s in shots:
if a.collides_with(s):
log_event("asteroid_shot")
s.kill()
a.split()
screen.fill("black") screen.fill("black")
for d in drawable:
d.draw(screen)
pygame.display.flip() pygame.display.flip()
dt = clock.tick(60) / 1000
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+60
View File
@@ -0,0 +1,60 @@
import pygame
from circleshape import CircleShape
from constants import (LINE_WIDTH, PLAYER_RADIUS, PLAYER_SHOOT_COOLDOWN_SECONDS, PLAYER_SHOOT_SPEED,
PLAYER_SPEED, PLAYER_TURN_SPEED, SHOT_RADIUS)
from shot import Shot
class Player(CircleShape):
def __init__(self, x, y) -> None:
super().__init__(x, y, PLAYER_RADIUS)
self.rotation = 0
self.cooldown = 0
def triangle(self) -> list[pygame.Vector2]:
forward = pygame.Vector2(0, 1).rotate(self.rotation)
right = pygame.Vector2(0, 1).rotate(self.rotation + 90) * self.radius / 1.5
a = self.position + forward * self.radius
b = self.position - forward * self.radius - right
c = self.position - forward * self.radius + right
return [a, b, c]
def draw(self, screen: pygame.Surface) -> None:
pygame.draw.polygon(screen, "white", self.triangle(), LINE_WIDTH)
def rotate(self, dt: float) -> None:
self.rotation += PLAYER_TURN_SPEED * dt
def update(self, dt: float) -> None:
self.cooldown -= dt
keys = pygame.key.get_pressed()
if keys[pygame.K_a]:
self.rotate(-dt)
if keys[pygame.K_d]:
self.rotate(dt)
if keys[pygame.K_s]:
self.move(dt)
if keys[pygame.K_w]:
self.move(-dt)
if keys[pygame.K_SPACE]:
self.shoot()
def move(self, dt: float) -> None:
unit_vector = pygame.Vector2(0, 1)
rotated_vector = unit_vector.rotate(self.rotation)
rotated_with_speed_vector = rotated_vector * PLAYER_SPEED * dt
self.position += rotated_with_speed_vector
def shoot(self):
if self.cooldown > 0:
return
s = Shot(self.position.x, self.position.y, SHOT_RADIUS)
s.velocity = pygame.Vector2(0, 1).rotate(self.rotation) * PLAYER_SHOOT_SPEED
self.cooldown = PLAYER_SHOOT_COOLDOWN_SECONDS
+4
View File
@@ -7,3 +7,7 @@ requires-python = ">=3.13"
dependencies = [ dependencies = [
"pygame==2.6.1", "pygame==2.6.1",
] ]
[tool.pyright]
include = ["."]
extraPaths = ["."]
+14
View File
@@ -0,0 +1,14 @@
import pygame
from circleshape import CircleShape
from constants import LINE_WIDTH
class Shot(CircleShape):
def __init__(self, x: float, y: float, radius: float) -> None:
super().__init__(x, y, radius)
def draw(self, screen: pygame.Surface) -> None:
pygame.draw.circle(screen, "white", self.position, self.radius, LINE_WIDTH)
def update(self, dt: float) -> None:
self.position += self.velocity * dt