]> Untitled Git - axy/ft/pacman.git/commitdiff
fix config, add level count + maze dimension, game over added, cheat options added...
authorThomas Rimelen <trimelen@k1r3p4.42mulhouse.fr>
Thu, 22 Oct 2026 10:44:33 +0000 (12:44 +0200)
committerThomas Rimelen <trimelen@k1r3p4.42mulhouse.fr>
Thu, 22 Oct 2026 10:44:33 +0000 (12:44 +0200)
Makefile
config.json
highscores.json
src/pacman/__init__.py
src/pacman/game/__init__.py

index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5eccacb6cbec77983dd6a163ca9e99fe1a335726 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -0,0 +1,32 @@
+MAIN=pacman
+PYTHON=python3
+CONFIG=config.json
+
+
+.PHONY: install run venv debug clean lint lint--strict
+
+install:
+       @uv sync
+
+run:
+       @uv run $(MAIN) $(CONFIG)
+
+
+debug:
+       @$(PYTHON) -m pdb $(MAIN) $(CONFIG)
+
+clean:
+       @find . -type d -name "__pycache__" -exec rm -r {} +
+       @find . -type d -name ".mypy_cache" -exec rm -r {} +
+       @find . -type d -name "*.pyc" -delete
+#      @rm -rf .venv
+
+lint:
+       @uv run flake8 src
+       @uv run mypy src --warn-return-any \
+       --warn-unused-ignores --ignore-missing-imports \
+       --disallow-untyped-defs
+
+lint-strict:
+       @uv run flake8 src
+       @uv run mypy src --strict
\ No newline at end of file
index 2314f002783bc02c5940e8f8392075fbd7908a4e..4f6ed913d36bf9197686c9293aea7559da41d03a 100644 (file)
@@ -3,11 +3,14 @@
     "ghost_flee_speed": 1.5,
     "pacman_speed": 3.0,
     "initial_pos_timer": 60,
-    "level_timer": 180,
+    "level_timer": 10800, 
+    "level_count": 10,
     "initial_lives": 3,
     "score_pacgum": 1,
     "score_superpacgum": 5,
     "score_ghost": 20,
     "superpacgum_duration": 480,
+    "maze_width": 15,
+    "maze_heigth": 13,
     "highscores_path": "highscores.json"
 }
index 416a8372f396b3555cf36d0aa1a6ba97595ff5dc..adc46f9a41a9896d2b2c3883f47931ef69bc9cf7 100644 (file)
@@ -1,22 +1,5 @@
 {
     "scores": {
-        "Axel": 30,
-        "Thomas": 20,
-        "Garobert": 1,
-        "agilliar": 5,
-        "agilliaras": 5,
-        "": 7,
-        "sdasd": 7,
-        "sdasdswwsw": 7,
-        "asads": 6,
-        "sswswss": 6,
-        "dds": 7,
-        "s": 8,
-        "sssd": 7,
-        "w": 8,
-        "ssswsws": 7,
-        "sssssaaa": 7,
-        "ssss": 8,
-        "sws": 8
+        "test": 3
     }
 }
\ No newline at end of file
index a9f9fddcda03d13ed79e4256c51b87f7a053a86f..4f0ad42fd7b91c973d87c4a27972847b9ad9c9d5 100644 (file)
@@ -2,6 +2,7 @@ from pacman.game import run_game
 
 
 def main() -> None:
+    """Run the game."""
     run_game()
     return None
     try:
index 9040306d12f6c0f88eaa087ba24119c12328afdb..271a16c27a997cc77831464ad1ed9b8d5aaa425c 100644 (file)
@@ -7,7 +7,7 @@ from typing import cast, override
 
 from mazegenerator import MazeGenerator  # type: ignore[import-untyped]
 from pydantic import BaseModel, Field
-from pygame import K_BACKSPACE, K_ESCAPE, K_RETURN, K_a, K_z, Vector2
+from pygame import K_BACKSPACE, K_ESCAPE, K_RETURN, K_a, K_c, K_n, K_z, Vector2
 from pygame.math import clamp
 
 from pacman.ecs import minimal_plugins
@@ -127,11 +127,14 @@ class Config(Resource, BaseModel):
     pacman_speed: float = Field(default=3.0, ge=0.1, le=100.0)
     initial_pos_timer: int = Field(default=60, ge=0)
     level_timer: int = Field(default=60 * 180, ge=60)
+    level_count: int = Field(default=10, ge=1)
     initial_lives: int = Field(default=3, ge=1)
     score_pacgum: int = Field(default=1, ge=0)
     score_superpacgum: int = Field(default=5, ge=0)
     score_ghost: int = Field(default=20, ge=0)
     superpacgum_duration: int = Field(default=60 * 8, ge=60)
+    maze_width: int = Field(default=12, ge=5, le=15)
+    maze_heigth: int = Field(default=12, ge=5, le=15)
     highscores_path: str = "highscores.json"
 
 
@@ -162,6 +165,7 @@ class GameState(Resource):
     level: int = 0
     timer: int = 0
     lives: int = 3
+    state: str = ""
 
 
 class CheatMode(Component):
@@ -425,9 +429,11 @@ def spawn_pacgums(
     )
 
 
-def setup_maze(world: World) -> None:
+def setup_maze(world: World, config: Config) -> None:
     """Create maze with a given MazeGenerator."""
-    world.run_system(create_maze, MazeGenerator((14, 14)))
+    world.run_system(
+        create_maze, MazeGenerator((config.maze_width, config.maze_heigth))
+    )
 
 
 def move_in_motion(
@@ -604,6 +610,20 @@ def move_pacman(
             motion.direction = direction
 
 
+def chase(
+    ghosts: Query[tuple[InMotion, GhostStatus], Ghost],
+) -> None:
+    """Change ghost status to 'flee'."""
+    for ghost, status in ghosts:
+        if status.state in (GhostState.wander, GhostState.chase):
+            tmp = ghost.src
+            ghost.src = ghost.pos
+            ghost.pos = tmp
+            status.state = GhostState.flee
+        if status.state is GhostState.flee:
+            status.tick = 0
+
+
 def collect_pacgum(
     world: World,
     pacgums: Query[tuple[CoordAbs2D, Entity], PacGum],
@@ -626,7 +646,6 @@ def collect_superpacgum(
     world: World,
     pacgums: Query[tuple[CoordAbs2D, Entity], SuperPacGum],
     pacmans: Query[CoordAbs2D, Pacman],
-    ghosts: Query[tuple[InMotion, GhostStatus], Ghost],
     score: GameState,
     config: Config,
 ) -> None:
@@ -640,14 +659,15 @@ def collect_superpacgum(
     score.score += len(collided * config.score_superpacgum)
     if len(collided) == 0:
         return
-    for ghost, status in ghosts:
-        if status.state in (GhostState.wander, GhostState.chase):
-            tmp = ghost.src
-            ghost.src = ghost.pos
-            ghost.pos = tmp
-            status.state = GhostState.flee
-        if status.state is GhostState.flee:
-            status.tick = 0
+    world.run_simple_system(chase)
+    # for ghost, status in ghosts:
+    #     if status.state in (GhostState.wander, GhostState.chase):
+    #         tmp = ghost.src
+    #         ghost.src = ghost.pos
+    #         ghost.pos = tmp
+    #         status.state = GhostState.flee
+    #     if status.state is GhostState.flee:
+    #         status.tick = 0
     world.despawn_many(collided)
 
 
@@ -898,12 +918,15 @@ def victory(
     mazes: Query[Entity, Maze],
     queue: CommandQueue,
     state: GameState,
+    config: Config,
 ) -> None:
     """Despawn maze and spawn new maze or init game over."""
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
-    if state.level == 2:
+    if state.level == config.level_count - 1:
+        state.state = "Victory"
         queue.queue(lambda world: world.run_simple_system(game_over))
+        queue.queue(lambda world: world.run_simple_system(spawn_game_over))
         return
     state.level += 1
     state.lives += 1
@@ -918,17 +941,21 @@ def level_is_lost(
     return config.level_timer < state.timer or state.lives <= 0
 
 
-def spawn_game_over(world: World, fonts: Assets[Font]) -> None:  # FIXME
+def spawn_game_over(
+    world: World,
+    fonts: Assets[Font],
+    state: GameState,
+) -> None:
     """Display game over menu."""
     root = world.spawn(
         tuple[WindowPositionned, GameOver],
         (
-            WindowPositionned(Vector2(0.5, 0), Vector2(0, 100)),
+            WindowPositionned(Vector2(0.5, 0), Vector2(0, 200)),
             GameOver(),
         ),
     )
-    font = fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:100")
-    text = "GAME OVER"
+    font = fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:200")
+    text = state.state
     world.spawn(
         tuple[ChildOf, ShouldRender, CoordBundle, TextDisplay],
         (
@@ -945,20 +972,31 @@ def spawn_game_over(world: World, fonts: Assets[Font]) -> None:  # FIXME
         (
             ChildOf(root),
             ShouldRender(),
-            font.center_text_for("", Vector2(0, 300)),
-            ComputedText(cast(System[str], print_name)),
+            font.center_text_for("Score = XXX", Vector2(0, 1200)),
+            ComputedText(cast(System[str], print_score)),
             TextDisplay("", font),
         ),
     )
 
-    font = fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30")
+    text = "Enter your name:"
+    world.spawn(
+        tuple[ChildOf, ShouldRender, CoordBundle, TextDisplay],
+        (
+            ChildOf(root),
+            ShouldRender(),
+            font.center_text_for(text, Vector2(0, 450)),
+            TextDisplay(text, font),
+        ),
+    )
+
+    font = fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:50")
     world.spawn(
         tuple[ChildOf, ShouldRender, CoordBundle, ComputedText, TextDisplay],
         (
             ChildOf(root),
             ShouldRender(),
-            font.center_text_for("Score = XXX", Vector2(0, 200)),
-            ComputedText(cast(System[str], print_score)),
+            font.center_text_for("          ", Vector2(0, 700)),
+            ComputedText(cast(System[str], print_name)),
             TextDisplay("", font),
         ),
     )
@@ -969,18 +1007,22 @@ def lose(
     gui: Query[Entity, GameGui],
     hierarchy: Hierarchy[tuple[()]],
     mazes: Query[Entity, Maze],
+    state: GameState,
     queue: CommandQueue,
 ) -> None:
     """Despawn mane and run the game over."""
     world.despawn_many(list(gui))
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
+    state.state = "Game Over"
     queue.queue(lambda world: world.add_simple_system(game_over)())
-    world.run_simple_system(spawn_game_over)
+    queue.queue(lambda world: world.run_simple_system(spawn_game_over))
 
 
-def game_over(queue: CommandQueue) -> None:
+def game_over(world: World, queue: CommandQueue) -> None:
     """Spawn resources when game is over."""
+    if world.run_simple_system(cheat_mode_enabled):
+        world.run_simple_system(disable_cheat_mode)
     queue.queue(lambda world: world.spawn_resource(GameIsOver()))
     queue.queue(lambda world: world.spawn_resource(PlayerName()))
 
@@ -1002,7 +1044,7 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool:
 
 def print_name(name: PlayerName) -> str:
     """Return a string with the player score."""
-    return f"Enter your name: {name.name}"
+    return name.name
 
 
 def print_score(state: GameState) -> str:
@@ -1021,6 +1063,11 @@ def print_timer(state: GameState, config: Config) -> str:
     return f"Time left: {time // 60}"
 
 
+def print_level(state: GameState, config: Config) -> str:
+    """Return a string with the level timer."""
+    return f"Level {state.level + 1} / {config.level_count}"
+
+
 def spawn_hud(world: World, fonts: Assets[Font]) -> None:
     """Draw in game HUD with lives, levels and timer informations."""
     world.spawn_many(
@@ -1034,7 +1081,7 @@ def spawn_hud(world: World, fonts: Assets[Font]) -> None:
         ],
         [
             (
-                WindowPositionned(Vector2(), Vector2(20, y * 100 + 20)),
+                WindowPositionned(Vector2(), Vector2(20, y * 60 + 20)),
                 CoordBundle(depth=-20),
                 ShouldRender(),
                 ComputedText(cast(System[str], fn)),
@@ -1046,6 +1093,7 @@ def spawn_hud(world: World, fonts: Assets[Font]) -> None:
             )
             for y, fn in enumerate(
                 [
+                    print_level,
                     print_score,
                     print_lives,
                     print_timer,
@@ -1059,13 +1107,14 @@ def start_game(
     world: World,
     hierarchy: Hierarchy[tuple[()]],
     menus: Query[Entity, MainMenu],
+    config: Config,
 ) -> None:
     """Close main menu and starting the game and HUD."""
     for menu in menus:
         hierarchy.despawn_with_children(menu)(world)
     world.despawn_resource(SelectedButton)
 
-    world.spawn_resource(GameState())
+    world.spawn_resource(GameState(lives=config.initial_lives))
     world.run_simple_system(setup_new_maze)
     world.run_simple_system(spawn_hud)
 
@@ -1140,6 +1189,33 @@ def spawn_cheat_info(world: World, fonts: Assets[Font]) -> None:
             CheatMode(),
         ),
     )
+    text = """
+    Cheat commands:
+Press 'n to go to next level
+Press 'c' to start chase
+"""
+    world.spawn_many(
+        tuple[
+            WindowPositionned,
+            ShouldRender,
+            TextDisplay,
+            CoordBundle,
+            CheatMode,
+        ],
+        [
+            (
+                WindowPositionned(Vector2(1, 1), Vector2(-400, -220 + i * 50)),
+                ShouldRender(),
+                TextDisplay(
+                    s,
+                    fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:22"),
+                ),
+                CoordBundle(depth=-20),
+                CheatMode(),
+            )
+            for i, s in enumerate(text.split("\n"))
+        ],
+    )
 
 
 def cheat_mode_enabled(cheat: Query[CheatMode]) -> bool:
@@ -1399,6 +1475,15 @@ def test(events: WindowEvents) -> None:  # FIXME
             print(key)
 
 
+def cheat_input(world: World, events: WindowEvents) -> None:
+    """Get cheat inputs."""
+    for key, state in events.keys_itr():
+        if key == K_n and state:
+            world.run_simple_system(victory)
+        if key == K_c and state:
+            world.run_simple_system(chase)
+
+
 def add_score(
     world: World,
     name: PlayerName,
@@ -1412,11 +1497,10 @@ def add_score(
     # FIXME
     scores.scores[name.name] = state.score
     world.run_simple_system(save_highscores)
-    # items = [item for item in menus]
-    # # world.despawn_many(items)
     for menu in menus:
         queue.queue(hierarchy.despawn_with_children(menu))
     world.despawn_resource(GameIsOver)
+    world.despawn_resource(GameState)
     world.run_simple_system(setup_main_menu)
 
 
@@ -1475,4 +1559,7 @@ def run_game() -> None:
         Systems(victory).run_if(level_is_finished, in_game),
         Systems(lose).run_if(level_is_lost),
         Systems(get_player_name).run_if(game_is_over),
+        Systems(cheat_input).run_if(
+            cheat_mode_enabled, initial_pause_done, in_game
+        ),
     ).run_main()