mirror of
https://github.com/sourabhv/FlapPyBird.git
synced 2024-11-25 16:35:52 +08:00
Improve code by adding a generic entity object
This commit is contained in:
parent
d3100119f2
commit
4960be1759
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ build
|
||||
.python-version
|
||||
*.egg-info
|
||||
.vscode
|
||||
.DS_Store
|
||||
|
@ -1,10 +1,10 @@
|
||||
from .background import Background
|
||||
from .entity import Entity
|
||||
from .floor import Floor
|
||||
from .game_over import GameOver
|
||||
from .pipe import Pipe, Pipes
|
||||
from .player import Player, PlayerMode
|
||||
from .score import Score
|
||||
from .sprite import Sprite
|
||||
from .welcome_message import WelcomeMessage
|
||||
|
||||
__all__ = [
|
||||
@ -14,6 +14,6 @@ __all__ = [
|
||||
"Pipes",
|
||||
"Player",
|
||||
"Score",
|
||||
"Sprite",
|
||||
"Entity",
|
||||
"WelcomeMessage",
|
||||
]
|
7
src/entities/background.py
Normal file
7
src/entities/background.py
Normal file
@ -0,0 +1,7 @@
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class Background(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(config, config.images.background)
|
77
src/entities/entity.py
Normal file
77
src/entities/entity.py
Normal file
@ -0,0 +1,77 @@
|
||||
from typing import Optional
|
||||
|
||||
import pygame
|
||||
|
||||
from ..utils import GameConfig, get_hit_mask, pixel_collision
|
||||
|
||||
|
||||
class Entity:
|
||||
def __init__(
|
||||
self,
|
||||
config: GameConfig,
|
||||
image: Optional[pygame.Surface] = None,
|
||||
x=0,
|
||||
y=0,
|
||||
w: int = None,
|
||||
h: int = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.image = image
|
||||
self.hit_mask = get_hit_mask(image) if image else None
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w or (image.get_width() if image else 0)
|
||||
self.h = h or (image.get_height() if image else 0)
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def update_image(
|
||||
self, image: pygame.Surface, w: int = None, h: int = None
|
||||
) -> None:
|
||||
self.image = image
|
||||
self.hit_mask = get_hit_mask(image)
|
||||
self.w = w or (image.get_width() if image else 0)
|
||||
self.h = h or (image.get_height() if image else 0)
|
||||
|
||||
@property
|
||||
def cx(self) -> float:
|
||||
return self.x + self.w / 2
|
||||
|
||||
@property
|
||||
def cy(self) -> float:
|
||||
return self.y + self.h / 2
|
||||
|
||||
@property
|
||||
def rect(self) -> pygame.Rect:
|
||||
return pygame.Rect(self.x, self.y, self.w, self.h)
|
||||
|
||||
def collide(self, other) -> bool:
|
||||
if not self.hit_mask or not other.hit_mask:
|
||||
return self.rect.colliderect(other.rect)
|
||||
return pixel_collision(
|
||||
self.rect, other.rect, self.hit_mask, other.hit_mask
|
||||
)
|
||||
|
||||
def tick(self) -> None:
|
||||
self.draw()
|
||||
if self.config.debug:
|
||||
pygame.draw.rect(
|
||||
self.config.screen,
|
||||
(255, 0, 0),
|
||||
self.rect,
|
||||
1,
|
||||
)
|
||||
# write x and y at top of rect
|
||||
font = pygame.font.SysFont("Arial", 13, True)
|
||||
text = font.render(f"{self.x}, {self.y}", True, (255, 255, 255))
|
||||
self.config.screen.blit(
|
||||
text,
|
||||
(
|
||||
self.x + self.w / 2 - text.get_width() / 2,
|
||||
self.y - text.get_height(),
|
||||
),
|
||||
)
|
||||
|
||||
def draw(self) -> None:
|
||||
if self.image:
|
||||
self.config.screen.blit(self.image, (self.x, self.y))
|
21
src/entities/floor.py
Normal file
21
src/entities/floor.py
Normal file
@ -0,0 +1,21 @@
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class Floor(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(
|
||||
config, config.images.base, 0, config.window.viewport_height
|
||||
)
|
||||
self.vel_x = 4
|
||||
self.x_extra = (
|
||||
config.images.base.get_width()
|
||||
- config.images.background.get_width()
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.vel_x = 0
|
||||
|
||||
def draw(self) -> None:
|
||||
self.x = -((-self.x + self.vel_x) % self.x_extra)
|
||||
super().draw()
|
12
src/entities/game_over.py
Normal file
12
src/entities/game_over.py
Normal file
@ -0,0 +1,12 @@
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class GameOver(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(
|
||||
config=config,
|
||||
image=config.images.game_over,
|
||||
x=(config.window.width - config.images.game_over.get_width()) // 2,
|
||||
y=int(config.window.height * 0.2),
|
||||
)
|
104
src/entities/pipe.py
Normal file
104
src/entities/pipe.py
Normal file
@ -0,0 +1,104 @@
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class Pipe(Entity):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.vel_x = -5
|
||||
|
||||
def draw(self) -> None:
|
||||
self.x += self.vel_x
|
||||
super().draw()
|
||||
|
||||
|
||||
class Pipes(Entity):
|
||||
upper: List[Pipe]
|
||||
lower: List[Pipe]
|
||||
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(config)
|
||||
self.pipe_gap = 120
|
||||
self.top = 0
|
||||
self.bottom = self.config.window.viewport_height
|
||||
self.upper = []
|
||||
self.lower = []
|
||||
self.spawn_initial_pipes()
|
||||
|
||||
def tick(self) -> None:
|
||||
if self.can_spawn_pipes():
|
||||
self.spawn_new_pipes()
|
||||
self.remove_old_pipes()
|
||||
|
||||
for up_pipe, low_pipe in zip(self.upper, self.lower):
|
||||
up_pipe.tick()
|
||||
low_pipe.tick()
|
||||
|
||||
def stop(self) -> None:
|
||||
for pipe in self.upper + self.lower:
|
||||
pipe.vel_x = 0
|
||||
|
||||
def can_spawn_pipes(self) -> bool:
|
||||
last = self.upper[-1]
|
||||
if not last:
|
||||
return True
|
||||
|
||||
return self.config.window.width - (last.x + last.w) > last.w * 2.5
|
||||
|
||||
def spawn_new_pipes(self):
|
||||
# add new pipe when first pipe is about to touch left of screen
|
||||
upper, lower = self.make_random_pipes()
|
||||
self.upper.append(upper)
|
||||
self.lower.append(lower)
|
||||
|
||||
def remove_old_pipes(self):
|
||||
# remove first pipe if its out of the screen
|
||||
for pipe in self.upper:
|
||||
if pipe.x < -pipe.w:
|
||||
self.upper.remove(pipe)
|
||||
|
||||
for pipe in self.lower:
|
||||
if pipe.x < -pipe.w:
|
||||
self.lower.remove(pipe)
|
||||
|
||||
def spawn_initial_pipes(self):
|
||||
upper_1, lower_1 = self.make_random_pipes()
|
||||
upper_1.x = self.config.window.width + upper_1.w * 3
|
||||
lower_1.x = self.config.window.width + upper_1.w * 3
|
||||
self.upper.append(upper_1)
|
||||
self.lower.append(lower_1)
|
||||
|
||||
upper_2, lower_2 = self.make_random_pipes()
|
||||
upper_2.x = upper_1.x + upper_1.w * 3.5
|
||||
lower_2.x = upper_1.x + upper_1.w * 3.5
|
||||
self.upper.append(upper_2)
|
||||
self.lower.append(lower_2)
|
||||
|
||||
def make_random_pipes(self):
|
||||
"""returns a randomly generated pipe"""
|
||||
# y of gap between upper and lower pipe
|
||||
base_y = self.config.window.viewport_height
|
||||
|
||||
gap_y = random.randrange(0, int(base_y * 0.6 - self.pipe_gap))
|
||||
gap_y += int(base_y * 0.2)
|
||||
pipe_height = self.config.images.pipe[0].get_height()
|
||||
pipe_x = self.config.window.width + 10
|
||||
|
||||
upper_pipe = Pipe(
|
||||
self.config,
|
||||
self.config.images.pipe[0],
|
||||
pipe_x,
|
||||
gap_y - pipe_height,
|
||||
)
|
||||
|
||||
lower_pipe = Pipe(
|
||||
self.config,
|
||||
self.config.images.pipe[1],
|
||||
pipe_x,
|
||||
gap_y + self.pipe_gap,
|
||||
)
|
||||
|
||||
return upper_pipe, lower_pipe
|
@ -3,10 +3,10 @@ from itertools import cycle
|
||||
|
||||
import pygame
|
||||
|
||||
from ..utils import clamp, pixel_collision
|
||||
from ..utils import GameConfig, clamp
|
||||
from .entity import Entity
|
||||
from .floor import Floor
|
||||
from .pipe import Pipe, Pipes
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class PlayerMode(Enum):
|
||||
@ -15,39 +15,34 @@ class PlayerMode(Enum):
|
||||
CRASH = "CRASH"
|
||||
|
||||
|
||||
class Player(Sprite):
|
||||
def setup(self) -> None:
|
||||
class Player(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
image = config.images.player[0]
|
||||
x = int(config.window.width * 0.2)
|
||||
y = int((config.window.height - image.get_height()) / 2)
|
||||
super().__init__(config, image, x, y)
|
||||
self.min_y = -2 * self.h
|
||||
self.max_y = config.window.viewport_height - self.h * 0.75
|
||||
self.img_idx = 0
|
||||
self.img_gen = cycle([0, 1, 2, 1])
|
||||
self.frame = 0
|
||||
self.crashed = False
|
||||
self.crash_entity = None
|
||||
self.width = self.images.player[0].get_width()
|
||||
self.height = self.images.player[0].get_height()
|
||||
self.reset_pos()
|
||||
self.set_mode(PlayerMode.SHM)
|
||||
|
||||
def set_mode(self, mode: PlayerMode) -> None:
|
||||
self.mode = mode
|
||||
if mode == PlayerMode.NORMAL:
|
||||
self.reset_vals_normal()
|
||||
self.sounds.wing.play()
|
||||
self.config.sounds.wing.play()
|
||||
elif mode == PlayerMode.SHM:
|
||||
self.reset_vals_shm()
|
||||
elif mode == PlayerMode.CRASH:
|
||||
self.sounds.hit.play()
|
||||
self.config.sounds.hit.play()
|
||||
if self.crash_entity == "pipe":
|
||||
self.sounds.die.play()
|
||||
self.config.sounds.die.play()
|
||||
self.reset_vals_crash()
|
||||
|
||||
def reset_pos(self) -> None:
|
||||
self.x = int(self.window.width * 0.2)
|
||||
self.y = int(
|
||||
(self.window.height - self.images.player[0].get_height()) / 2
|
||||
)
|
||||
self.mid_x = self.x + self.width / 2
|
||||
self.mid_y = self.y + self.height / 2
|
||||
|
||||
def reset_vals_normal(self) -> None:
|
||||
self.vel_y = -9 # player's velocity along Y axis
|
||||
self.max_vel_y = 10 # max vel along Y, max descend speed
|
||||
@ -82,10 +77,13 @@ class Player(Sprite):
|
||||
self.max_vel_y = 15
|
||||
self.vel_rot = -8
|
||||
|
||||
def update_img_idx(self):
|
||||
def update_image(self):
|
||||
self.frame += 1
|
||||
if self.frame % 5 == 0:
|
||||
self.img_idx = next(self.img_gen)
|
||||
self.image = self.config.images.player[self.img_idx]
|
||||
self.w = self.image.get_width()
|
||||
self.h = self.image.get_height()
|
||||
|
||||
def tick_shm(self) -> None:
|
||||
if self.vel_y >= self.max_vel_y or self.vel_y <= self.min_vel_y:
|
||||
@ -93,29 +91,18 @@ class Player(Sprite):
|
||||
self.vel_y += self.acc_y
|
||||
self.y += self.vel_y
|
||||
|
||||
self.mid_x = self.x + self.width / 2
|
||||
self.mid_y = self.y + self.height / 2
|
||||
|
||||
def tick_normal(self) -> None:
|
||||
if self.vel_y < self.max_vel_y and not self.flapped:
|
||||
self.vel_y += self.acc_y
|
||||
if self.flapped:
|
||||
self.flapped = False
|
||||
|
||||
self.y += min(
|
||||
self.vel_y, self.window.play_area_height - self.y - self.height
|
||||
)
|
||||
|
||||
self.mid_x = self.x + self.width / 2
|
||||
self.mid_y = self.y + self.height / 2
|
||||
|
||||
self.y = clamp(self.y + self.vel_y, self.min_y, self.max_y)
|
||||
self.rotate()
|
||||
|
||||
def tick_crash(self) -> None:
|
||||
if self.y + self.height < self.window.play_area_height - 1:
|
||||
self.y += min(
|
||||
self.vel_y, self.window.play_area_height - self.y - self.height
|
||||
)
|
||||
if self.min_y <= self.y <= self.max_y:
|
||||
self.y = clamp(self.y + self.vel_y, self.min_y, self.max_y)
|
||||
# rotate only when it's a pipe crash and bird is still falling
|
||||
if self.crash_entity != "floor":
|
||||
self.rotate()
|
||||
@ -127,8 +114,8 @@ class Player(Sprite):
|
||||
def rotate(self) -> None:
|
||||
self.rot = clamp(self.rot + self.vel_rot, self.rot_min, self.rot_max)
|
||||
|
||||
def tick(self) -> None:
|
||||
self.update_img_idx()
|
||||
def draw(self) -> None:
|
||||
self.update_image()
|
||||
if self.mode == PlayerMode.SHM:
|
||||
self.tick_shm()
|
||||
elif self.mode == PlayerMode.NORMAL:
|
||||
@ -139,56 +126,38 @@ class Player(Sprite):
|
||||
self.draw_player()
|
||||
|
||||
def draw_player(self) -> None:
|
||||
player_surface = pygame.transform.rotate(
|
||||
self.images.player[self.img_idx], self.rot
|
||||
)
|
||||
self.screen.blit(player_surface, (self.x, self.y))
|
||||
rotated_image = pygame.transform.rotate(self.image, self.rot)
|
||||
rotated_rect = rotated_image.get_rect(center=self.rect.center)
|
||||
self.config.screen.blit(rotated_image, rotated_rect)
|
||||
|
||||
def flap(self) -> None:
|
||||
self.vel_y = self.flap_acc
|
||||
self.flapped = True
|
||||
self.rot = 80
|
||||
self.sounds.wing.play()
|
||||
if self.y > self.min_y:
|
||||
self.vel_y = self.flap_acc
|
||||
self.flapped = True
|
||||
self.rot = 80
|
||||
self.config.sounds.wing.play()
|
||||
|
||||
def crossed(self, pipe: Pipe) -> bool:
|
||||
return pipe.mid_x <= self.mid_x < pipe.mid_x + 4
|
||||
return pipe.cx <= self.cx < pipe.cx - pipe.vel_x
|
||||
|
||||
def collided(self, pipes: Pipes, floor: Floor) -> bool:
|
||||
"""returns True if player collides with base or pipes."""
|
||||
"""returns True if player collides with floor or pipes."""
|
||||
|
||||
# if player crashes into ground
|
||||
if self.y + self.height >= floor.y - 1:
|
||||
if self.collide(floor):
|
||||
self.crashed = True
|
||||
self.crash_entity = "floor"
|
||||
return True
|
||||
else:
|
||||
p_rect = pygame.Rect(self.x, self.y, self.width, self.height)
|
||||
|
||||
for u_pipe, l_pipe in zip(pipes.upper, pipes.lower):
|
||||
# upper and lower pipe rects
|
||||
u_pipe_rect = pygame.Rect(
|
||||
u_pipe.x, u_pipe.y, u_pipe.width, u_pipe.height
|
||||
)
|
||||
l_pipe_rect = pygame.Rect(
|
||||
l_pipe.x, l_pipe.y, l_pipe.width, l_pipe.height
|
||||
)
|
||||
for pipe in pipes.upper:
|
||||
if self.collide(pipe):
|
||||
self.crashed = True
|
||||
self.crash_entity = "pipe"
|
||||
return True
|
||||
for pipe in pipes.lower:
|
||||
if self.collide(pipe):
|
||||
self.crashed = True
|
||||
self.crash_entity = "pipe"
|
||||
return True
|
||||
|
||||
# player and upper/lower pipe hitmasks
|
||||
p_hit_mask = self.hit_mask.player[self.img_idx]
|
||||
u_hit_mask = self.hit_mask.pipe[0]
|
||||
l_hit_mask = self.hit_mask.pipe[1]
|
||||
|
||||
# if bird collided with upipe or lpipe
|
||||
u_collide = pixel_collision(
|
||||
p_rect, u_pipe_rect, p_hit_mask, u_hit_mask
|
||||
)
|
||||
l_collide = pixel_collision(
|
||||
p_rect, l_pipe_rect, p_hit_mask, l_hit_mask
|
||||
)
|
||||
|
||||
if u_collide or l_collide:
|
||||
self.crashed = True
|
||||
self.crash_entity = "pipe"
|
||||
return True
|
||||
|
||||
return False
|
||||
return False
|
28
src/entities/score.py
Normal file
28
src/entities/score.py
Normal file
@ -0,0 +1,28 @@
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class Score(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(config)
|
||||
self.score = 0
|
||||
|
||||
def reset(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
def add(self) -> None:
|
||||
self.score += 1
|
||||
self.config.sounds.point.play()
|
||||
|
||||
def draw(self) -> None:
|
||||
"""displays score in center of screen"""
|
||||
score_digits = [int(x) for x in list(str(self.score))]
|
||||
images = [self.config.images.numbers[digit] for digit in score_digits]
|
||||
digits_width = sum(image.get_width() for image in images)
|
||||
x_offset = (self.config.window.width - digits_width) / 2
|
||||
|
||||
for image in images:
|
||||
self.config.screen.blit(
|
||||
image, (x_offset, self.config.window.height * 0.1)
|
||||
)
|
||||
x_offset += image.get_width()
|
14
src/entities/welcome_message.py
Normal file
14
src/entities/welcome_message.py
Normal file
@ -0,0 +1,14 @@
|
||||
from ..utils import GameConfig
|
||||
from .entity import Entity
|
||||
|
||||
|
||||
class WelcomeMessage(Entity):
|
||||
def __init__(self, config: GameConfig) -> None:
|
||||
super().__init__(
|
||||
config=config,
|
||||
image=config.images.message,
|
||||
x=int(
|
||||
(config.window.width - config.images.message.get_width()) / 2
|
||||
),
|
||||
y=int(config.window.height * 0.12),
|
||||
)
|
@ -4,7 +4,7 @@ import sys
|
||||
import pygame
|
||||
from pygame.locals import K_ESCAPE, K_SPACE, K_UP, KEYDOWN, QUIT
|
||||
|
||||
from .sprites import (
|
||||
from .entities import (
|
||||
Background,
|
||||
Floor,
|
||||
GameOver,
|
||||
@ -14,42 +14,35 @@ from .sprites import (
|
||||
Score,
|
||||
WelcomeMessage,
|
||||
)
|
||||
from .utils import HitMask, Images, Sounds, Window
|
||||
from .utils import GameConfig, Images, Sounds, Window
|
||||
|
||||
|
||||
class Flappy:
|
||||
def __init__(self):
|
||||
pygame.init()
|
||||
pygame.display.set_caption("Flappy Bird")
|
||||
self.window = Window(288, 512)
|
||||
self.fps = 30
|
||||
self.clock = pygame.time.Clock()
|
||||
self.screen = pygame.display.set_mode(
|
||||
(self.window.width, self.window.height)
|
||||
)
|
||||
self.images = Images()
|
||||
self.sounds = Sounds()
|
||||
self.hit_mask = HitMask(self.images)
|
||||
window = Window(288, 512)
|
||||
screen = pygame.display.set_mode((window.width, window.height))
|
||||
images = Images()
|
||||
|
||||
self.sprite_args = (
|
||||
self.screen,
|
||||
self.clock,
|
||||
self.fps,
|
||||
self.window,
|
||||
self.images,
|
||||
self.sounds,
|
||||
self.hit_mask,
|
||||
self.config = GameConfig(
|
||||
screen=screen,
|
||||
clock=pygame.time.Clock(),
|
||||
fps=30,
|
||||
window=window,
|
||||
images=images,
|
||||
sounds=Sounds(),
|
||||
)
|
||||
|
||||
async def start(self):
|
||||
while True:
|
||||
self.background = Background(*self.sprite_args)
|
||||
self.floor = Floor(*self.sprite_args)
|
||||
self.player = Player(*self.sprite_args)
|
||||
self.welcome_message = WelcomeMessage(*self.sprite_args)
|
||||
self.game_over_message = GameOver(*self.sprite_args)
|
||||
self.pipes = Pipes(*self.sprite_args)
|
||||
self.score = Score(*self.sprite_args)
|
||||
self.background = Background(self.config)
|
||||
self.floor = Floor(self.config)
|
||||
self.player = Player(self.config)
|
||||
self.welcome_message = WelcomeMessage(self.config)
|
||||
self.game_over_message = GameOver(self.config)
|
||||
self.pipes = Pipes(self.config)
|
||||
self.score = Score(self.config)
|
||||
await self.splash()
|
||||
await self.play()
|
||||
await self.game_over()
|
||||
@ -72,7 +65,7 @@ class Flappy:
|
||||
|
||||
pygame.display.update()
|
||||
await asyncio.sleep(0)
|
||||
self.clock.tick(self.fps)
|
||||
self.config.tick()
|
||||
|
||||
def check_quit_event(self, event):
|
||||
if event.type == QUIT or (
|
||||
@ -94,21 +87,18 @@ class Flappy:
|
||||
self.player.set_mode(PlayerMode.NORMAL)
|
||||
|
||||
while True:
|
||||
|
||||
if self.player.collided(self.pipes, self.floor):
|
||||
return
|
||||
|
||||
for pipe in self.pipes.upper:
|
||||
for i, pipe in enumerate(self.pipes.upper):
|
||||
if self.player.crossed(pipe):
|
||||
self.score.add()
|
||||
|
||||
for event in pygame.event.get():
|
||||
self.check_quit_event(event)
|
||||
if self.is_tap_event(event):
|
||||
if self.player.y > -2 * self.images.player[0].get_height():
|
||||
self.player.flap()
|
||||
self.player.flap()
|
||||
|
||||
# draw sprites
|
||||
self.background.tick()
|
||||
self.floor.tick()
|
||||
self.pipes.tick()
|
||||
@ -117,7 +107,7 @@ class Flappy:
|
||||
|
||||
pygame.display.update()
|
||||
await asyncio.sleep(0)
|
||||
self.clock.tick(self.fps)
|
||||
self.config.tick()
|
||||
|
||||
async def game_over(self):
|
||||
"""crashes the player down and shows gameover image"""
|
||||
@ -130,18 +120,16 @@ class Flappy:
|
||||
for event in pygame.event.get():
|
||||
self.check_quit_event(event)
|
||||
if self.is_tap_event(event):
|
||||
if self.player.y + self.player.height >= self.floor.y - 1:
|
||||
if self.player.y + self.player.h >= self.floor.y - 1:
|
||||
return
|
||||
|
||||
# draw sprites
|
||||
self.background.tick()
|
||||
self.floor.tick()
|
||||
self.pipes.tick()
|
||||
self.score.tick()
|
||||
self.player.tick()
|
||||
self.game_over_message.tick()
|
||||
# self.screen.blit(self.images.gameover, (50, 180))
|
||||
|
||||
self.clock.tick(self.fps)
|
||||
self.config.tick()
|
||||
pygame.display.update()
|
||||
await asyncio.sleep(0)
|
||||
|
@ -1,10 +0,0 @@
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class Background(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
def tick(self) -> None:
|
||||
self.screen.blit(self.images.background, (self.x, self.y))
|
@ -1,20 +0,0 @@
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class Floor(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.x = 0
|
||||
self.y = self.window.play_area_height
|
||||
# amount to shift on each tick
|
||||
self.vel_x = 4
|
||||
# amount by which floor can maximum shift to left
|
||||
self.x_extra = (
|
||||
self.images.base.get_width() - self.images.background.get_width()
|
||||
)
|
||||
|
||||
def stop(self) -> None:
|
||||
self.vel_x = 0
|
||||
|
||||
def tick(self) -> None:
|
||||
self.x = -((-self.x + self.vel_x) % self.x_extra)
|
||||
self.screen.blit(self.images.base, (self.x, self.y))
|
@ -1,10 +0,0 @@
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class GameOver(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.x = int((self.window.width - self.images.gameover.get_width()) / 2)
|
||||
self.y = int(self.window.height * 0.2)
|
||||
|
||||
def tick(self) -> None:
|
||||
self.screen.blit(self.images.gameover, (self.x, self.y))
|
@ -1,112 +0,0 @@
|
||||
import random
|
||||
from typing import List
|
||||
|
||||
from pygame import Surface
|
||||
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class Pipe(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.set_image(self.images.pipe[0])
|
||||
self.mid_x = self.x + self.images.pipe[0].get_width() / 2
|
||||
# TODO: make this change with game progress
|
||||
self.vel_x = -5
|
||||
|
||||
def set_image(self, image: Surface) -> None:
|
||||
self.image = image
|
||||
self.width = self.image.get_width()
|
||||
self.height = self.image.get_height()
|
||||
|
||||
def tick(self) -> None:
|
||||
self.x += self.vel_x
|
||||
self.mid_x = self.x + self.images.pipe[0].get_width() / 2
|
||||
self.screen.blit(self.image, (self.x, self.y))
|
||||
|
||||
|
||||
class Pipes(Sprite):
|
||||
upper: List[Pipe]
|
||||
lower: List[Pipe]
|
||||
|
||||
def setup(self) -> None:
|
||||
# TODO: make this change with game progress
|
||||
self.pipe_gap = 120
|
||||
self.top = 0
|
||||
self.bottom = self.window.play_area_height
|
||||
self.reset()
|
||||
|
||||
def reset(self) -> None:
|
||||
self.upper = []
|
||||
self.lower = []
|
||||
self.spawn_initial_pipes()
|
||||
|
||||
def tick(self) -> None:
|
||||
if self.can_spawn_more():
|
||||
self.spawn_new_pipes()
|
||||
self.remove_old_pipes()
|
||||
|
||||
for up_pipe, low_pipe in zip(self.upper, self.lower):
|
||||
up_pipe.tick()
|
||||
low_pipe.tick()
|
||||
|
||||
def stop(self) -> None:
|
||||
for pipe in self.upper + self.lower:
|
||||
pipe.vel_x = 0
|
||||
|
||||
def can_spawn_more(self) -> bool:
|
||||
# has 1 or 2 pipe and first pipe is almost about to exit the screen
|
||||
return 0 < len(self.upper) < 3 and 0 < self.upper[0].x < 5
|
||||
|
||||
def spawn_new_pipes(self):
|
||||
# add new pipe when first pipe is about to touch left of screen
|
||||
upper, lower = self.make_random_pipes()
|
||||
self.upper.append(upper)
|
||||
self.lower.append(lower)
|
||||
|
||||
def remove_old_pipes(self):
|
||||
# remove first pipe if its out of the screen
|
||||
if (
|
||||
len(self.upper) > 0
|
||||
and self.upper[0].x < -self.images.pipe[0].get_width()
|
||||
):
|
||||
self.upper.pop(0)
|
||||
self.lower.pop(0)
|
||||
|
||||
def spawn_initial_pipes(self):
|
||||
upper_1, lower_1 = self.make_random_pipes()
|
||||
upper_1.x = self.window.width + 100
|
||||
lower_1.x = self.window.width + 100
|
||||
|
||||
upper_2, lower_2 = self.make_random_pipes()
|
||||
upper_2.x = self.window.width + 100 + (self.window.width / 2)
|
||||
lower_2.x = self.window.width + 100 + (self.window.width / 2)
|
||||
|
||||
self.upper.append(upper_1)
|
||||
self.upper.append(upper_2)
|
||||
|
||||
self.lower.append(lower_1)
|
||||
self.lower.append(lower_2)
|
||||
|
||||
def make_random_pipes(self):
|
||||
"""returns a randomly generated pipe"""
|
||||
# y of gap between upper and lower pipe
|
||||
base_y = self.window.play_area_height
|
||||
|
||||
gap_y = random.randrange(0, int(base_y * 0.6 - self.pipe_gap))
|
||||
gap_y += int(base_y * 0.2)
|
||||
pipe_height = self.images.pipe[0].get_height()
|
||||
pipe_x = self.window.width + 10
|
||||
|
||||
upper_pipe = Pipe(*self._args)
|
||||
upper_pipe.x = pipe_x
|
||||
upper_pipe.y = gap_y - pipe_height
|
||||
upper_pipe.set_image(self.images.pipe[0])
|
||||
|
||||
lower_pipe = Pipe(*self._args)
|
||||
lower_pipe.x = pipe_x
|
||||
lower_pipe.y = gap_y + self.pipe_gap
|
||||
lower_pipe.set_image(self.images.pipe[1])
|
||||
|
||||
return upper_pipe, lower_pipe
|
@ -1,30 +0,0 @@
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class Score(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
def reset(self) -> None:
|
||||
self.score = 0
|
||||
|
||||
def add(self) -> None:
|
||||
self.score += 1
|
||||
self.sounds.point.play()
|
||||
|
||||
def tick(self) -> None:
|
||||
"""displays score in center of screen"""
|
||||
scoreDigits = [int(x) for x in list(str(self.score))]
|
||||
totalWidth = 0 # total width of all numbers to be printed
|
||||
|
||||
for digit in scoreDigits:
|
||||
totalWidth += self.images.numbers[digit].get_width()
|
||||
|
||||
x_offset = (self.window.width - totalWidth) / 2
|
||||
|
||||
for digit in scoreDigits:
|
||||
self.screen.blit(
|
||||
self.images.numbers[digit],
|
||||
(x_offset, self.window.height * 0.1),
|
||||
)
|
||||
x_offset += self.images.numbers[digit].get_width()
|
@ -1,43 +0,0 @@
|
||||
import pygame
|
||||
|
||||
from ..utils.hit_mask import HitMask
|
||||
from ..utils.images import Images
|
||||
from ..utils.sounds import Sounds
|
||||
from ..utils.window import Window
|
||||
|
||||
|
||||
class Sprite:
|
||||
def __init__(
|
||||
self,
|
||||
screen: pygame.Surface,
|
||||
clock: pygame.time.Clock,
|
||||
fps: int,
|
||||
window: Window,
|
||||
images: Images,
|
||||
sounds: Sounds,
|
||||
hit_mask: HitMask,
|
||||
) -> None:
|
||||
self.screen = screen
|
||||
self.clock = clock
|
||||
self.fps = fps
|
||||
self.window = window
|
||||
self.images = images
|
||||
self.sounds = sounds
|
||||
self.hit_mask = hit_mask
|
||||
self._args = (
|
||||
self.screen,
|
||||
self.clock,
|
||||
self.fps,
|
||||
self.window,
|
||||
self.images,
|
||||
self.sounds,
|
||||
self.hit_mask,
|
||||
)
|
||||
|
||||
self.setup()
|
||||
|
||||
def setup(self) -> None:
|
||||
pass
|
||||
|
||||
def tick() -> None:
|
||||
pass
|
@ -1,10 +0,0 @@
|
||||
from .sprite import Sprite
|
||||
|
||||
|
||||
class WelcomeMessage(Sprite):
|
||||
def setup(self) -> None:
|
||||
self.x = int((self.window.width - self.images.message.get_width()) / 2)
|
||||
self.y = int(self.window.height * 0.12)
|
||||
|
||||
def tick(self) -> None:
|
||||
self.screen.blit(self.images.message, (self.x, self.y))
|
@ -1,12 +1,5 @@
|
||||
from .hit_mask import HitMask
|
||||
from .game_config import GameConfig
|
||||
from .images import Images
|
||||
from .sounds import Sounds
|
||||
from .utils import clamp, pixel_collision
|
||||
from .utils import clamp, get_hit_mask, pixel_collision
|
||||
from .window import Window
|
||||
|
||||
__all__ = [
|
||||
"HitMask",
|
||||
"Images",
|
||||
"Sounds",
|
||||
"Window",
|
||||
]
|
||||
|
29
src/utils/game_config.py
Normal file
29
src/utils/game_config.py
Normal file
@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
import pygame
|
||||
|
||||
from .images import Images
|
||||
from .sounds import Sounds
|
||||
from .window import Window
|
||||
|
||||
|
||||
class GameConfig:
|
||||
def __init__(
|
||||
self,
|
||||
screen: pygame.Surface,
|
||||
clock: pygame.time.Clock,
|
||||
fps: int,
|
||||
window: Window,
|
||||
images: Images,
|
||||
sounds: Sounds,
|
||||
) -> None:
|
||||
self.screen = screen
|
||||
self.clock = clock
|
||||
self.fps = fps
|
||||
self.window = window
|
||||
self.images = images
|
||||
self.sounds = sounds
|
||||
self.debug = os.environ.get("DEBUG", False)
|
||||
|
||||
def tick(self) -> None:
|
||||
self.clock.tick(self.fps)
|
@ -1,38 +0,0 @@
|
||||
from typing import List, Tuple
|
||||
|
||||
from .images import Images
|
||||
|
||||
# create custom type
|
||||
HitMaskType = List[List[bool]]
|
||||
|
||||
|
||||
class HitMask:
|
||||
pipe: Tuple[HitMaskType]
|
||||
player: Tuple[HitMaskType]
|
||||
|
||||
def __init__(self, images: Images) -> None:
|
||||
# hit mask for pipe
|
||||
self.pipe = (
|
||||
self.make_hit_mask(images.pipe[0]),
|
||||
self.make_hit_mask(images.pipe[1]),
|
||||
)
|
||||
# hit mask for player
|
||||
self.player = (
|
||||
self.make_hit_mask(images.player[0]),
|
||||
self.make_hit_mask(images.player[1]),
|
||||
self.make_hit_mask(images.player[2]),
|
||||
)
|
||||
|
||||
def make_hit_mask(self, image) -> HitMaskType:
|
||||
"""returns a hit mask using an image's alpha."""
|
||||
return list(
|
||||
(
|
||||
list(
|
||||
(
|
||||
bool(image.get_at((x, y))[3])
|
||||
for y in range(image.get_height())
|
||||
)
|
||||
)
|
||||
for x in range(image.get_width())
|
||||
)
|
||||
)
|
@ -8,7 +8,7 @@ from .constants import BACKGROUNDS, PIPES, PLAYERS
|
||||
|
||||
class Images:
|
||||
numbers: List[pygame.Surface]
|
||||
gameover: pygame.Surface
|
||||
game_over: pygame.Surface
|
||||
message: pygame.Surface
|
||||
base: pygame.Surface
|
||||
background: pygame.Surface
|
||||
@ -24,7 +24,7 @@ class Images:
|
||||
)
|
||||
|
||||
# game over sprite
|
||||
self.gameover = pygame.image.load(
|
||||
self.game_over = pygame.image.load(
|
||||
"assets/sprites/gameover.png"
|
||||
).convert_alpha()
|
||||
# message sprite for welcome screen
|
||||
|
@ -1,6 +1,43 @@
|
||||
from functools import wraps
|
||||
from typing import List
|
||||
|
||||
import pygame
|
||||
|
||||
from .hit_mask import HitMaskType
|
||||
HitMaskType = List[List[bool]]
|
||||
|
||||
|
||||
def clamp(n: float, minn: float, maxn: float) -> float:
|
||||
"""Clamps a number between two values"""
|
||||
return max(min(maxn, n), minn)
|
||||
|
||||
|
||||
def memoize(func):
|
||||
cache = {}
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
key = (args, frozenset(kwargs.items()))
|
||||
if key not in cache:
|
||||
cache[key] = func(*args, **kwargs)
|
||||
return cache[key]
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@memoize
|
||||
def get_hit_mask(image: pygame.Surface) -> HitMaskType:
|
||||
"""returns a hit mask using an image's alpha."""
|
||||
return list(
|
||||
(
|
||||
list(
|
||||
(
|
||||
bool(image.get_at((x, y))[3])
|
||||
for y in range(image.get_height())
|
||||
)
|
||||
)
|
||||
for x in range(image.get_width())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def pixel_collision(
|
||||
@ -23,8 +60,3 @@ def pixel_collision(
|
||||
if hitmask1[x1 + x][y1 + y] and hitmask2[x2 + x][y2 + y]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def clamp(n: float, minn: float, maxn: float) -> float:
|
||||
"""Clamps a number between two values"""
|
||||
return max(min(maxn, n), minn)
|
||||
|
@ -2,5 +2,5 @@ class Window:
|
||||
def __init__(self, width, height):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.play_area_width = width
|
||||
self.play_area_height = height * 0.79
|
||||
self.viewport_width = width
|
||||
self.viewport_height = height * 0.79
|
||||
|
Loading…
Reference in New Issue
Block a user