]> Untitled Git - axy/ft/pacman.git/commitdiff
Finishing touches and 1.0
author= <=>
Fri, 23 Oct 2026 13:42:38 +0000 (15:42 +0200)
committer= <=>
Fri, 23 Oct 2026 13:42:38 +0000 (15:42 +0200)
.gitignore
Makefile
README.md
config.json
highscores.json
pyproject.toml
src/pacman/ecs/gui.py
src/pacman/game/__init__.py
uv.lock

index 858b83f53c0af6f0313b27b6c6c5054e1a434361..eec25a0ad2d29720424c4c1af740b90f97c4178e 100644 (file)
@@ -9,3 +9,4 @@ a.txt
 .mypy_cache
 .ruff_cache
 /pacman
+/*.tgz
index ff0e902f3d6b0380d05bd8cbed89344ee61a2b77..cda17ce04c35fe325745726baee020a4af179609 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -38,3 +38,4 @@ check:
 build:
        uv pip compile pyproject.toml -o requirements.txt
        uv run shiv -o pacman -e pacman:main -r requirements.txt .
+       tar czf "$$(uv version).tgz" pacman config.json
index 09edb8d271baecd8f1c809b1148195ea2ec98b61..2a716009b8bfa4e764672b3d27db3bc6c5dae4e5 100644 (file)
--- a/README.md
+++ b/README.md
@@ -30,7 +30,27 @@ TODO
 
 # Configuration
 
-TODO
+The configuration is as follows, with these default values:
+```json
+{
+    "ghost_chase_speed": 2.5,
+    "ghost_flee_speed": 1.5,
+    "pacman_speed": 3.0,
+    "initial_pos_timer": 60,
+    "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,
+    "seed": 42,
+    "highscores_path": "highscores.json"
+}
+```
+Durations are in frames.
 
 # Highscore
 
index 4f6ed913d36bf9197686c9293aea7559da41d03a..972b6983dac7850fac9c508d4040c8e94eef3125 100644 (file)
@@ -1,3 +1,4 @@
+# Comments are supported!
 {
     "ghost_chase_speed": 2.5,
     "ghost_flee_speed": 1.5,
@@ -12,5 +13,6 @@
     "superpacgum_duration": 480,
     "maze_width": 15,
     "maze_heigth": 13,
+    "seed": 42,
     "highscores_path": "highscores.json"
 }
index adc46f9a41a9896d2b2c3883f47931ef69bc9cf7..d16c2e0180325cfeb59101e70313f2553e0ce234 100644 (file)
@@ -1,5 +1,7 @@
 {
     "scores": {
-        "test": 3
+        "garobert": 1,
+        "agilliar": 15,
+        "trimelen": 1038
     }
-}
\ No newline at end of file
+}
index 1ce7867ee388016f6fdafd0aa1da739c9e170396..c31ad8c80ebf8ecfe69388b8b973834343a64a71 100644 (file)
@@ -1,6 +1,6 @@
 [project]
 name = "pacman"
-version = "0.1.0"
+version = "1.0.0"
 description = ""
 readme = "README.md"
 requires-python = ">=3.13"
index 88f24c75b208d9089a1937396c739c19cfdcaf54..61d53753cf641e00512378c315e225085295e412 100644 (file)
@@ -56,10 +56,10 @@ class Font(Asset, Component):
     def center_text_for(self, text: str, dims: Vector2) -> CoordBundle:
         """Compute how to center the text in the given rectangle."""
         width, _ = self.font.size(text)
-        height = self.font.get_ascent() + self.font.get_descent()
+        height = self.font.get_linesize()
         return CoordBundle(
             (dims.x - width) / 2,
-            (dims.y - height) / 2 + self.font.get_descent(),
+            (dims.y - height) / 2,
         )
 
 
index e23322ee7ba86c88c72ae5a7cd3ecf6a80d59d9a..f81c7fdfe96b36805a02de2e190f72a43bd130d3 100644 (file)
@@ -1,10 +1,10 @@
 """All the gameplay logic of pacman."""
 
+import random
 import sys
 from collections.abc import Callable
 from dataclasses import dataclass
 from enum import Enum, auto
-from random import random, shuffle
 from typing import cast, override
 
 from mazegenerator import (  # type: ignore[import-untyped, unused-ignore]
@@ -139,6 +139,7 @@ class Config(Resource, BaseModel):
     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)
+    seed: int = Field(default=42, ge=1)
     highscores_path: str = "highscores.json"
 
 
@@ -167,11 +168,11 @@ class GameState(Resource):
     level: int = 0
     timer: int = 0
     lives: int = 3
-    state: str = ""
+    won: bool = False
 
 
 class CheatMode(Component):
-    """Component for the cheat mod."""
+    """Component for the cheat mode."""
 
 
 class MainMenu(Component):
@@ -281,9 +282,11 @@ class Cell(Component):
         return [(1 & (self.n >> i)) == 1 for i in range(4)]
 
 
-def remove_comment(s: str) -> str:
+def remove_comments(s: str) -> str:
     """Return s with all comment lines removed."""
-    return "\n".join(line for line in s.split("\n") if not s.startswith("#"))
+    return "\n".join(
+        line for line in s.split("\n") if not line.startswith("#")
+    )
 
 
 def load_config(world: World) -> None:
@@ -294,7 +297,7 @@ def load_config(world: World) -> None:
         )
 
     with open(sys.argv[1]) as f:
-        config = Config.model_validate_json(remove_comment(f.read()))
+        config = Config.model_validate_json(remove_comments(f.read()))
     world.spawn_resource(config)
 
 
@@ -400,7 +403,7 @@ def spawn_pacgums(
             continue
         if cell.n == 15:
             continue
-        if random() > 0.7:
+        if random.random() > 0.7:
             continue
         pacgums.append(
             (
@@ -417,10 +420,12 @@ def spawn_pacgums(
     )
 
 
-def setup_maze(world: World, config: Config) -> None:
+def setup_maze(world: World, config: Config, state: GameState) -> None:
     """Create maze with a given MazeGenerator."""
+    seed = config.seed if state.level == 0 else 0
     world.run_system(
-        create_maze, MazeGenerator((config.maze_width, config.maze_heigth))
+        create_maze,
+        MazeGenerator((config.maze_width, config.maze_heigth), seed=seed),
     )
 
 
@@ -744,7 +749,7 @@ def move_to_target(
         )
         if status.state is GhostState.flee:
             lst = candidates[:-1]
-            shuffle(lst)
+            random.shuffle(lst)
             candidates = lst + [candidates[-1]]
         nxt, nxt_dir = candidates[0]
         motion.src = motion.pos
@@ -902,22 +907,22 @@ def level_is_finished(
 def victory(
     hierarchy: Hierarchy[tuple[()]],
     mazes: Query[Entity, Maze],
-    queue: CommandQueue,
+    world: World,
     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))
+        world.run_simple_system(hierarchy.despawn_with_children(maze))
     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))
+        state.won = True
+        world.run_simple_system(game_over)
+        world.run_simple_system(spawn_game_over)
         return
     state.level += 1
     state.lives += 1
     state.timer = 0
-    queue.queue(setup_new_maze)
+    world.run_simple_system(setup_new_maze)
 
 
 def level_is_lost(
@@ -941,7 +946,7 @@ def spawn_game_over(
         ),
     )
     font = fonts.load("fonts/JetBrainsMonoNL-Regular.ttf:200")
-    text = state.state
+    text = "Victory" if state.won else "Game Over"
     world.spawn(
         tuple[ChildOf, ShouldRender, CoordBundle, TextDisplay],
         (
@@ -988,29 +993,28 @@ def spawn_game_over(
     )
 
 
-def lose(
+def lose(world: World) -> None:
+    """Despawn maze and run the game over."""
+    world.run_simple_system(game_over)
+    world.run_simple_system(spawn_game_over)
+
+
+def game_over(
     world: World,
+    queue: CommandQueue,
     gui: Query[Entity, GameGui],
-    hierarchy: Hierarchy[tuple[()]],
     mazes: Query[Entity, Maze],
-    state: GameState,
-    queue: CommandQueue,
+    hierarchy: Hierarchy[tuple[()]],
 ) -> None:
-    """Despawn mane and run the game over."""
-    world.despawn_many(list(gui))
+    """Change state to game over."""
+    for e in gui:
+        queue.queue(hierarchy.despawn_with_children(e))
     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)())
-    queue.queue(lambda world: world.run_simple_system(spawn_game_over))
-
-
-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()))
+    world.spawn_resource(GameIsOver())
+    world.spawn_resource(PlayerName())
 
 
 def update_game_timer(world: World, state: GameState) -> None:
@@ -1020,12 +1024,14 @@ def update_game_timer(world: World, state: GameState) -> None:
     state.timer += 1
 
 
+def update_pause_timer(timer: InitialPauseTimer) -> None:
+    """Update the pause timer."""
+    timer.tick += 1
+
+
 def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool:
     """Return if the inital timer is done."""
-    if timer.tick < config.initial_pos_timer:
-        timer.tick += 1
-        return False
-    return True
+    return timer.tick >= config.initial_pos_timer
 
 
 def print_name(name: PlayerName) -> str:
@@ -1210,19 +1216,23 @@ def cheat_mode_enabled(cheat: Query[CheatMode]) -> bool:
 
 
 def enable_cheat_mode(world: World) -> None:
-    """Enable cheat mod."""
+    """Enable cheat mode."""
     world.run_simple_system(spawn_cheat_info)
     world.spawn(CheatMode, CheatMode())
 
 
-def disable_cheat_mode(world: World, cheats: Query[Entity, CheatMode]) -> None:
-    """Disable cheat mod."""
-    items: list[Entity] = [cheat for cheat in cheats]
-    world.despawn_many(items)
+def disable_cheat_mode(
+    queue: CommandQueue,
+    cheats: Query[Entity, CheatMode],
+    hierarchy: Hierarchy[tuple[()]],
+) -> None:
+    """Disable cheat mode."""
+    for e in cheats:
+        queue.queue(hierarchy.despawn_with_children(e))
 
 
 def toggle_cheat_mode(world: World) -> None:
-    """Enable or disable cheat mod."""
+    """Enable or disable cheat mode."""
     if world.run_simple_system(cheat_mode_enabled):
         world.run_simple_system(disable_cheat_mode)
     else:
@@ -1239,7 +1249,8 @@ def close_current_game(
     """Remove maze with childrens and game GUI."""
     world.run_simple_system(close_game_menu)
     world.despawn_resource(GameState)
-    world.despawn_many(list(gui))
+    for e in gui:
+        queue.queue(hierarchy.despawn_with_children(e))
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
     if world.run_simple_system(cheat_mode_enabled):
@@ -1293,7 +1304,7 @@ def setup_game_menu(
     cheat_button = world.run_system(
         spawn_button,
         WindowPositionned(Vector2(0.5), Vector2(-150, -40)),
-        "Cheat mod",
+        "Cheat mode",
         toggle_cheat_mode,
     )
 
@@ -1344,6 +1355,7 @@ When you eat a super pacgum, you can eat ghost.
         )
     ]
 
+    instructions_font = fonts.load("fonts/JetBrainsMonoNL-Regular.ttf:20")
     entities.extend(
         world.spawn_many(
             tuple[
@@ -1354,13 +1366,13 @@ When you eat a super pacgum, you can eat ghost.
             [
                 (
                     WindowPositionned(
-                        Vector2(0, 0), Vector2(50, 200 + i * 50)
+                        Vector2(0, 0),
+                        Vector2(
+                            50, 200 + i * instructions_font.font.get_linesize()
+                        ),
                     ),
                     ShouldRender(),
-                    TextDisplay(
-                        s,
-                        fonts.load("fonts/JetBrainsMonoNL-Regular.ttf:20"),
-                    ),
+                    TextDisplay(s, instructions_font),
                 )
                 for i, s in enumerate(text.split("\n"))
             ],
@@ -1450,13 +1462,6 @@ def setup_main_menu(world: World) -> None:
     world.spawn_resource(SelectedButton(selected))
 
 
-def test(events: WindowEvents) -> None:  # FIXME
-    """Test."""
-    for key, state in events.keys_itr():
-        if state:
-            print(key)
-
-
 def cheat_input(world: World, events: WindowEvents) -> None:
     """Get cheat inputs."""
     for key, state in events.keys_itr():
@@ -1476,7 +1481,6 @@ def add_score(
     hierarchy: Hierarchy[tuple[()]],
 ) -> None:
     """Add players score to highscores, save it and go to main menu."""
-    # FIXME
     scores.scores[name.name] = state.score
     world.run_simple_system(save_highscores)
     for menu in menus:
@@ -1523,6 +1527,7 @@ def run_game() -> None:
         .chain()
         .run_if(initial_pause_done, in_game)
         .in_set(Update),
+        Systems(update_pause_timer).in_set(PostUpdate).run_if(in_game),
         Systems(toggle_game_menu)
         .run_if(key_just_pressed(K_ESCAPE), maze_exists)
         .in_set(PreUpdate)
@@ -1541,7 +1546,5 @@ 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
-        ),
+        Systems(cheat_input).run_if(cheat_mode_enabled, in_game),
     ).run_main()
diff --git a/uv.lock b/uv.lock
index f19fdb7dc7dbf22e0da1bc01af8c0f5d9c548d5a..9e7bc17a017596cbd1be84e0f302926effdce863 100644 (file)
--- a/uv.lock
+++ b/uv.lock
@@ -264,7 +264,7 @@ wheels = [
 
 [[package]]
 name = "pacman"
-version = "0.1.0"
+version = "1.0.0"
 source = { editable = "." }
 dependencies = [
     { name = "mazegenerator" },