From: Thomas Rimelen Date: Thu, 8 Oct 2026 13:47:03 +0000 (+0200) Subject: Levels and pacman respawning X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=ad8669143dc0621493bfc71932bd4e3c1a2fc081;p=axy%2Fft%2Fpacman.git Levels and pacman respawning --- diff --git a/old_src/__main__.py b/old_src/__main__.py deleted file mode 100644 index 314fc41..0000000 --- a/old_src/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from pacman import main - -if __name__ == "__main__": - main() diff --git a/old_src/pacman/__init__.py b/old_src/pacman/__init__.py deleted file mode 100644 index 8b82c04..0000000 --- a/old_src/pacman/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys - -from pacman.config import load_config -from pacman.error import error -from pacman.game import Game - - -def main() -> None: - try: - file_path = sys.argv[1] - except IndexError: - file_path = "" - config = load_config(file_path) - try: - Game(config) - except KeyboardInterrupt: - error("keyboardInterrupt.") diff --git a/old_src/pacman/collectibles.py b/old_src/pacman/collectibles.py deleted file mode 100644 index 9e472f2..0000000 --- a/old_src/pacman/collectibles.py +++ /dev/null @@ -1,64 +0,0 @@ -import random -from abc import ABC - -from mazegenerator import MazeGenerator - -from pacman.config import Configuration -from pacman.player import Player - - -class Collectibles(ABC): - def __init__(self, point: int, size: int) -> None: - self.point = point - self.size = size - - def collect(self) -> int: - return self.point - - -class Pacgum(Collectibles): - def __init__(self, config: Configuration): - point = config.points_per_pacgum - super().__init__(point, 3) - - -class SuperPacgum(Collectibles): - def __init__(self, config: Configuration): - point = config.points_per_pacgum - super().__init__(point, 6) - - -class CollectableManager: - def __init__(self) -> None: - self.collectibles: dict[tuple[int, int], Collectibles] = {} - - def fill( - self, config: Configuration, maze: MazeGenerator, player: Player - ) -> None: - collectibles: dict[tuple[int, int], Collectibles] = {} - h = maze._height - w = maze._width - for i in range(w): - for j in range(h): - if (i, j) == player.coordonate or maze.maze[j][i] == 15: - continue - if (i, j) in [(0, 0), (h - 1, 0), (0, w - 1), (h - 1, w - 1)]: - collectibles[(i, j)] = SuperPacgum(config) - else: - if random.randint(0, 100) < config.pacgum: - collectibles[(i, j)] = Pacgum(config) - - self.collectibles = collectibles - - def collect(self, pos: tuple[int, int]) -> tuple[int, bool]: - point = 0 - hunt = False - if self.collectibles.get(pos): - pacgum = self.collectibles.pop(pos) - point = pacgum.collect() - if isinstance(pacgum, SuperPacgum): - hunt = True - return (point, hunt) - - def is_empty(self) -> bool: - return len(self.collectibles) == 0 diff --git a/old_src/pacman/config.py b/old_src/pacman/config.py deleted file mode 100644 index f00cd6c..0000000 --- a/old_src/pacman/config.py +++ /dev/null @@ -1,81 +0,0 @@ -import json -from pathlib import Path - -from pydantic import BaseModel, Field, ValidationError - -from pacman.error import error - -CELL_SIZE = 64 - - -class Configuration(BaseModel): - highscore_filename: str = Field(default="score.json") - levels: int = Field(ge=1, le=256, default=10) - width: int = Field(ge=5, le=20, default=13) - height: int = Field(ge=5, le=20, default=13) - lives: int = Field(ge=1, le=99, default=3) - pacgum: int = Field(ge=10, le=100, default=80) - points_per_pacgum: int = Field(ge=1, le=99, default=1) - points_per_super_pacgum: int = Field(ge=1, le=99, default=5) - points_per_ghost: int = Field(ge=1, le=99, default=20) - cheat_mod: bool = Field(default=False) - time: int = Field(ge=60, le=300, default=180) - - -def load_config(path: str) -> Configuration: - if not path: - error("no file path given, default configuration applied.", False) - return Configuration() - - file_path = Path(path) - - if not file_path.exists(): - error( - f"file not found: '{path}', default configuration applied.", False - ) - return Configuration() - - text = "" - for item in file_path.read_text(encoding="utf-8").split("\n"): - if not item.strip().startswith("#") and not item.strip().startswith( - "//" - ): - if text: - text += "\n" - text += item - - try: - data = json.loads(text) - except json.JSONDecodeError: - error( - f"invalid JSON in {path}, default configuration applied..", False - ) - return Configuration() - - try: - config = Configuration.model_validate(data) - - except ValidationError as err: - errors = err.errors() - - for e in errors: - field = e["loc"][0] - - if e["type"] == "missing": - error( - f"missing '{field}' configuration, defaultvalue applied", - False, - ) - else: - error( - f"wrong value for '{field}' configuration, " - "default value applied", - False, - ) - - for e in errors: - data.pop(e["loc"][0], None) - - config = Configuration.model_validate(data) - - return config diff --git a/old_src/pacman/error.py b/old_src/pacman/error.py deleted file mode 100644 index b597bae..0000000 --- a/old_src/pacman/error.py +++ /dev/null @@ -1,28 +0,0 @@ -import sys - - -def error(err: str, close: bool = True, line: int = 0) -> None: - """ - Display an error message to the standard error stream and terminate - the program with exit code 1. - - Args: - err (str): Description of the error. - line (int, optional): Line number where the error occurred. - If provided and greater than 0, it is included in the error - message. Defaults to 0. - - Returns: - NoReturn: This function never returns because it terminates - the program. - - Raises: - SystemExit: Always raised with exit code 1. - """ - if line: - sys.stderr.write("\nError in line " + str(line) + ": " + err + "\n") - else: - sys.stderr.write("\nError: " + err + "\n") - sys.stderr.flush() - if close: - exit(1) diff --git a/old_src/pacman/game.py b/old_src/pacman/game.py deleted file mode 100644 index cc4e81e..0000000 --- a/old_src/pacman/game.py +++ /dev/null @@ -1,162 +0,0 @@ -import time - -import pygame -from mazegenerator import MazeGenerator - -from .collectibles import CollectableManager -from .config import Configuration -from .input import EndingInput, GameInput, Input, MenuInput -from .player import Player -from .render import ( - EndingRenderer, - GameRenderer, - MenuRenderer, - Renderer, - RenderInfo, -) -from .score import ScoreManager - - -class Game: - def __init__(self, config: Configuration): - self.config = config - self.screen = pygame.display.set_mode((2560, 1440)) - self.score_manager = ScoreManager(config.highscore_filename) - self.score = 0 - self.lives = config.lives - self.maze = MazeGenerator( - (self.config.height, self.config.width), False, (0, 0), (1, 1), 42 - ) - self.level = 0 - self.render_info = self.get_render_info() - self.renderer: Renderer = MenuRenderer( - self.screen, self.render_info, self.score_manager - ) - self.input: Input = MenuInput(self.renderer, self.new_game, self.quit) - self.collectibles = CollectableManager() - self.player = Player(self.maze.maze) - self.running = True - self.start_time = 0.0 - self.now_time = 0.0 - self.run() - - def add_collectibles(self) -> None: - self.collectibles.fill(self.config, self.maze, self.player) - - def finish_game(self, victory: bool) -> None: - self.renderer = EndingRenderer(self.screen, self.render_info, victory) - self.input = EndingInput(self.renderer, self.add_score) - - def win(self) -> None: - self.finish_game(True) - - def loose(self) -> None: - self.finish_game(False) - - def time_out(self) -> None: - if isinstance(self.renderer, GameRenderer): - self.renderer.time_out() - pygame.display.flip() - time.sleep(3) - self.loose() - - def update_time(self) -> None: - self.now_time = time.time() - remain_time = self.config.time - (self.now_time - self.start_time) - self.render_info.remain_time = round(remain_time, 1) - if remain_time <= 0: - self.time_out() - - def restart(self) -> None: - self.renderer = MenuRenderer( - self.screen, self.render_info, self.score_manager - ) - self.input = MenuInput(self.renderer, self.new_game, self.quit) - - def get_render_info(self) -> RenderInfo: - return RenderInfo( - fps=0, - width=self.config.width, - height=self.config.height, - maze=self.maze.maze, - level=self.level, - score=self.score, - lives=self.lives, - remain_time=0.0, - ) - - def next_level(self) -> None: - if self.level == 10: - self.win() - return - self.level += 1 - self.maze.generate() - self.render_info.level = self.level - self.render_info.maze = self.maze.maze - self.player.next_level((1, 1), self.maze.maze) - if isinstance(self.renderer, GameRenderer): - self.renderer.next_level() - self.add_collectibles() - self.start_time = time.time() - - def new_game(self) -> None: - self.score = 0 - self.level = 0 - self.lives = self.config.lives - self.renderer = GameRenderer( - self.screen, self.render_info, self.player, self.collectibles - ) - self.input = GameInput(self.player, self.next_level, self.kill_player) - self.next_level() - - def hunt(self) -> None: - print("Hunting ghost") - pass - - def kill_player(self) -> None: - self.lives -= 1 - self.render_info.lives = self.lives - if self.lives == 0: - self.loose() - else: - self.player.reset() - - def pick_up(self) -> None: - point, hunt = self.collectibles.collect(self.player.coordonate) - self.score += point - self.render_info.score = self.score - if hunt: - self.hunt() - if self.collectibles.is_empty(): - self.next_level() - - def quit(self) -> None: - self.running = False - - def run(self) -> None: - pygame.init() - pygame.font.init() - clock = pygame.time.Clock() - - while self.running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.running = False - self.input.get_event(event) - keys = pygame.key.get_pressed() - self.input.get_key(keys) - self.render_info.fps = round(clock.get_fps(), 2) - if isinstance(self.input, GameInput): - self.player.move() - self.update_time() - if self.player.can_move(): - self.pick_up() - self.screen.fill("black") - self.renderer.render() - pygame.display.flip() - clock.tick(60) - pygame.quit() - - def add_score(self, name: str) -> None: - self.score_manager.add_score(name, self.score) - self.restart() diff --git a/old_src/pacman/input.py b/old_src/pacman/input.py deleted file mode 100644 index c7ab3da..0000000 --- a/old_src/pacman/input.py +++ /dev/null @@ -1,113 +0,0 @@ -import time -from abc import ABC -from collections.abc import Callable -from typing import override - -import pygame - -from pacman.player import Player -from pacman.render import EndingRenderer, MenuRenderer - - -class Input(ABC): - def get_event(self, event: pygame.event.Event) -> None: - return None - - def get_key(self, keys: pygame.key.ScancodeWrapper) -> None: - return None - - -class MenuInput(Input): - def __init__( - self, - renderer: MenuRenderer, - new_game: Callable[[], None], - quit: Callable[[], None], - ) -> None: - self.renderer = renderer - self.index = 0 - self.functions = { - 0: new_game, - 1: quit, - } - self.cooldown = 0.0 - - def change_index(self, value: int) -> None: - self.index = max(0, min(1, self.index + value)) - self.renderer.index = self.index - self.cooldown = time.time() - - def validation(self) -> None: - self.functions[self.index]() - - @override - def get_event(self, event: pygame.event.Event) -> None: - if event.type == pygame.KEYDOWN: - if event.key in [1073741906, 119]: - self.change_index(-1) - elif event.key in [1073741905, 115]: - self.change_index(1) - elif event.key in [13, 32]: - self.validation() - - -class GameInput(Input): - def __init__( - self, - player: Player, - next_lvl: Callable[[], None], - kill: Callable[[], None], - ): - self.player = player - self.next_level = next_lvl - self.kill = kill - self.next_move: tuple[int, int] | None = None - - @override - def get_key(self, keys: pygame.key.ScancodeWrapper) -> None: - if keys[pygame.K_UP] or keys[pygame.K_w]: - self.next_move = (0, -1) - elif keys[pygame.K_DOWN] or keys[pygame.K_s]: - self.next_move = (0, 1) - elif keys[pygame.K_LEFT] or keys[pygame.K_a]: - self.next_move = (-1, 0) - elif keys[pygame.K_RIGHT] or keys[pygame.K_d]: - self.next_move = (1, 0) - if self.player.can_move() and self.next_move: - self.player.go_to(self.next_move) - self.next_move = None - - @override - def get_event(self, event: pygame.event.Event) -> None: - if event.type == pygame.KEYDOWN: - if event.unicode == "n": - self.next_level() - if event.unicode == "k": - self.kill() - - -class EndingInput(Input): - def __init__( - self, renderer: EndingRenderer, add_score: Callable[[str], None] - ) -> None: - self.renderer = renderer - self.add_score = add_score - self.name = "" - - @override - def get_event(self, event: pygame.event.Event) -> None: - if event.type == pygame.TEXTINPUT: - c: str = event.text - if c.isalnum(): - if len(self.name) < 10: - self.name += event.text - elif event.type == pygame.KEYDOWN: - if event.key == 8: - if self.name: - self.name = self.name[0:-1] - elif event.key == 32: - if len(self.name) < 10: - self.name += " " - elif event.key == 13: - self.add_score(self.name) - self.renderer.name = self.name diff --git a/old_src/pacman/player.py b/old_src/pacman/player.py deleted file mode 100644 index 75819d1..0000000 --- a/old_src/pacman/player.py +++ /dev/null @@ -1,85 +0,0 @@ -import pygame - -from pacman.config import CELL_SIZE as CELL_SIZE - - -class Player: - def __init__(self, maze: list[list[int]]): - self.position = (0, 0) - self.coordonate = (0, 0) - self.direction = (0, 0) - self.start_coordonate = (0, 0) - self.speed = 4 - self.maze = maze - self.spritesheet = self.load_player_sprites() - self.sprite = self.spritesheet[(1, 0)] - - def update_position(self) -> None: - self.position = ( - self.coordonate[0] * CELL_SIZE, - self.coordonate[1] * CELL_SIZE, - ) - - def next_level(self, pos: tuple[int, int], maze: list[list[int]]) -> None: - self.maze = maze - self.start_coordonate = pos - self.reset() - - def reset(self) -> None: - self.sprite = self.spritesheet[(1, 0)] - self.direction = (0, 0) - self.coordonate = self.start_coordonate - self.update_position() - - @staticmethod - def load_player_sprites() -> dict[tuple[int, int], pygame.Surface]: - player_textures = {} - for t, l in [ - ((0, 1), "D"), - ((0, -1), "U"), - ((1, 0), "R"), - ((-1, 0), "L"), - ]: - filename = "asset/player/player_" + str(l) + "A.png" - img = pygame.image.load(filename) - player_textures[t] = img - return player_textures - - def get_sprite(self) -> pygame.Surface: - if self.direction != (0, 0): - self.sprite = self.spritesheet[self.direction] - return self.sprite - - @staticmethod - def binary(cell: int) -> list[int]: - b = bin(cell)[2:].zfill(4) - r = [1 if i == "1" else 0 for i in b] - return r[::-1] - - def can_go(self, dir: tuple[int, int]) -> bool: - cell = self.maze[self.coordonate[1]][self.coordonate[0]] - index = [(0, -1), (1, 0), (0, 1), (-1, 0)].index(dir) - return self.binary(cell)[index] == 0 - - def can_move(self) -> bool: - x = self.position[0] - y = self.position[1] - return x % CELL_SIZE == 0 and y % CELL_SIZE == 0 - - def go_to(self, dir: tuple[int, int]) -> None: - if not self.can_go(dir) or not self.can_move(): - return - self.direction = dir - - def move(self) -> None: - if self.direction == (0, 0): - return - x = self.position[0] + self.direction[0] * self.speed - y = self.position[1] + self.direction[1] * self.speed - self.position = (x, y) - if self.can_move(): - x = self.coordonate[0] + self.direction[0] - y = self.coordonate[1] + self.direction[1] - self.coordonate = (x, y) - if not self.can_go(self.direction): - self.direction = (0, 0) diff --git a/old_src/pacman/render.py b/old_src/pacman/render.py deleted file mode 100644 index 2639429..0000000 --- a/old_src/pacman/render.py +++ /dev/null @@ -1,309 +0,0 @@ -import time -from abc import ABC, abstractmethod - -import pygame -from pydantic import BaseModel - -from pacman.collectibles import CollectableManager -from pacman.config import CELL_SIZE as CELL_SIZE -from pacman.player import Player -from pacman.score import ScoreManager - - -class RenderInfo(BaseModel): - fps: float - width: int - height: int - maze: list[list[int]] - level: int - score: int - lives: int - remain_time: float - - -class Renderer(ABC): - def __init__(self, screen: pygame.Surface, info: RenderInfo) -> None: - self.screen = screen - self.info = info - super().__init__() - - @abstractmethod - def render(self) -> None: ... - - def draw_text( - self, - text: str, - font_color: tuple[int, int, int], - font_size: int, - position: tuple[int, int], - ) -> int: - font = pygame.font.Font(None, font_size) - txt = font.render(text, True, font_color) - self.screen.blit(txt, position) - return txt.get_width() - - -class Animate_chararacter: - def __init__(self, sprite: str): - self.sprite = { - letter: pygame.image.load("asset/enemy/ghost_blue.png") - if ("ghost" in sprite and letter == "R") - else pygame.image.load(sprite.replace("*", letter)) - for letter in ["L", "R"] - } - - def get_sprite(self, dir: str) -> pygame.Surface: - return self.sprite[dir] - - -class MenuRenderer(Renderer): - def __init__( - self, - screen: pygame.Surface, - info: RenderInfo, - score_manager: ScoreManager, - ) -> None: - super().__init__(screen, info) - self.index = 0 - self.characters = self.add_animated_characters() - self.pos = (-1000, 100) - self.dir = "R" - self.score_manager = score_manager - - def add_animated_characters(self) -> list[Animate_chararacter]: - char = [Animate_chararacter("asset/player/player_*A.png")] - for letter in "ABCD": - s = "asset/enemy/ghost" + letter + "_*.png" - char.append(Animate_chararacter(s)) - return char - - def move(self) -> None: - self.pos = ( - self.pos[0] + 8 * (1 if self.dir == "R" else -1), - self.pos[1], - ) - - if self.pos[0] > 2700: - self.dir = "L" - if self.pos[0] < -600: - self.dir = "R" - - def draw_character(self) -> None: - self.move() - padding = 80 - i = 0 - for c in self.characters: - pos = (self.pos[0] + i * padding + (100 if i else 0), self.pos[1]) - self.screen.blit(c.get_sprite(self.dir), pos) - i += 1 - - def highscore(self) -> None: - x = 1800 - y = 400 - padding = 50 - - self.draw_text("High Score", (255, 255, 255), 100, (x + 50, y)) - - count = 0 - lst = sorted( - self.score_manager.score_list, key=lambda x: x.score, reverse=True - ) - for s in lst: - count += 1 - txt = str(count) + ". " + s.name + " : " + str(s.score) - self.draw_text( - txt, (255, 255, 255), 40, (x + 100, y + 100 + count * padding) - ) - if count == 10: - break - - def instructions(self) -> None: - x = 100 - y = 400 - padding = 70 - size = 40 - self.draw_text("Instructions", (255, 255, 255), 100, (x + 50, y)) - self.draw_text( - "Use arrows or WASD to move in the maze.", - (255, 255, 255), - size, - (x, y + 2 * padding), - ) - self.draw_text( - "Pick up every pacgum to win the level.", - (255, 255, 255), - size, - (x, y + 3 * padding), - ) - self.draw_text( - "If a ghost touch you, you lose a life.", - (255, 255, 255), - size, - (x, y + 4 * padding), - ) - self.draw_text( - "If your life or the timer go to 0 you lose.", - (255, 255, 255), - size, - (x, y + 5 * padding), - ) - self.draw_text( - "If you finish every level you win.", - (255, 255, 255), - size, - (x, y + 6 * padding), - ) - - def render( - self, - ) -> None: - self.instructions() - self.highscore() - self.draw_character() - x = 1130 - y = 600 - padding = 150 - self.draw_text("PAC-MAN", (255, 255, 0), 200, (x - 180, 200)) - len = self.draw_text("New Game", (255, 255, 255), 80, (x, y)) - self.draw_text("Quit", (255, 255, 255), 80, (x + 80, y + padding)) - rect = pygame.Rect(x - 20, y - 20 + self.index * padding, len + 40, 92) - pygame.draw.rect(self.screen, (255, 255, 255), rect, 4) - - -class GameRenderer(Renderer): - def __init__( - self, - screen: pygame.Surface, - info: RenderInfo, - player: Player, - collectibles: CollectableManager, - ): - super().__init__(screen, info) - self.maze = self.render_maze() - self.player = player - self.collectibles = collectibles - self.x = 500 - self.y = 80 + (1280 - self.info.height * CELL_SIZE) / 2 - - @staticmethod - def load_wall_textures() -> dict[int, pygame.Surface]: - return { - n: pygame.image.load("asset/walls/" + str(n) + ".png") - for n in range(16) - } - - def render_maze(self) -> pygame.Surface: - maze_surface = pygame.Surface( - (self.info.width * CELL_SIZE, self.info.height * CELL_SIZE) - ) - walls_textures = self.load_wall_textures() - y = 0 - for row in self.info.maze: - x = 0 - for cell in row: - img = walls_textures[cell] - maze_surface.blit(img, (x * CELL_SIZE, y * CELL_SIZE)) - x += 1 - y += 1 - return maze_surface - - def next_level(self) -> None: - self.maze = self.render_maze() - - def draw_HUD(self) -> None: - x = 200 - y = 100 - padding = 40 - self.draw_text( - "FPS: " + str(self.info.fps), (255, 255, 255), 30, (x, y) - ) - self.draw_text( - "Level: " + str(self.info.level), - (255, 255, 255), - 30, - (x, y + padding), - ) - self.draw_text( - "Lives: " + str(self.info.lives), - (255, 255, 255), - 30, - (x, y + 2 * padding), - ) - self.draw_text( - "Score: " + str(self.info.score), - (255, 255, 255), - 30, - (x, y + 3 * padding), - ) - self.draw_text( - "Time: " + str(self.info.remain_time), - (255, 255, 255), - 30, - (x, y + 4 * padding), - ) - - def render(self) -> None: - self.draw_HUD() - self.screen.blit(self.maze, (self.x, self.y)) - player_sprite = self.player.get_sprite() - x = self.x + self.player.position[0] - y = self.y + self.player.position[1] - self.screen.blit(player_sprite, (x, y)) - for i in self.collectibles.collectibles: - pacgum = self.collectibles.collectibles[i] - size = pacgum.size - center = ( - self.x + i[0] * CELL_SIZE + CELL_SIZE // 2, - self.y + i[1] * CELL_SIZE + CELL_SIZE // 2, - ) - pygame.draw.circle(self.screen, (255, 255, 255), center, size) - - def time_out(self) -> None: - x = self.x + (self.info.width * CELL_SIZE) / 2 - 200 - y = self.y + (self.info.height * CELL_SIZE) / 2 - 50 - rect = pygame.Rect(x - 22, y - 21, 372, 110) - pygame.draw.rect(self.screen, (0, 0, 0), rect) - pygame.draw.rect(self.screen, (255, 255, 255), rect, 4) - - -class EndingRenderer(Renderer): - def __init__( - self, screen: pygame.Surface, info: RenderInfo, victory: bool - ) -> None: - super().__init__(screen, info) - self.name = "" - self.victory = victory - - def render(self) -> None: - x = 900 - y = 500 - if self.victory: - self.draw_text("YOU WIN !", (255, 255, 50), 200, (x + 50, y - 100)) - self.draw_text( - "Enter your name", (255, 255, 255), 50, (x + 250, y + 300) - ) - else: - self.draw_text("GAME OVER", (200, 50, 50), 200, (x, y - 100)) - self.draw_text( - "Enter your name, loser", - (255, 255, 255), - 50, - (x + 200, y + 300), - ) - self.draw_text( - "Your score : " + str(self.info.score), - (255, 255, 255), - 80, - (x + 200, y + 200), - ) - txt = self.draw_text( - self.name, - (255, 255, 255), - 50, - (x + 400 - 10 * len(self.name), y + 400), - ) - if round(time.time()) % 2: - rect = pygame.Rect( - x + 405 + txt - 10 * len(self.name), y + 395, 4, 40 - ) - pygame.draw.rect(self.screen, (255, 255, 255), rect) diff --git a/old_src/pacman/score.py b/old_src/pacman/score.py deleted file mode 100644 index fb36e45..0000000 --- a/old_src/pacman/score.py +++ /dev/null @@ -1,67 +0,0 @@ -import json -from pathlib import Path - -from pydantic import BaseModel, ValidationError - -from pacman.error import error - - -class ScoreDefinition(BaseModel): - name: str - score: int - - -class ScoreManager: - def __init__(self, path: str) -> None: - file_path = Path(path) - self.score_list: list[ScoreDefinition] = [] - self.file_path = file_path - if not file_path.exists(): - error("No highscore file found : new file created", False) - self.save() - else: - self.load() - - def score_error(self) -> None: - error("Highscore file corrupted : new file created", False) - self.save() - - def load(self) -> None: - try: - data = json.loads(self.file_path.read_text(encoding="utf-8")) - except json.JSONDecodeError: - return self.score_error() - - if not isinstance(data, list): - return self.score_error() - - score_list: list[ScoreDefinition] = [] - - for item in data: - try: - score_list.append(ScoreDefinition.model_validate(item)) - except ValidationError: - return self.score_error() - self.score_list = score_list - - def save(self) -> None: - data = [item.model_dump() for item in self.score_list] - self.file_path.parent.mkdir(parents=True, exist_ok=True) - self.file_path.write_text( - json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8" - ) - - def add_score(self, name: str, score: int) -> None: - try: - item = ScoreDefinition(name=name, score=score) - except ValidationError: - return - exist = False - for i in self.score_list: - if i.name == name: - exist = True - if i.score < score: - i.score = score - if not exist: - self.score_list.append(item) - self.save() diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index 34722c9..9d0135f 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -733,6 +733,9 @@ class World(FromWorld): self._components_lookup: dict[type, ComponentID] = {} self._schedules: dict[ScheduleLabel, Schedule[RawSystem[Any]]] = {} self._base_archetype: Archetype = Archetype(self, ()) + self._extractors_cache: dict[ + System[Any], tuple[Callable[[], FromWorld | None], ...] + ] = {} def component_id(self, comp: type) -> ComponentID: if (res := self._components_lookup.get(comp)) is None: @@ -835,6 +838,8 @@ class World(FromWorld): def _sys_extractors[T]( self, system: System[T] ) -> tuple[Callable[[], FromWorld | None], ...]: + if system in self._extractors_cache: + return self._extractors_cache[system] sig = inspect.signature(system, eval_str=True) if err := next( filter( @@ -846,12 +851,14 @@ class World(FromWorld): None, ): raise TypeError(f"System {system}'s {err[0]} parameter is invalid") - return tuple( + res = tuple( cast(type[FromWorld], param.annotation).extract_from_world( param.annotation, self ) for param in sig.parameters.values() ) + self._extractors_cache[system] = res + return res def add_system[T, *P]( self, diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index 7362e19..2d3dea2 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from enum import Enum, auto from random import random, shuffle from time import time +from typing import override from mazegenerator import MazeGenerator # type: ignore[import-untyped] from pygame import K_a, K_d, K_s, K_w, Vector2 @@ -100,7 +101,8 @@ class SuperPacGum(Component): @dataclass class Pacman(Component): - pass + home: tuple[int, int] + home_entity: Entity class GhostState(Enum): @@ -261,7 +263,7 @@ def spawn_pacgums( def setup_maze(world: World) -> None: - world.add_system(create_maze)(MazeGenerator((5, 5))) + world.add_system(create_maze)(MazeGenerator((14, 14))) def move_in_motion( @@ -330,11 +332,21 @@ def move_to_target( def spawn_pacman( - world: World, assets: Assets[Sprite2D], maze_q: Query[tuple[Maze, Entity]] + world: World, + assets: Assets[Sprite2D], + maze_q: Query[tuple[Maze, Entity]], + cells: Query[Cell], ) -> None: if (maze_t := maze_q.single()) is None: return maze, maze_ent = maze_t + home_cell = sorted( + (cell for cell in cells if cell.n != 15), + key=lambda cell: Vector2(cell.grid_pos).distance_squared_to( + (Vector2(maze._x, maze._y).elementwise() / 2).elementwise() - 0.5 + ), + )[0] + pos = home_cell.grid_pos world.spawn( tuple[ Coord2D, @@ -348,7 +360,7 @@ def spawn_pacman( ChildOf, ], ( - Coord2D(3 * 64, 3 * 64, -10), + Coord2D(pos[0] * 64, pos[1] * 64, -10), CoordAbs2D(), AnimationSet( [ @@ -362,11 +374,11 @@ def spawn_pacman( ], 6, ), - Pacman(), + Pacman(pos, maze._cells[pos]), ShouldRender(), - InMotion(maze._cells[(3, 3)], maze._cells[(3, 3)], 1), + InMotion(maze._cells[(pos)], maze._cells[(pos)], 1), MoveSpeed(3.0), - TilePos(maze._cells[(3, 3)]), + TilePos(maze._cells[(pos)]), ChildOf(maze_ent), ), ) @@ -498,6 +510,20 @@ def collect_superpacgum( world.despawn_many(collided) +def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None: + for pacman, pos, motion in pacmans: + pos.xy = Vector2(*pacman.home).elementwise() * 64 + motion.pos = pacman.home_entity + motion.src = pacman.home_entity + + +def respawn_ghost(ghosts: Query[tuple[Ghost, Coord2D, InMotion]]) -> None: + for ghost, pos, motion in ghosts: + pos.xy = Vector2(*ghost.home).elementwise() * 64 + motion.pos = ghost.home_entity + motion.src = ghost.home_entity + + def collide_ghost( pacmans: Query[CoordAbs2D, Pacman], ghosts: Query[ @@ -505,6 +531,7 @@ def collide_ghost( Ghost, ], score: Score, + world: World, ) -> None: for pacman_pos in pacmans: for ghost_pos, status, ghost, motion, pos in ghosts: @@ -515,8 +542,9 @@ def collide_ghost( continue match status.state: case GhostState.wander | GhostState.chase: - # TODO: kill pacman - print("Pacman hit") + world.add_raw_system(respawn_pacman)() + world.add_raw_system(respawn_ghost)() + world.add_raw_system(start_pause_timer)() case GhostState.flee: score.score += 100 # FIXME @@ -628,6 +656,22 @@ def setup_resources(world: World) -> None: world.spawn_resource(Score()) +def start_pause_timer(timer: "InitialPauseTimer") -> None: + timer.tick = 0 + + +def setup_new_maze(world: World) -> None: + for sys in ( + setup_maze, + spawn_superpacgums, + spawn_pacgums, + spawn_pacman, + spawn_ghosts, + start_pause_timer, + ): + world.add_raw_system(sys)() + + def update_inputs(inputs: Inputs, events: WindowEvents) -> None: for key, state in events.keys_itr(): if state: @@ -643,19 +687,32 @@ def victory( ) -> None: for maze in mazes: queue.queue(hierarchy.despawn_with_children(maze)) + queue.queue(setup_new_maze) + + +@dataclass +class InitialPauseTimer(Resource): + tick: int = 0 + + @classmethod + @override + def extract_default( + cls, ty: type, world: "World" + ) -> Callable[[], "InitialPauseTimer | None"]: + return InitialPauseTimer + + +def initial_pause_done(timer: InitialPauseTimer) -> bool: + if timer.tick < 60: + timer.tick += 1 + return False + return True def run_game() -> None: World().with_plugins(graphics_plugins, minimal_plugins).with_systems( StartupSchedule, - Systems( - setup_resources, - setup_maze, - spawn_superpacgums, - spawn_pacgums, - spawn_pacman, - spawn_ghosts, - ).chain(), + Systems(setup_resources, setup_new_maze).chain(), ).with_systems( UpdateSchedule, Systems( @@ -668,6 +725,7 @@ def run_game() -> None: update_ghost_speed, ) .chain() + .run_if(initial_pause_done) .in_set(Update), Systems(update_inputs).in_set(PreUpdate).after(InputsSystems), Systems(update_pacman_anim, update_ghost_anim) @@ -678,6 +736,7 @@ def run_game() -> None: collect_superpacgum, Systems(collide_ghost).before(update_ghost_anim), ) + .chain() .after(PropagateTransform) .before(GraphicsSystems), Systems(victory).run_if(level_is_finished),