Improve code by adding a generic entity object

This commit is contained in:
Sourabh Verma 2023-06-19 03:49:14 +05:30
parent d3100119f2
commit 4960be1759
No known key found for this signature in database
GPG Key ID: 117177475D1726CF
24 changed files with 408 additions and 406 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ build
.python-version
*.egg-info
.vscode
.DS_Store

View File

@ -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",
]

View 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
View 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
View 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
View 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
View 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

View File

@ -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
View 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()

View 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),
)

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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))

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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))

View File

@ -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
View 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)

View File

@ -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())
)
)

View File

@ -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

View File

@ -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)

View File

@ -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