From 4960be17597f7c95466f439c54cb249b62933900 Mon Sep 17 00:00:00 2001 From: Sourabh Verma Date: Mon, 19 Jun 2023 03:49:14 +0530 Subject: [PATCH] Improve code by adding a generic entity object --- .gitignore | 1 + src/{sprites => entities}/__init__.py | 4 +- src/entities/background.py | 7 ++ src/entities/entity.py | 77 +++++++++++++++++ src/entities/floor.py | 21 +++++ src/entities/game_over.py | 12 +++ src/entities/pipe.py | 104 ++++++++++++++++++++++ src/{sprites => entities}/player.py | 119 ++++++++++---------------- src/entities/score.py | 28 ++++++ src/entities/welcome_message.py | 14 +++ src/flappy.py | 62 ++++++-------- src/sprites/background.py | 10 --- src/sprites/floor.py | 20 ----- src/sprites/game_over.py | 10 --- src/sprites/pipe.py | 112 ------------------------ src/sprites/score.py | 30 ------- src/sprites/sprite.py | 43 ---------- src/sprites/welcome_message.py | 10 --- src/utils/__init__.py | 11 +-- src/utils/game_config.py | 29 +++++++ src/utils/hit_mask.py | 38 -------- src/utils/images.py | 4 +- src/utils/utils.py | 44 ++++++++-- src/utils/window.py | 4 +- 24 files changed, 408 insertions(+), 406 deletions(-) rename src/{sprites => entities}/__init__.py (89%) create mode 100644 src/entities/background.py create mode 100644 src/entities/entity.py create mode 100644 src/entities/floor.py create mode 100644 src/entities/game_over.py create mode 100644 src/entities/pipe.py rename src/{sprites => entities}/player.py (55%) create mode 100644 src/entities/score.py create mode 100644 src/entities/welcome_message.py delete mode 100644 src/sprites/background.py delete mode 100644 src/sprites/floor.py delete mode 100644 src/sprites/game_over.py delete mode 100644 src/sprites/pipe.py delete mode 100644 src/sprites/score.py delete mode 100644 src/sprites/sprite.py delete mode 100644 src/sprites/welcome_message.py create mode 100644 src/utils/game_config.py delete mode 100644 src/utils/hit_mask.py diff --git a/.gitignore b/.gitignore index aa4b526..ba627c1 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ build .python-version *.egg-info .vscode +.DS_Store diff --git a/src/sprites/__init__.py b/src/entities/__init__.py similarity index 89% rename from src/sprites/__init__.py rename to src/entities/__init__.py index b319d8e..4317b26 100644 --- a/src/sprites/__init__.py +++ b/src/entities/__init__.py @@ -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", ] diff --git a/src/entities/background.py b/src/entities/background.py new file mode 100644 index 0000000..1524701 --- /dev/null +++ b/src/entities/background.py @@ -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) diff --git a/src/entities/entity.py b/src/entities/entity.py new file mode 100644 index 0000000..72a7db2 --- /dev/null +++ b/src/entities/entity.py @@ -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)) diff --git a/src/entities/floor.py b/src/entities/floor.py new file mode 100644 index 0000000..1614c0f --- /dev/null +++ b/src/entities/floor.py @@ -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() diff --git a/src/entities/game_over.py b/src/entities/game_over.py new file mode 100644 index 0000000..1dc1d93 --- /dev/null +++ b/src/entities/game_over.py @@ -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), + ) diff --git a/src/entities/pipe.py b/src/entities/pipe.py new file mode 100644 index 0000000..f8b1851 --- /dev/null +++ b/src/entities/pipe.py @@ -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 diff --git a/src/sprites/player.py b/src/entities/player.py similarity index 55% rename from src/sprites/player.py rename to src/entities/player.py index 08ed6da..d958c4c 100644 --- a/src/sprites/player.py +++ b/src/entities/player.py @@ -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 diff --git a/src/entities/score.py b/src/entities/score.py new file mode 100644 index 0000000..6d89949 --- /dev/null +++ b/src/entities/score.py @@ -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() diff --git a/src/entities/welcome_message.py b/src/entities/welcome_message.py new file mode 100644 index 0000000..964b107 --- /dev/null +++ b/src/entities/welcome_message.py @@ -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), + ) diff --git a/src/flappy.py b/src/flappy.py index 86d9998..a8c8732 100644 --- a/src/flappy.py +++ b/src/flappy.py @@ -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) diff --git a/src/sprites/background.py b/src/sprites/background.py deleted file mode 100644 index 4726f71..0000000 --- a/src/sprites/background.py +++ /dev/null @@ -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)) diff --git a/src/sprites/floor.py b/src/sprites/floor.py deleted file mode 100644 index 85d71db..0000000 --- a/src/sprites/floor.py +++ /dev/null @@ -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)) diff --git a/src/sprites/game_over.py b/src/sprites/game_over.py deleted file mode 100644 index 945a21d..0000000 --- a/src/sprites/game_over.py +++ /dev/null @@ -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)) diff --git a/src/sprites/pipe.py b/src/sprites/pipe.py deleted file mode 100644 index d144ba4..0000000 --- a/src/sprites/pipe.py +++ /dev/null @@ -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 diff --git a/src/sprites/score.py b/src/sprites/score.py deleted file mode 100644 index 03a7e14..0000000 --- a/src/sprites/score.py +++ /dev/null @@ -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() diff --git a/src/sprites/sprite.py b/src/sprites/sprite.py deleted file mode 100644 index 35fdfc6..0000000 --- a/src/sprites/sprite.py +++ /dev/null @@ -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 diff --git a/src/sprites/welcome_message.py b/src/sprites/welcome_message.py deleted file mode 100644 index 7f2dfd7..0000000 --- a/src/sprites/welcome_message.py +++ /dev/null @@ -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)) diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 5f13a3b..5cb8d1f 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -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", -] diff --git a/src/utils/game_config.py b/src/utils/game_config.py new file mode 100644 index 0000000..610f0c1 --- /dev/null +++ b/src/utils/game_config.py @@ -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) diff --git a/src/utils/hit_mask.py b/src/utils/hit_mask.py deleted file mode 100644 index 38c69f8..0000000 --- a/src/utils/hit_mask.py +++ /dev/null @@ -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()) - ) - ) diff --git a/src/utils/images.py b/src/utils/images.py index b345c49..87df610 100644 --- a/src/utils/images.py +++ b/src/utils/images.py @@ -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 diff --git a/src/utils/utils.py b/src/utils/utils.py index 5d5fa33..5e8fa51 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -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) diff --git a/src/utils/window.py b/src/utils/window.py index 6148c57..83297cd 100644 --- a/src/utils/window.py +++ b/src/utils/window.py @@ -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