From f3a316053a8d108d0519ec376281adb3f9557350 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Wed, 9 Oct 2024 14:12:32 +0100 Subject: [PATCH] feat: add code for asteroids game --- .gitignore | 2 ++ README.md | 41 ++++++++++++++++++++++++++++++- asteroid.py | 33 +++++++++++++++++++++++++ asteroidfield.py | 51 ++++++++++++++++++++++++++++++++++++++ circleshape.py | 32 ++++++++++++++++++++++++ constants.py | 15 ++++++++++++ main.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ player.py | 58 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + shot.py | 14 +++++++++++ 10 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 asteroid.py create mode 100644 asteroidfield.py create mode 100644 circleshape.py create mode 100644 constants.py create mode 100644 main.py create mode 100644 player.py create mode 100644 requirements.txt create mode 100644 shot.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93526df --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +__pycache__/ diff --git a/README.md b/README.md index 14fa2f2..bacdd5e 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# asteroids +# Asteroids + +`Asteroids` is a very simple asteroids game developed with Python and [Pygame](https://www.pygame.org/docs/). + +## Repository mirrors + +- **Code Flow:** https://codeflow.dananglin.me.uk/apollo/asteroids +- **GitHub:** https://github.com/dananglin/asteroids + +## Requirements + +To run the application you'll need a recent version of Python 3. +This application was built and tested with Python 3.12.3. + +You'll also need to install pygame. +The installation instructions are below. + +## How to run the game + +- Clone this repository to your local machine. + ``` + git clone https://github.com/dananglin/asteroids.git + cd asteroids + ``` + +- (Optional) create a virtual environment and activate it. + ``` + python3 -m venv venv + source venv/bin/activate + ``` + +- Install the dependencies listed in `requirements.txt`. + ``` + pip3 install -r requirements.txt + ``` + +- Launch the game. + ``` + python3 main.py + ``` diff --git a/asteroid.py b/asteroid.py new file mode 100644 index 0000000..ca67bed --- /dev/null +++ b/asteroid.py @@ -0,0 +1,33 @@ +import random +import pygame +from circleshape import CircleShape +from constants import ASTEROID_MIN_RADIUS + + +class Asteroid(CircleShape): + def __init__(self, x, y, radius) -> None: + super().__init__(x, y, radius) + + def draw(self, screen) -> None: + pygame.draw.circle(screen, "white", self.position, self.radius, 2) + + def update(self, dt: int): + self.position += self.velocity * dt + + def split(self): + self.kill() + + if self.radius <= ASTEROID_MIN_RADIUS: + return + + new_radius = self.radius - ASTEROID_MIN_RADIUS + + random_angle = random.uniform(20, 50) + vec_0 = self.velocity.rotate(random_angle) + vec_1 = self.velocity.rotate(-random_angle) + + new_asteroid_0 = Asteroid(self.position.x, self.position.y, new_radius) + new_asteroid_0.velocity = vec_0 * 1.2 + + new_asteroid_1 = Asteroid(self.position.x, self.position.y, new_radius) + new_asteroid_1.velocity = vec_1 * 1.2 diff --git a/asteroidfield.py b/asteroidfield.py new file mode 100644 index 0000000..da82d41 --- /dev/null +++ b/asteroidfield.py @@ -0,0 +1,51 @@ +import pygame +import random +from asteroid import Asteroid +from constants import * + + +class AsteroidField(pygame.sprite.Sprite): + edges = [ + [ + 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): + pygame.sprite.Sprite.__init__(self, self.containers) + self.spawn_timer = 0.0 + + def spawn(self, radius, position, velocity): + asteroid = Asteroid(position.x, position.y, radius) + asteroid.velocity = velocity + + def update(self, dt): + self.spawn_timer += dt + if self.spawn_timer > ASTEROID_SPAWN_RATE: + self.spawn_timer = 0 + + # spawn a new asteroid at a random edge + 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) diff --git a/circleshape.py b/circleshape.py new file mode 100644 index 0000000..200e532 --- /dev/null +++ b/circleshape.py @@ -0,0 +1,32 @@ +import pygame + + +# Base class for game objects +class CircleShape(pygame.sprite.Sprite): + def __init__(self, x: int, y: int, radius: int) -> None: + # we will be using this later + if hasattr(self, "containers"): + super().__init__(self.containers) + else: + super().__init__() + + self.position = pygame.Vector2(x, y) + self.velocity = pygame.Vector2(0, 0) + self.radius = radius + + def draw(self, screen): + # sub-classes must override + pass + + def update(self, dt): + # sub-classes must override + pass + + def is_colliding_with(self, obj: "CircleShape") -> bool: + distance = self.position.distance_to(obj.position) + radius_total = self.radius + obj.radius + + if distance <= radius_total: + return True + + return False diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..2af71c6 --- /dev/null +++ b/constants.py @@ -0,0 +1,15 @@ +SCREEN_WIDTH = 1280 +SCREEN_HEIGHT = 720 + +ASTEROID_MIN_RADIUS = 20 +ASTEROID_KINDS = 3 +ASTEROID_SPAWN_RATE = 0.8 # seconds +ASTEROID_MAX_RADIUS = ASTEROID_MIN_RADIUS * ASTEROID_KINDS + +PLAYER_RADIUS = 20 +PLAYER_TURN_SPEED = 300 +PLAYER_SPEED = 200 +PLAYER_SHOOT_SPEED = 500 +PLAYER_SHOOT_COOLDOWN = 0.3 + +SHOT_RADUIS = 3 diff --git a/main.py b/main.py new file mode 100644 index 0000000..e04e820 --- /dev/null +++ b/main.py @@ -0,0 +1,64 @@ +import sys +import pygame +from constants import SCREEN_WIDTH, SCREEN_HEIGHT +from player import Player +from asteroid import Asteroid +from asteroidfield import AsteroidField +from shot import Shot + + +def main(): + print("Starting asteroids!") + print(f"Screen width: {SCREEN_WIDTH}") + print(f"Screen height: {SCREEN_HEIGHT}") + + pygame.init() + + screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) + + clock = pygame.time.Clock() + dt = 0 + + updatable = pygame.sprite.Group() + drawable = pygame.sprite.Group() + asteroids = pygame.sprite.Group() + shots = pygame.sprite.Group() + + Player.containers = (updatable, drawable) + Asteroid.containers = (asteroids, updatable, drawable) + AsteroidField.containers = updatable + Shot.containers = (shots, updatable, drawable) + + playerObj = Player(SCREEN_WIDTH/2, SCREEN_HEIGHT/2) + asteroidFieldObj = AsteroidField() + + # this is the game loop + while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + return + + for obj in updatable: + obj.update(dt) + + for asteroid in asteroids: + if playerObj.is_colliding_with(asteroid): + print("Game over!") + sys.exit(0) + + for shot in shots: + if shot.is_colliding_with(asteroid): + asteroid.split() + shot.kill() + + screen.fill("black") + + for obj in drawable: + obj.draw(screen) + + pygame.display.flip() + dt = clock.tick(60) / 1000 + + +if __name__ == "__main__": + main() diff --git a/player.py b/player.py new file mode 100644 index 0000000..685f5b5 --- /dev/null +++ b/player.py @@ -0,0 +1,58 @@ +import pygame +from circleshape import CircleShape +from constants import * +from shot import Shot + + +class Player(CircleShape): + def __init__(self, x: int, y: int) -> None: + super().__init__(x, y, PLAYER_RADIUS) + + self.rotation = 0 + self.timer = 0 + + def triangle(self): + 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) -> None: + pygame.draw.polygon(screen, "white", self.triangle(), 2) + + def rotate(self, dt: int): + self.rotation += PLAYER_TURN_SPEED * dt + + def update(self, dt: int): + 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_w]: + self.move(dt) + + if keys[pygame.K_s]: + self.move(-dt) + + if keys[pygame.K_SPACE]: + if self.timer <= 0: + self.shoot() + + self.timer -= dt + + def move(self, dt: int): + forward = pygame.Vector2(0, 1).rotate(self.rotation) + self.position += forward * PLAYER_SPEED * dt + + def shoot(self): + player_shot = Shot(self.position.x, self.position.y) + player_shot.velocity = pygame.Vector2(0, 1).rotate( + self.rotation) * PLAYER_SHOOT_SPEED + self.timer = PLAYER_SHOOT_COOLDOWN diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5873083 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pygame==2.6.1 diff --git a/shot.py b/shot.py new file mode 100644 index 0000000..41aa764 --- /dev/null +++ b/shot.py @@ -0,0 +1,14 @@ +import pygame +from constants import SHOT_RADUIS +from circleshape import CircleShape + + +class Shot(CircleShape): + def __init__(self, x: int, y :int) -> None: + super().__init__(x, y, SHOT_RADUIS) + + def draw(self, screen) -> None: + pygame.draw.circle(screen, "white", self.position, self.radius, 2) + + def update(self, dt: int): + self.position += self.velocity * dt