feat: add code for asteroids game
This commit is contained in:
parent
8cce09584c
commit
f3a316053a
10 changed files with 310 additions and 1 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
venv/
|
||||||
|
__pycache__/
|
41
README.md
41
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
|
||||||
|
```
|
||||||
|
|
33
asteroid.py
Normal file
33
asteroid.py
Normal file
|
@ -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
|
51
asteroidfield.py
Normal file
51
asteroidfield.py
Normal file
|
@ -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)
|
32
circleshape.py
Normal file
32
circleshape.py
Normal file
|
@ -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
|
15
constants.py
Normal file
15
constants.py
Normal file
|
@ -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
|
64
main.py
Normal file
64
main.py
Normal file
|
@ -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()
|
58
player.py
Normal file
58
player.py
Normal file
|
@ -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
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pygame==2.6.1
|
14
shot.py
Normal file
14
shot.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue