From 53cea01052b4aab6c02d2733caed8aa9ba22af47 Mon Sep 17 00:00:00 2001 From: Sourabh Verma Date: Mon, 19 Jun 2023 00:02:31 +0530 Subject: [PATCH] Split game logic into multiple sprites --- src/flappy.py | 435 ++++++--------------------------- src/sprites/__init__.py | 19 ++ src/sprites/background.py | 10 + src/sprites/floor.py | 20 ++ src/sprites/game_over.py | 10 + src/sprites/pipe.py | 112 +++++++++ src/sprites/player.py | 193 +++++++++++++++ src/sprites/score.py | 30 +++ src/sprites/sprite.py | 43 ++++ src/sprites/welcome_message.py | 10 + src/utils/__init__.py | 12 + src/{ => utils}/constants.py | 0 src/{ => utils}/hit_mask.py | 0 src/{ => utils}/images.py | 0 src/{ => utils}/sounds.py | 0 src/{ => utils}/utils.py | 5 + src/utils/window.py | 6 + 17 files changed, 547 insertions(+), 358 deletions(-) create mode 100644 src/sprites/__init__.py create mode 100644 src/sprites/background.py create mode 100644 src/sprites/floor.py create mode 100644 src/sprites/game_over.py create mode 100644 src/sprites/pipe.py create mode 100644 src/sprites/player.py create mode 100644 src/sprites/score.py create mode 100644 src/sprites/sprite.py create mode 100644 src/sprites/welcome_message.py create mode 100644 src/utils/__init__.py rename src/{ => utils}/constants.py (100%) rename src/{ => utils}/hit_mask.py (100%) rename src/{ => utils}/images.py (100%) rename src/{ => utils}/sounds.py (100%) rename src/{ => utils}/utils.py (81%) create mode 100644 src/utils/window.py diff --git a/src/flappy.py b/src/flappy.py index 2060edd..176784e 100644 --- a/src/flappy.py +++ b/src/flappy.py @@ -1,40 +1,27 @@ import asyncio -import random import sys -from itertools import cycle import pygame from pygame.locals import K_ESCAPE, K_SPACE, K_UP, KEYDOWN, QUIT -from .hit_mask import HitMask -from .images import Images -from .sounds import Sounds -from .utils import pixel_collision - - -class Window: - def __init__(self, width, height): - self.width = width - self.height = height +from .sprites import ( + Background, + Floor, + GameOver, + Pipes, + Player, + PlayerMode, + Score, + WelcomeMessage, +) +from .utils import HitMask, Images, Sounds, Window class Flappy: - screen: pygame.Surface - clock: pygame.time.Clock - fps = 30 - window: Window - pipe_gap: int - base_y: float - images: Images - sounds: Sounds - hit_masks: HitMask - def __init__(self): pygame.init() pygame.display.set_caption("Flappy Bird") self.window = Window(288, 512) - self.pipe_gap = 100 - self.base_y = self.window.height * 0.79 self.fps = 30 self.clock = pygame.time.Clock() self.screen = pygame.display.set_mode( @@ -42,386 +29,118 @@ def __init__(self): ) self.images = Images() self.sounds = Sounds() - self.hit_masks = HitMask(self.images) + self.hit_mask = HitMask(self.images) + + self.sprite_args = ( + self.screen, + self.clock, + self.fps, + self.window, + self.images, + self.sounds, + self.hit_mask, + ) async def start(self): while True: - movement_info = await self.splash() - crash_info = await self.play(movement_info) - await self.game_over(crash_info) + 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) + await self.splash() + await self.play() + await self.game_over() async def splash(self): """Shows welcome splash screen animation of flappy bird""" - # index of player to blit on screen - player_index = 0 - player_index_gen = cycle([0, 1, 2, 1]) - # iterator used to change player_index after every 5th iteration - loop_iter = 0 - player_x = int(self.window.width * 0.2) - player_y = int( - (self.window.height - self.images.player[0].get_height()) / 2 - ) - - message_x = int( - (self.window.width - self.images.message.get_width()) / 2 - ) - message_y = int(self.window.height * 0.12) - - base_x = 0 - # amount by which base can maximum shift to left - baseShift = ( - self.images.base.get_width() - self.images.background.get_width() - ) - - # player shm for up-down motion on welcome screen - player_shm_vals = {"val": 0, "dir": 1} + self.player.set_mode(PlayerMode.SHM) while True: for event in pygame.event.get(): - if event.type == QUIT or ( - event.type == KEYDOWN and event.key == K_ESCAPE - ): - pygame.quit() - sys.exit() + self.check_quit_event(event) if self.is_tap_event(event): - # make first flap sound and return values for mainGame - self.sounds.wing.play() - return { - "player_y": player_y + player_shm_vals["val"], - "base_x": base_x, - "player_index_gen": player_index_gen, - } + return - # adjust player_y, player_index, base_x - if (loop_iter + 1) % 5 == 0: - player_index = next(player_index_gen) - loop_iter = (loop_iter + 1) % 30 - base_x = -((-base_x + 4) % baseShift) - self.player_shm(player_shm_vals) - - # draw sprites - self.screen.blit(self.images.background, (0, 0)) - self.screen.blit( - self.images.player[player_index], - (player_x, player_y + player_shm_vals["val"]), - ) - self.screen.blit(self.images.message, (message_x, message_y)) - self.screen.blit(self.images.base, (base_x, self.base_y)) + self.background.tick() + self.floor.tick() + self.player.tick() + self.welcome_message.tick() pygame.display.update() await asyncio.sleep(0) self.clock.tick(self.fps) + def check_quit_event(self, event): + if event.type == QUIT or ( + event.type == KEYDOWN and event.key == K_ESCAPE + ): + pygame.quit() + sys.exit() + def is_tap_event(self, event): - left, _, _ = pygame.mouse.get_pressed() + m_left, _, _ = pygame.mouse.get_pressed() space_or_up = event.type == KEYDOWN and ( event.key == K_SPACE or event.key == K_UP ) screen_tap = event.type == pygame.FINGERDOWN - return left or space_or_up or screen_tap - - def player_shm(self, shm): - """oscillates the value of shm['val'] between 8 and -8""" - if abs(shm["val"]) == 8: - shm["dir"] *= -1 + return m_left or space_or_up or screen_tap - if shm["dir"] == 1: - shm["val"] += 1 - else: - shm["val"] -= 1 - - async def play(self, movement_nfo): - score = playerIndex = loopIter = 0 - player_index_gen = movement_nfo["player_index_gen"] - player_x, player_y = ( - int(self.window.width * 0.2), - movement_nfo["player_y"], - ) - - base_x = movement_nfo["base_x"] - baseShift = ( - self.images.base.get_width() - self.images.background.get_width() - ) - - # get 2 new pipes to add to upperPipes lowerPipes list - newPipe1 = self.get_random_pipe() - newPipe2 = self.get_random_pipe() - - # list of upper pipes - upperPipes = [ - {"x": self.window.width + 200, "y": newPipe1[0]["y"]}, - { - "x": self.window.width + 200 + (self.window.width / 2), - "y": newPipe2[0]["y"], - }, - ] - - # list of lowerpipe - lowerPipes = [ - {"x": self.window.width + 200, "y": newPipe1[1]["y"]}, - { - "x": self.window.width + 200 + (self.window.width / 2), - "y": newPipe2[1]["y"], - }, - ] - - dt = self.clock.tick(self.fps) / 1000 - pipeVelX = -128 * dt - - # player velocity, max velocity, downward acceleration, acceleration on flap - playerVelY = ( - -9 - ) # player's velocity along Y, default same as playerFlapped - playerMaxVelY = 10 # max vel along Y, max descend speed - # playerMinVelY = -8 # min vel along Y, max ascend speed - playerAccY = 1 # players downward acceleration - playerRot = 45 # player's rotation - playerVelRot = 3 # angular speed - playerRotThr = 20 # rotation threshold - playerFlapAcc = -9 # players speed on flapping - playerFlapped = False # True when player flaps + async def play(self): + self.score.reset() + self.player.set_mode(PlayerMode.NORMAL) while True: for event in pygame.event.get(): - if event.type == QUIT or ( - event.type == KEYDOWN and event.key == K_ESCAPE - ): - pygame.quit() - sys.exit() + self.check_quit_event(event) if self.is_tap_event(event): - if player_y > -2 * self.images.player[0].get_height(): - playerVelY = playerFlapAcc - playerFlapped = True - self.sounds.wing.play() - - # check for crash here - crashTest = self.check_crash( - {"x": player_x, "y": player_y, "index": playerIndex}, - upperPipes, - lowerPipes, - ) - if crashTest[0]: - return { - "y": player_y, - "groundCrash": crashTest[1], - "base_x": base_x, - "upperPipes": upperPipes, - "lowerPipes": lowerPipes, - "score": score, - "playerVelY": playerVelY, - "playerRot": playerRot, - } - - # check for score - playerMidPos = player_x + self.images.player[0].get_width() / 2 - for pipe in upperPipes: - pipeMidPos = pipe["x"] + self.images.pipe[0].get_width() / 2 - if pipeMidPos <= playerMidPos < pipeMidPos + 4: - score += 1 - self.sounds.point.play() - - # playerIndex base_x change - if (loopIter + 1) % 3 == 0: - playerIndex = next(player_index_gen) - loopIter = (loopIter + 1) % 30 - base_x = -((-base_x + 100) % baseShift) - - # rotate the player - if playerRot > -90: - playerRot -= playerVelRot - - # player's movement - if playerVelY < playerMaxVelY and not playerFlapped: - playerVelY += playerAccY - if playerFlapped: - playerFlapped = False - - # more rotation to cover the threshold (calculated in visible rotation) - playerRot = 45 + if self.player.y > -2 * self.images.player[0].get_height(): + self.player.flap() - playerHeight = self.images.player[playerIndex].get_height() - player_y += min(playerVelY, self.base_y - player_y - playerHeight) + for pipe in self.pipes.upper: + if self.player.crossed(pipe): + self.score.add() - # move pipes to left - for uPipe, lPipe in zip(upperPipes, lowerPipes): - uPipe["x"] += pipeVelX - lPipe["x"] += pipeVelX - - # add new pipe when first pipe is about to touch left of screen - if 3 > len(upperPipes) > 0 and 0 < upperPipes[0]["x"] < 5: - newPipe = self.get_random_pipe() - upperPipes.append(newPipe[0]) - lowerPipes.append(newPipe[1]) - - # remove first pipe if its out of the screen - if ( - len(upperPipes) > 0 - and upperPipes[0]["x"] < -self.images.pipe[0].get_width() - ): - upperPipes.pop(0) - lowerPipes.pop(0) + if self.player.collided(self.pipes, self.floor): + return # draw sprites - self.screen.blit(self.images.background, (0, 0)) - - for uPipe, lPipe in zip(upperPipes, lowerPipes): - self.screen.blit(self.images.pipe[0], (uPipe["x"], uPipe["y"])) - self.screen.blit(self.images.pipe[1], (lPipe["x"], lPipe["y"])) - - self.screen.blit(self.images.base, (base_x, self.base_y)) - # print score so player overlaps the score - self.show_score(score) - - # Player rotation has a threshold - visibleRot = playerRotThr - if playerRot <= playerRotThr: - visibleRot = playerRot - - playerSurface = pygame.transform.rotate( - self.images.player[playerIndex], visibleRot - ) - self.screen.blit(playerSurface, (player_x, player_y)) + self.background.tick() + self.floor.tick() + self.pipes.tick() + self.score.tick() + self.player.tick() pygame.display.update() await asyncio.sleep(0) self.clock.tick(self.fps) - async def game_over(self, crashInfo): + async def game_over(self): """crashes the player down and shows gameover image""" - score = crashInfo["score"] - playerx = self.window.width * 0.2 - player_y = crashInfo["y"] - playerHeight = self.images.player[0].get_height() - playerVelY = crashInfo["playerVelY"] - playerAccY = 2 - playerRot = crashInfo["playerRot"] - playerVelRot = 7 - base_x = crashInfo["base_x"] - - upperPipes, lowerPipes = ( - crashInfo["upperPipes"], - crashInfo["lowerPipes"], - ) - - # play hit and die sounds - self.sounds.hit.play() - if not crashInfo["groundCrash"]: - self.sounds.die.play() + self.player.set_mode(PlayerMode.CRASH) + self.pipes.stop() + self.floor.stop() while True: for event in pygame.event.get(): - if event.type == QUIT or ( - event.type == KEYDOWN and event.key == K_ESCAPE - ): - pygame.quit() - sys.exit() + self.check_quit_event(event) if self.is_tap_event(event): - if player_y + playerHeight >= self.base_y - 1: + if self.player.y + self.player.height >= self.floor.y - 1: return - # player y shift - if player_y + playerHeight < self.base_y - 1: - player_y += min( - playerVelY, self.base_y - player_y - playerHeight - ) - - # player velocity change - if playerVelY < 15: - playerVelY += playerAccY - - # rotate only when it's a pipe crash - if not crashInfo["groundCrash"]: - if playerRot > -90: - playerRot -= playerVelRot - # draw sprites - self.screen.blit(self.images.background, (0, 0)) - - for uPipe, lPipe in zip(upperPipes, lowerPipes): - self.screen.blit(self.images.pipe[0], (uPipe["x"], uPipe["y"])) - self.screen.blit(self.images.pipe[1], (lPipe["x"], lPipe["y"])) - - self.screen.blit(self.images.base, (base_x, self.base_y)) - self.show_score(score) - - playerSurface = pygame.transform.rotate( - self.images.player[1], playerRot - ) - self.screen.blit(playerSurface, (playerx, player_y)) - self.screen.blit(self.images.gameover, (50, 180)) + 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) pygame.display.update() await asyncio.sleep(0) - - def get_random_pipe(self): - """returns a randomly generated pipe""" - # y of gap between upper and lower pipe - gapY = random.randrange(0, int(self.base_y * 0.6 - self.pipe_gap)) - gapY += int(self.base_y * 0.2) - pipeHeight = self.images.pipe[0].get_height() - pipeX = self.window.width + 10 - - return [ - {"x": pipeX, "y": gapY - pipeHeight}, # upper pipe - {"x": pipeX, "y": gapY + self.pipe_gap}, # lower pipe - ] - - def show_score(self, score): - """displays score in center of screen""" - scoreDigits = [int(x) for x in list(str(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() - - def check_crash(self, player, upperPipes, lowerPipes): - """returns True if player collides with base or pipes.""" - pi = player["index"] - player["w"] = self.images.player[0].get_width() - player["h"] = self.images.player[0].get_height() - - # if player crashes into ground - if player["y"] + player["h"] >= self.base_y - 1: - return [True, True] - else: - - playerRect = pygame.Rect( - player["x"], player["y"], player["w"], player["h"] - ) - pipeW = self.images.pipe[0].get_width() - pipeH = self.images.pipe[0].get_height() - - for uPipe, lPipe in zip(upperPipes, lowerPipes): - # upper and lower pipe rects - uPipeRect = pygame.Rect(uPipe["x"], uPipe["y"], pipeW, pipeH) - lPipeRect = pygame.Rect(lPipe["x"], lPipe["y"], pipeW, pipeH) - - # player and upper/lower pipe hitmasks - pHitMask = self.hit_masks.player[pi] - uHitmask = self.hit_masks.pipe[0] - lHitmask = self.hit_masks.pipe[1] - - # if bird collided with upipe or lpipe - uCollide = pixel_collision( - playerRect, uPipeRect, pHitMask, uHitmask - ) - lCollide = pixel_collision( - playerRect, lPipeRect, pHitMask, lHitmask - ) - - if uCollide or lCollide: - return [True, False] - - return [False, False] diff --git a/src/sprites/__init__.py b/src/sprites/__init__.py new file mode 100644 index 0000000..b319d8e --- /dev/null +++ b/src/sprites/__init__.py @@ -0,0 +1,19 @@ +from .background import Background +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__ = [ + "Background", + "Floor", + "Pipe", + "Pipes", + "Player", + "Score", + "Sprite", + "WelcomeMessage", +] diff --git a/src/sprites/background.py b/src/sprites/background.py new file mode 100644 index 0000000..4726f71 --- /dev/null +++ b/src/sprites/background.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..85d71db --- /dev/null +++ b/src/sprites/floor.py @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..945a21d --- /dev/null +++ b/src/sprites/game_over.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..d144ba4 --- /dev/null +++ b/src/sprites/pipe.py @@ -0,0 +1,112 @@ +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/player.py b/src/sprites/player.py new file mode 100644 index 0000000..764bb55 --- /dev/null +++ b/src/sprites/player.py @@ -0,0 +1,193 @@ +from enum import Enum +from itertools import cycle + +import pygame + +from ..utils import clamp, pixel_collision +from .floor import Floor +from .pipe import Pipe, Pipes +from .sprite import Sprite + + +class PlayerMode(Enum): + SHM = "SHM" + NORMAL = "NORMAL" + CRASH = "CRASH" + + +class Player(Sprite): + def setup(self) -> None: + 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() + elif mode == PlayerMode.SHM: + self.reset_vals_shm() + elif mode == PlayerMode.CRASH: + self.sounds.hit.play() + if self.crash_entity == "pipe": + self.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 + self.min_vel_y = -8 # min vel along Y, max ascend speed + self.acc_y = 1 # players downward acceleration + + self.rot = 45 # player's current rotation + self.vel_rot = -3 # player's rotation speed + self.rot_min = -90 # player's min rotation angle + self.rot_max = 20 # player's max rotation angle + + self.flap_acc = -9 # players speed on flapping + self.flapped = False # True when player flaps + + def reset_vals_shm(self) -> None: + self.vel_y = 1 # player's velocity along Y axis + self.max_vel_y = 4 # max vel along Y, max descend speed + self.min_vel_y = -4 # min vel along Y, max ascend speed + self.acc_y = 0.5 # players downward acceleration + + self.rot = 0 # player's current rotation + self.vel_rot = 0 # player's rotation speed + self.rot_min = 0 # player's min rotation angle + self.rot_max = 0 # player's max rotation angle + + self.flap_acc = 0 # players speed on flapping + self.flapped = False # True when player flaps + + def reset_vals_crash(self) -> None: + self.acc_y = 2 + self.vel_y = 7 + self.max_vel_y = 15 + + def update_img_idx(self): + self.frame += 1 + if self.frame % 5 == 0: + self.img_idx = next(self.img_gen) + + def tick_shm(self) -> None: + if self.vel_y >= self.max_vel_y or self.vel_y <= self.min_vel_y: + self.acc_y *= -1 + 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 + + 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 + ) + + # player velocity change + if self.vel_y < self.max_vel_y: + self.vel_y += self.acc_y + + # rotate only when it's a pipe crash + if self.crash_entity != "floor": + self.rotate() + + 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() + if self.mode == PlayerMode.SHM: + self.tick_shm() + elif self.mode == PlayerMode.NORMAL: + self.tick_normal() + self.rotate() + elif self.mode == PlayerMode.CRASH: + self.tick_crash() + + 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)) + + def flap(self) -> None: + self.vel_y = self.flap_acc + self.flapped = True + self.rot = 45 + self.sounds.wing.play() + + def crossed(self, pipe: Pipe) -> bool: + return pipe.mid_x <= self.mid_x < pipe.mid_x + 4 + + def collided(self, pipes: Pipes, floor: Floor) -> bool: + """returns True if player collides with base or pipes.""" + + # if player crashes into ground + if self.y + self.height >= floor.y - 1: + 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 + ) + + # 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 diff --git a/src/sprites/score.py b/src/sprites/score.py new file mode 100644 index 0000000..03a7e14 --- /dev/null +++ b/src/sprites/score.py @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..35fdfc6 --- /dev/null +++ b/src/sprites/sprite.py @@ -0,0 +1,43 @@ +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 new file mode 100644 index 0000000..7f2dfd7 --- /dev/null +++ b/src/sprites/welcome_message.py @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000..5f13a3b --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,12 @@ +from .hit_mask import HitMask +from .images import Images +from .sounds import Sounds +from .utils import clamp, pixel_collision +from .window import Window + +__all__ = [ + "HitMask", + "Images", + "Sounds", + "Window", +] diff --git a/src/constants.py b/src/utils/constants.py similarity index 100% rename from src/constants.py rename to src/utils/constants.py diff --git a/src/hit_mask.py b/src/utils/hit_mask.py similarity index 100% rename from src/hit_mask.py rename to src/utils/hit_mask.py diff --git a/src/images.py b/src/utils/images.py similarity index 100% rename from src/images.py rename to src/utils/images.py diff --git a/src/sounds.py b/src/utils/sounds.py similarity index 100% rename from src/sounds.py rename to src/utils/sounds.py diff --git a/src/utils.py b/src/utils/utils.py similarity index 81% rename from src/utils.py rename to src/utils/utils.py index 1713f57..5d5fa33 100644 --- a/src/utils.py +++ b/src/utils/utils.py @@ -23,3 +23,8 @@ 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 new file mode 100644 index 0000000..6148c57 --- /dev/null +++ b/src/utils/window.py @@ -0,0 +1,6 @@ +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