]> Untitled Git - axy/ft/pacman.git/commitdiff
Levels and pacman respawning
authorThomas Rimelen <trimelen@k0r1p10.42mulhouse.fr>
Thu, 8 Oct 2026 13:47:03 +0000 (15:47 +0200)
committerThomas Rimelen <trimelen@k0r1p10.42mulhouse.fr>
Thu, 8 Oct 2026 13:47:03 +0000 (15:47 +0200)
12 files changed:
old_src/__main__.py [deleted file]
old_src/pacman/__init__.py [deleted file]
old_src/pacman/collectibles.py [deleted file]
old_src/pacman/config.py [deleted file]
old_src/pacman/error.py [deleted file]
old_src/pacman/game.py [deleted file]
old_src/pacman/input.py [deleted file]
old_src/pacman/player.py [deleted file]
old_src/pacman/render.py [deleted file]
old_src/pacman/score.py [deleted file]
src/pacman/ecs/world.py
src/pacman/game/__init__.py

diff --git a/old_src/__main__.py b/old_src/__main__.py
deleted file mode 100644 (file)
index 314fc41..0000000
+++ /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 (file)
index 8b82c04..0000000
+++ /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 (file)
index 9e472f2..0000000
+++ /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 (file)
index f00cd6c..0000000
+++ /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 (file)
index b597bae..0000000
+++ /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 (file)
index cc4e81e..0000000
+++ /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 (file)
index c7ab3da..0000000
+++ /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 (file)
index 75819d1..0000000
+++ /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 (file)
index 2639429..0000000
+++ /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 (file)
index fb36e45..0000000
+++ /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()
index 34722c969a66eaa8323a1df28878c365cbe74dd6..9d0135f79a0c8778e089dffb73312fc4c3d219ff 100644 (file)
@@ -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,
index 7362e19c422db79248fad945b10acff7053aaf2b..2d3dea203d4a5776662a1681bf1aa39be3e068cd 100644 (file)
@@ -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),