From 20c51376f5aba3279afd7a55a0eed9cd867060bf Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 23 Oct 2026 15:42:38 +0200 Subject: [PATCH] Finishing touches and 1.0 --- .gitignore | 1 + Makefile | 1 + README.md | 22 ++++++- config.json | 2 + highscores.json | 6 +- pyproject.toml | 2 +- src/pacman/ecs/gui.py | 4 +- src/pacman/game/__init__.py | 123 ++++++++++++++++++------------------ uv.lock | 2 +- 9 files changed, 96 insertions(+), 67 deletions(-) diff --git a/.gitignore b/.gitignore index 858b83f..eec25a0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ a.txt .mypy_cache .ruff_cache /pacman +/*.tgz diff --git a/Makefile b/Makefile index ff0e902..cda17ce 100644 --- 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 diff --git a/README.md b/README.md index 09edb8d..2a71600 100644 --- 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 diff --git a/config.json b/config.json index 4f6ed91..972b698 100644 --- a/config.json +++ b/config.json @@ -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" } diff --git a/highscores.json b/highscores.json index adc46f9..d16c2e0 100644 --- a/highscores.json +++ b/highscores.json @@ -1,5 +1,7 @@ { "scores": { - "test": 3 + "garobert": 1, + "agilliar": 15, + "trimelen": 1038 } -} \ No newline at end of file +} diff --git a/pyproject.toml b/pyproject.toml index 1ce7867..c31ad8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pacman" -version = "0.1.0" +version = "1.0.0" description = "" readme = "README.md" requires-python = ">=3.13" diff --git a/src/pacman/ecs/gui.py b/src/pacman/ecs/gui.py index 88f24c7..61d5375 100644 --- a/src/pacman/ecs/gui.py +++ b/src/pacman/ecs/gui.py @@ -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, ) diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index e23322e..f81c7fd 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -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 f19fdb7..9e7bc17 100644 --- 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" }, -- 2.53.0