diff --git a/mod_pygame/phantom_castle.py b/mod_pygame/phantom_castle.py index 081dcae..5b16b7e 100644 --- a/mod_pygame/phantom_castle.py +++ b/mod_pygame/phantom_castle.py @@ -32,6 +32,9 @@ from typing import NamedTuple, Optional import pygame +FONT_NAME = "Arial" +FPS = 30 + def download_asset(asset, path): """ @@ -59,7 +62,7 @@ def make_stub_image(path, name): rect = pygame.Rect(5, 5, 190, 190) pygame.draw.rect(img, "black", rect, 3) - font = pygame.font.SysFont("Arial", 44) + font = pygame.font.SysFont(FONT_NAME, 44) text1 = font.render(name, True, "blue") text1_rect = text1.get_rect() text1_rect.center = img.get_rect().center @@ -174,7 +177,7 @@ def maze_gen(row=4, col=4): or 1 == maze[r1][c1] == maze[r2][c2] ) ] - if len(nexts): + if nexts: r1, c1, r2, c2 = choice(nexts) maze[r1][c1] = maze[r2][c2] = 0 path.append((r2, c2)) @@ -199,14 +202,14 @@ def maze_gen(row=4, col=4): None, ), } - for key in upd: - for i in range(len(maze) - len(key) + 1): - for j in range(len(maze[0]) - len(key[0]) + 1): + for pattern, replacement in upd.items(): + for i in range(len(maze) - len(pattern) + 1): + for j in range(len(maze[0]) - len(pattern[0]) + 1): if all( maze[i + k][j : j + len(v)] == list(map(int, v)) - for k, v in enumerate(key) + for k, v in enumerate(pattern) ): - for k, v in filter(lambda x: x[1], enumerate(upd[key])): + for k, v in filter(lambda x: x[1], enumerate(replacement)): maze[i + k][j : j + len(v)] = list(map(int, v)) return maze @@ -222,8 +225,27 @@ class Direction(Enum): UP = 3 DOWN = 4 + def as_coords(self): + match self: + case Direction.LEFT: + return Coords(-1, 0) + case Direction.RIGHT: + return Coords(1, 0) + case Direction.UP: + return Coords(0, -1) + case Direction.DOWN: + return Coords(0, 1) -class GameObject(ABC): + +class SurfaceWithRect(NamedTuple): + surface: pygame.Surface + rect: pygame.Rect + + def draw_to(self, target: pygame.Surface): + target.blit(self.surface, self.rect) + + +class DrawableGameObject(ABC): """обобщение игрового элемента""" coords = property( @@ -233,7 +255,7 @@ class GameObject(ABC): def __init__( self, coords: Coords, - parent: Optional["GameObject"] = None, + parent: Optional["DrawableGameObject"] = None, assets: dict | None = None, ): self.parent = parent @@ -265,17 +287,20 @@ class GameObject(ABC): def draw(self): pass - def handle_event(self, event: pygame.event.Event): + +class EventHandler(ABC): + @abstractmethod + def handle_event(self): pass -class Hero(GameObject): +class Hero(DrawableGameObject, EventHandler): """объект главного героя""" def __init__( self, coords: Coords, - parent: GameObject, + parent: DrawableGameObject, assets: dict | None = None, ): super().__init__(coords, parent, assets) @@ -296,7 +321,7 @@ class Hero(GameObject): def _check_collision(self, coords): """Проверка пересечения со стенами""" - new_rect = self.rect.copy() + new_rect = self.surface.get_bounding_rect() new_rect.topleft = coords new_rect.scale_by_ip(0.99, 0.99) return self.scene.walls.check_collision(new_rect) @@ -336,46 +361,25 @@ class Hero(GameObject): def flip(self): self.looking_right = not self.looking_right - self._surface = pygame.transform.flip(self.surface, True, False) + self._surface = pygame.transform.flip(self.surface, flip_x=True, flip_y=False) - def update_direction(self, direction: Coords): - if direction.x != 0: - going_right = direction.x > 0 + def update_direction(self, direction: Direction): + if direction in (Direction.LEFT, Direction.RIGHT): + going_right = direction == Direction.RIGHT if self.looking_right != going_right: self.flip() - if direction.x < 0: - new_direction = Direction.LEFT - elif direction.x > 0: - new_direction = Direction.RIGHT - elif direction.y < 0: - new_direction = Direction.UP - elif direction.y > 0: - new_direction = Direction.DOWN - - if new_direction != self.direction: + if direction != self.direction: self.speed = 0 - self.direction = new_direction + self.direction = direction else: self.speed += 1 - def move(self, direction: Coords, step: int = 1): + def move(self, direction: Direction, step: int = 1): self.update_direction(direction) - self.coords += direction * step * self.speed // 3 + self.coords += direction.as_coords() * step * self.speed // 3 self.scene.coins.collect(self.rect) - def move_left(self, step: int = 1): - self.move(Coords(-1, 0), step) - - def move_right(self, step: int = 1): - self.move(Coords(1, 0), step) - - def move_up(self, step: int = 1): - self.move(Coords(0, -1), step) - - def move_down(self, step: int = 1): - self.move(Coords(0, 1), step) - def handle_event(self, event: pygame.event.Event): if not self.active: return @@ -384,30 +388,30 @@ class Hero(GameObject): if event.type == pygame.KEYDOWN: match event.key: case pygame.K_UP: - self.move_up(wide) + self.move(Direction.UP, wide) case pygame.K_DOWN: - self.move_down(wide) + self.move(Direction.DOWN, wide) case pygame.K_LEFT: - self.move_left(wide) + self.move(Direction.LEFT, wide) case pygame.K_RIGHT: - self.move_right(wide) + self.move(Direction.RIGHT, wide) case pygame.K_w: - self.move_up(short) + self.move(Direction.UP, short) case pygame.K_s: - self.move_down(short) + self.move(Direction.DOWN, short) case pygame.K_a: - self.move_left(short) + self.move(Direction.LEFT, short) case pygame.K_d: - self.move_right(short) + self.move(Direction.RIGHT, short) -class WallBlock(GameObject): +class WallBlock(DrawableGameObject): """объект элемента стены""" def __init__( self, coords: Coords, - parent: GameObject, + parent: DrawableGameObject, assets: dict | None = None, ): super().__init__(coords, parent, assets) @@ -422,12 +426,12 @@ class WallBlock(GameObject): self.parent.surface.blit(self.surface, self.rect) -class Walls(GameObject): +class Walls(DrawableGameObject): """объект стен""" def __init__( self, - parent: GameObject, + parent: DrawableGameObject, maze: list[list[int]], box_sz: Coords, assets: dict | None = None, @@ -452,13 +456,13 @@ class Walls(GameObject): return False -class Coin(GameObject): +class Coin(DrawableGameObject): """объект монетки""" def __init__( self, coords: Coords, - parent: GameObject, + parent: DrawableGameObject, assets: dict | None = None, ): super().__init__(coords, parent, assets) @@ -472,13 +476,19 @@ class Coin(GameObject): def draw(self): self.parent.surface.blit(self.surface, self.rect) + @property + def bounding_rect(self) -> pygame.Rect: + new_rect = self.surface.get_bounding_rect() + new_rect.center = self.rect.center + return new_rect -class Coins(GameObject): + +class Coins(DrawableGameObject): """объект коллекции монеток""" def __init__( self, - parent: GameObject, + parent: DrawableGameObject, maze: list[list[int]], box_sz: Coords, count: int, @@ -504,26 +514,26 @@ class Coins(GameObject): self.collected_coins = [] # Надпись, если все монетки собраны - font = pygame.font.SysFont("Arial", 30) + font = pygame.font.SysFont(FONT_NAME, 30) text = "Все монетки собраны!" self.done_txt = font.render(text, 1, "#050366e3") self.done_txt_rect = self.done_txt.get_rect() self.done_txt_rect.topleft = Coords(10, 10) @property - def capacity(self): + def capacity(self) -> int: return self._capacity @property - def coins_left(self): + def coins_left(self) -> int: return len(self.coins) @property - def coins_collected(self): + def coins_collected(self) -> int: return self.capacity - self.coins_left @property - def all_collected(self): + def all_collected(self) -> int: return self.coins_left == 0 def draw(self): @@ -543,48 +553,81 @@ class Coins(GameObject): self.collected_coins.append(coin) def collect(self, rect: pygame.Rect): - mined = [*filter(lambda coin: coin.rect.colliderect(rect), self.coins)] + mined = [*filter(lambda coin: coin.bounding_rect.colliderect(rect), self.coins)] for coin in mined: self.coins.remove(coin) self.add_to_collected(coin) -class EndLevel(GameObject): - def __init__(self, scene: GameObject): +class EndLevelMenu(DrawableGameObject, EventHandler): + def __init__(self, scene: DrawableGameObject): super().__init__(Coords.zero(), scene, scene.assets) - self.image = pygame.image.load(scene.assets["win.png"]) + self._surface, self.rect = self._create_end_game_label() + self.win_image = self._create_win_image() + self.win_label = self._create_win_label() + self.keys_hint = self._create_keys_hint() + self.stats_label = None self.active = False - # надпись завершения игры - font = pygame.font.SysFont("Arial", 70) + def _create_end_game_label(self) -> SurfaceWithRect: + """Надпись завершения игры""" + font = pygame.font.SysFont(FONT_NAME, 70) text = "Конец игры!" - self._surface = font.render(text, 1, "#1b10a8c4") - self.rect = self._surface.get_rect() - self.rect.center = self.parent.rect.center + surface = font.render(text, 1, "#1b10a8c4") + rect = surface.get_rect() + rect.center = Coords(*self.parent.rect.center) + Coords(0, 38) + return SurfaceWithRect(surface, rect) - # совет по кнопкам - hint = "Для новой игры нажмите N, для выхода Q" - font_hint = pygame.font.SysFont("Arial", 27) - self.hint = font_hint.render(hint, 1, "#24053da4") - self.hint_rect = self.hint.get_rect() - self.hint_rect.center = self.parent.rect.center - self.hint_rect = self.hint_rect.move(Coords(0, 300)) + def _create_keys_hint(self) -> SurfaceWithRect: + """Совет по кнопкам""" + hint_text = "Для новой игры нажмите N, для выхода Q" + font_hint = pygame.font.SysFont(FONT_NAME, 27) + surface = font_hint.render(hint_text, 1, "#24053da4") + rect = surface.get_rect() + rect.center = self.parent.rect.center + rect = rect.move(Coords(0, 220)) + return SurfaceWithRect(surface, rect) - # Надпись для хорошего финала + def _create_win_label(self) -> SurfaceWithRect: + """Надпись для хорошего финала""" + font = pygame.font.SysFont(FONT_NAME, 33) text = "Все монетки собраны!" - self.goodtxt = font_hint.render(text, 1, "#96081ba4") - self.goodtxt_rect = self.goodtxt.get_rect() - self.goodtxt_rect.center = self.parent.rect.center - self.goodtxt_rect = self.goodtxt_rect.move(Coords(0, -100)) + surface = font.render(text, 1, "#96081ba4") + rect = surface.get_rect() + rect.center = self.parent.rect.center + rect.move_ip(Coords(0, -200)) + return SurfaceWithRect(surface, rect) + + def _create_win_image(self) -> SurfaceWithRect: + """Картинка для хорошего финала""" + surface = pygame.image.load(self.scene.assets["win.png"]) + rect = surface.get_rect() + rect.center = self.parent.rect.center + return SurfaceWithRect(surface, rect) + + def _create_stats_label(self) -> SurfaceWithRect: + """Общая статистика игры""" + stats_text = f"Всего пройдено уровней: {self.scene.total_levels}, собрано монет: {self.scene.total_coins}" + stats_font = pygame.font.SysFont(FONT_NAME, 27) + surface = stats_font.render(stats_text, 1, "#031f03a4") + rect = surface.get_rect() + rect.center = Coords(*self.scene.rect.center) + Coords(0, 350) + return SurfaceWithRect(surface, rect) def draw(self): if not self.active: return if self.scene.coins.all_collected: - self.parent.surface.blit(self.image, self.rect) - self.parent.surface.blit(self.goodtxt, self.goodtxt_rect) + self.win_image.draw_to(self.parent.surface) + self.win_label.draw_to(self.parent.surface) + self.parent.surface.blit(self.surface, self.rect) - self.parent.surface.blit(self.hint, self.hint_rect) + self.keys_hint.draw_to(self.parent.surface) + + # статистика + if self.stats_label is None: + self.stats_label = self._create_stats_label() + self.stats_label.draw_to(self.parent.surface) def handle_event(self, event: pygame.event.Event): if not self.active: @@ -595,7 +638,7 @@ class EndLevel(GameObject): self.parent.done = True -class Scene(GameObject): +class Scene(DrawableGameObject, EventHandler): """основной игровой объект""" # кнопки для выхода из игры @@ -612,10 +655,12 @@ class Scene(GameObject): self.box_sz = box_sz self._surface = pygame.display.set_mode(screen_sz) self.surface.fill("white") - self.rect = self._surface.get_rect() + self.rect = self.surface.get_rect() self.background = pygame.image.load(self.assets["bg1k.png"]) self.background = pygame.transform.scale(self.background, self.rect.size) + self.total_levels, self.total_coins = 0, 0 + hero_sz = Coords(*map(int, box_sz * 0.8)) hero_y_offset = (box_sz.y - hero_sz.y) // 2 + box_sz.y self.hero = Hero(Coords(0, hero_y_offset), self) @@ -626,31 +671,27 @@ class Scene(GameObject): self.walls = Walls(self, self.maze, box_sz) self.coins = Coins(self, self.maze, box_sz, coins_count) - self.end = EndLevel(self) + self.end = EndLevelMenu(self) self.end.active = False self.want_new_level = False self.exit_rect = self.get_exit_rect() - # #для тестирования экрана конца уровня - # self.hero.coords = Coords(*self.exit_rect.topleft) + Coords( - # -self.box_sz.x // 2, 5 - # ) def get_exit_rect(self) -> pygame.Rect: # находим клетку в которой будет выход с карты maze_sz = get_maze_sz(self.maze) coords = (maze_sz - Coords(1, 2)) * self.box_sz rect = pygame.Rect(coords, coords) - rect.width, rect.height = self.box_sz - # уменьшаем размер клетки и перемещаем её вправо - rect.width = self.box_sz.x // 4 - rect = rect.move(Coords(self.box_sz.x // 2, 0)) - return rect + rect.width, rect.height = 1, self.box_sz.y + return rect.move((self.box_sz.x, 0)) def check_level_completed(self): - self.level_completed = self.exit_rect.colliderect(self.hero.rect) - if self.level_completed: + level_completed = self.exit_rect.colliderect(self.hero.rect) + if level_completed and not self.level_completed: + self.total_coins += self.coins.coins_collected + self.total_levels += 1 self.end.active = True self.hero.active = False + self.level_completed = True def draw(self): if self.done: @@ -694,7 +735,7 @@ class Scene(GameObject): self.handle_event(event) self.draw() pygame.display.flip() - clock.tick(30) + clock.tick(FPS) def game(assets): @@ -703,25 +744,31 @@ def game(assets): coins_count = 10 pygame.display.set_caption("Призрачный лабиринт: сокровища небесного замка") + total_levels = 0 + total_coins = 0 want_new_level = True while want_new_level: scene = Scene(assets, screen_sz, maze_sz, coins_count) + scene.total_levels, scene.total_coins = total_levels, total_coins scene.event_loop() + want_new_level = scene.want_new_level + total_levels = scene.total_levels + total_coins = scene.total_coins pygame.quit() def main(): pygame.init() - assets = [ + required_assets = [ "bg1k.png", "ghost.png", "brick.png", "win.png", "coin.png", ] - with get_assets(assets) as assets: + with get_assets(required_assets) as assets: game(assets)