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