From: Thomas Rimelen Date: Mon, 19 Oct 2026 14:26:11 +0000 (+0200) Subject: Dockstring added to game/init file X-Git-Url: https://git.uwuaxy.net/Project%20Management.png?a=commitdiff_plain;h=726059060f275b062388a0975f974c3b51b44145;p=axy%2Fft%2Fpacman.git Dockstring added to game/init file --- diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index bebebbd..6bc3067 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -58,6 +58,10 @@ _DIR_OFFSETS = [(0, -1), (1, 0), (0, 1), (-1, 0)] class GhostVariant(Enum): + """ + Enumeration for Ghost variant and behavior. + """ + A = auto() B = auto() C = auto() @@ -71,6 +75,9 @@ class GhostVariant(Enum): slf: "Ghost", blinky_pos: tuple[int, int], ) -> tuple[int, int]: + """ + Return ghost target coordinates. + """ match self: case GhostVariant.A: return player @@ -90,8 +97,24 @@ class GhostVariant(Enum): return (x_target * 2 - x_blinky, y_target * 2 - y_blinky) +class GhostState(Enum): + """ + Enumeration for Ghost state and attitudes. + """ + + chase = auto() + wander = auto() + flee = auto() + dead = auto() + respawning = auto() + + @dataclass class InitialPauseTimer(Resource): + """ + Resource use for the initial pause before starting a level. + """ + tick: int = 0 @classmethod @@ -104,6 +127,10 @@ class InitialPauseTimer(Resource): class Config(Resource, BaseModel): + """ + Configuration of the game. + """ + ghost_chase_speed: float = Field(default=2.5, ge=0.1, le=100.0) ghost_flee_speed: float = Field(default=1.5, ge=0.1, le=100.0) pacman_speed: float = Field(default=3.0, ge=0.1, le=100.0) @@ -118,15 +145,27 @@ class Config(Resource, BaseModel): class Highscores(Resource, BaseModel): + """ + Store the hichscores in a dictionnary. + """ + scores: dict[str, int] class GameIsOver(Resource): + """ + Resource set when game is over. + """ + pass @dataclass class GameState(Resource): + """ + Resource storing current score, level, timer and lives. + """ + score: int = 0 level: int = 0 timer: int = 0 @@ -134,41 +173,61 @@ class GameState(Resource): class MainMenu(Component): + """ + Component for main menu items. + """ + pass class GameMenu(Component): + """ + Component for game menu items. + """ + pass class GameGui(Component): + """ + Component for game graphical user interface items. + """ + pass class PacGum(Component): + """ + Component for pacgums items. + """ + pass class SuperPacGum(Component): + """ + Component for super pacgums items. + """ + pass @dataclass class Pacman(Component): + """ + Component for pacman items storing the home coordinates and entity. + """ + home: tuple[int, int] home_entity: Entity -class GhostState(Enum): - chase = auto() - wander = auto() - flee = auto() - dead = auto() - respawning = auto() - - @dataclass class GhostStatus(Component): + """ + Component for ghost state and cycle. + """ + tick: int = 0 cycle_wander: int = 60 * 10 state: GhostState = GhostState.wander @@ -176,6 +235,11 @@ class GhostStatus(Component): @dataclass class Ghost(Component): + """ + Component for ghost items storing the ghost variant and the home + coordinates and entity. + """ + variant: GhostVariant home: tuple[int, int] home_entity: Entity @@ -183,16 +247,29 @@ class Ghost(Component): @dataclass class TargetTile(Component): + """ + Component for the coordinates of a target used for ghost movement. + """ + pos: tuple[int, int] @dataclass class TilePos(Component): + """ + Component tile position as entity. + """ + pos: Entity @dataclass class InMotion(Component): + """ + Component for pacman or ghost movement storing the position, the provenance + and the direction. + """ + src: Entity pos: Entity direction: int @@ -200,10 +277,19 @@ class InMotion(Component): @dataclass class MoveSpeed(Component): + """ + Component for movement storing the movement speed by pixels per frames. + """ + pixels_per_frame: float class Maze(Component): + """ + Component for maze storing the x and y dimension and the cells in a + dictionnary (coordonate entity). + """ + def __init__(self, x: int, y: int) -> None: self._cells: dict[tuple[int, int], Entity] = {} self._x = x @@ -212,22 +298,39 @@ class Maze(Component): @dataclass class Cell(Component): + """ + Component for the cells of the maze storing the value and the position. + """ + n: int grid_pos: tuple[int, int] def neighbor(self, idx: int) -> tuple[int, int]: + """ + Get neighbor position by direction (1: up, 2: right ...). + """ dx, dy = _DIR_OFFSETS[idx] return (self.grid_pos[0] + dx, self.grid_pos[1] + dy) def walls(self) -> list[bool]: + """ + Get list of walls. + """ return [(1 & (self.n >> i)) == 1 for i in range(4)] def remove_comment(s: str) -> str: + """ + Return s with all comment lines removed. + """ return "\n".join(line for line in s.split("\n") if not s.startswith("#")) def load_config(world: World) -> None: + """ + Load the configuration json file given in first argument and spawn the + Config resource. + """ if len(sys.argv) != 2: raise Exception( f"Expected exactly 1 argument, got {len(sys.argv) - 1}" @@ -239,6 +342,9 @@ def load_config(world: World) -> None: def load_highscores(world: World, config: Config) -> None: + """ + Load the highscores json file and spawn the Highscores resource. + """ try: with open(config.highscores_path) as f: scores = Highscores.model_validate_json(f.read()) @@ -248,6 +354,9 @@ def load_highscores(world: World, config: Config) -> None: def save_highscores(config: Config, scores: Highscores) -> None: + """ + Write the json highscores file from the Highscores resource. + """ path = config.highscores_path with open(path, "w+") as f: f.write(scores.model_dump_json(indent=4)) @@ -256,6 +365,10 @@ def save_highscores(config: Config, scores: Highscores) -> None: def create_maze( world: World, assets: Assets[Sprite2D] ) -> Callable[[MazeGenerator], Entity]: + """ + System creating and adding the maze to the world. + """ + def inner(maze: MazeGenerator) -> Entity: root_maze = Maze(len(maze.maze[0]), len(maze.maze)) cell_dims = Vector2(64, 64) @@ -297,6 +410,9 @@ def create_maze( def spawn_superpacgums( world: World, mazes: Query[Maze], assets: Assets[Sprite2D] ) -> None: + """ + System creating and adding super pacgums to the maze. + """ pacgums = [] for maze in mazes: for pos in [ @@ -327,6 +443,9 @@ def spawn_pacgums( superpacgums: Query[ChildOf, SuperPacGum], assets: Assets[Sprite2D], ) -> None: + """ + System creating and adding pacgums to the maze. + """ ignore = {e.parent for e in superpacgums} pacgums = [] for ent, cell in cells: @@ -352,6 +471,9 @@ def spawn_pacgums( def setup_maze(world: World) -> None: + """ + System running the 'create_maze' system with a give MazeGenerator. + """ world.run_system(create_maze, MazeGenerator((14, 14))) @@ -359,6 +481,9 @@ def move_in_motion( entities: Query[tuple[InMotion, Coord2D, MoveSpeed]], positions: Query[Coord2D], ) -> None: + """ + System enabling movement operation for entity wit InMotion component. + """ for motion, pos, speed in entities: if (dst := positions.get(motion.pos)) is None: continue @@ -373,6 +498,9 @@ def update_tile_pos( entities: Query[tuple[InMotion, Coord2D, TilePos]], cells: Query[Coord2D], ) -> None: + """ + System updating the tile position for entity wit InMotion component. + """ for motion, coord, pos in entities: src = cells[motion.src] dst = cells[motion.pos] @@ -381,45 +509,6 @@ def update_tile_pos( pos.pos = motion.pos -def move_to_target( - entities: Query[tuple[TargetTile, InMotion, Coord2D, GhostStatus]], - cells: Query[tuple[Cell, Coord2D]], - maze_q: Query[Maze], -) -> None: - if (maze := maze_q.single()) is None: - return - for target, motion, pos, status in entities: - if status.state in (GhostState.dead, GhostState.respawning): - continue - cell, cell_pos = cells[motion.pos] - if cell_pos.xy.distance_squared_to(pos.xy) > 0.01: - continue - candidates = [] - for direction, wall in enumerate(cell.walls()): - if wall: - continue - dst_next = cell.neighbor(direction) - dst_ent = maze._cells[dst_next] - candidates.append((dst_ent, direction)) - candidates.sort( - key=lambda e: ( - 2 << 32 - if e[0] is motion.src - else Vector2(cells[e[0]][0].grid_pos).distance_squared_to( - target.pos - ) - ), - ) - if status.state is GhostState.flee: - lst = candidates[:-1] - shuffle(lst) - candidates = lst + [candidates[-1]] - nxt, nxt_dir = candidates[0] - motion.src = motion.pos - motion.pos = nxt - motion.direction = nxt_dir - - def spawn_pacman( world: World, assets: Assets[Sprite2D], @@ -427,6 +516,9 @@ def spawn_pacman( cells: Query[Cell], config: Config, ) -> None: + """ + System creating and adding pacman to the maze. + """ if (maze_t := maze_q.single()) is None: return maze, maze_ent = maze_t @@ -478,6 +570,9 @@ def spawn_ghosts( maze_q: Query[tuple[Maze, Entity]], config: Config, ) -> None: + """ + System creating and adding ghosts to the maze. + """ if (maze_t := maze_q.single()) is None: return maze, maze_ent = maze_t @@ -542,6 +637,9 @@ def move_pacman( cells: Query[tuple[Cell, Coord2D]], maze_q: Query[Maze], ) -> None: + """ + System giving target movement to pacman with the input given by the user. + """ if (maze := maze_q.single()) is None: return for coord, motion, pos in pacman_q: @@ -570,6 +668,9 @@ def collect_pacgum( score: GameState, config: Config, ) -> None: + """ + System managing the collision and collection of pacgum by pacman. + """ collided = [ pacgum for coord, pacgum in pacgums @@ -588,6 +689,10 @@ def collect_superpacgum( score: GameState, config: Config, ) -> None: + """ + System managing the collision and collection of superpacgum by pacman and + starting the ghost flee state if a superpacgum is collected. + """ collided = [ pacgum for pacgum_pos, pacgum in pacgums @@ -609,6 +714,9 @@ def collect_superpacgum( def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None: + """ + System reseting pacman position when touched by ghost. + """ for pacman, pos, motion in pacmans: pos.xy = Vector2(*pacman.home).elementwise() * 64 motion.pos = pacman.home_entity @@ -616,6 +724,9 @@ def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None: def respawn_ghost(ghosts: Query[tuple[Ghost, Coord2D, InMotion]]) -> None: + """ + System reseting ghost fleeing position when touched by pacman. + """ for ghost, pos, motion in ghosts: pos.xy = Vector2(*ghost.home).elementwise() * 64 motion.pos = ghost.home_entity @@ -633,6 +744,9 @@ def collide_ghost( state: GameState, config: Config, ) -> None: + """ + System managing the collision between by pacman and ghost. + """ for pacman_pos in pacmans: for ghost_pos, status, ghost, motion, pos in ghosts: if ( @@ -659,11 +773,56 @@ def collide_ghost( pass +def move_to_target( + entities: Query[tuple[TargetTile, InMotion, Coord2D, GhostStatus]], + cells: Query[tuple[Cell, Coord2D]], + maze_q: Query[Maze], +) -> None: + """ + System giving target movement to ghost. + """ + if (maze := maze_q.single()) is None: + return + for target, motion, pos, status in entities: + if status.state in (GhostState.dead, GhostState.respawning): + continue + cell, cell_pos = cells[motion.pos] + if cell_pos.xy.distance_squared_to(pos.xy) > 0.01: + continue + candidates = [] + for direction, wall in enumerate(cell.walls()): + if wall: + continue + dst_next = cell.neighbor(direction) + dst_ent = maze._cells[dst_next] + candidates.append((dst_ent, direction)) + candidates.sort( + key=lambda e: ( + 2 << 32 + if e[0] is motion.src + else Vector2(cells[e[0]][0].grid_pos).distance_squared_to( + target.pos + ) + ), + ) + if status.state is GhostState.flee: + lst = candidates[:-1] + shuffle(lst) + candidates = lst + [candidates[-1]] + nxt, nxt_dir = candidates[0] + motion.src = motion.pos + motion.pos = nxt + motion.direction = nxt_dir + + def update_ghost_targets( ghosts: Query[tuple[Ghost, TilePos, TargetTile, GhostStatus]], pacman_q: Query[tuple[InMotion, TilePos], Pacman], cells: Query[Cell], ) -> None: + """ + System updating target to ghost. + """ player = pacman_q.single() if player is None: return @@ -690,6 +849,9 @@ def update_ghost_targets( def update_chase_status(status_q: Query[GhostStatus], config: Config) -> None: + """ + System updating ghost status and cycle. + """ for status in status_q: target_time: int next_state: GhostState @@ -726,6 +888,9 @@ def update_chase_status(status_q: Query[GhostStatus], config: Config) -> None: def update_ghost_speed( ghost: Query[tuple[MoveSpeed, GhostStatus]], config: Config ) -> None: + """ + System updating ghost speed according to his state. + """ for speed, status in ghost: match status.state: case GhostState.chase | GhostState.wander: @@ -737,6 +902,9 @@ def update_ghost_speed( def update_pacman_anim( pacman_q: Query[tuple[InMotion, AnimationSet], Pacman], ) -> None: + """ + System updating pacman animation according to his direction. + """ for motion, anim in pacman_q: anim.idx = motion.direction @@ -745,6 +913,9 @@ def update_ghost_anim( ghost_q: Query[tuple[InMotion, AnimationSet, GhostStatus], Ghost], config: Config, ) -> None: + """ + System updating pacman animation according to his direction and state. + """ for motion, anim, status in ghost_q: match status.state: case GhostState.chase | GhostState.wander: @@ -760,10 +931,18 @@ def update_ghost_anim( def start_pause_timer(timer: "InitialPauseTimer") -> None: + """ + System starting the initial pause timer. + """ timer.tick = 0 def setup_new_maze(world: World) -> None: + """ + System setting up the maze by running 'setup_maze', 'spawn_superpacgums', + 'spawn_pacgums', 'spawn_pacman', 'spawn_ghosts' and + 'start_pause_timer' systems. + """ for system in ( setup_maze, spawn_superpacgums, @@ -776,10 +955,16 @@ def setup_new_maze(world: World) -> None: def maze_exists(itr: Query[Maze]) -> bool: + """ + System cheking maze existance. + """ return next(itr.__iter__(), None) is not None def in_game(world: World) -> bool: + """ + System cheking that the game is running. + """ return world.run_raw_system(maze_exists) and not world.run_raw_system( in_game_menu ) @@ -788,6 +973,9 @@ def in_game(world: World) -> bool: def level_is_finished( pacgums: Query[SuperPacGum | PacGum], _: Not[GameIsOver] ) -> bool: + """ + System cheking the level completion. + """ return next(iter(pacgums), None) is None @@ -797,6 +985,10 @@ def victory( queue: CommandQueue, state: GameState, ) -> None: + """ + System called when level is completed, generating a new level or finishing + the game if all levels are completed. + """ for maze in mazes: queue.queue(hierarchy.despawn_with_children(maze)) if state.level == 2: @@ -811,6 +1003,9 @@ def victory( def level_is_lost( config: Config, state: GameState, _: Not[GameIsOver] ) -> bool: + """ + System cheking the level is lost. + """ return config.level_timer < state.timer or state.lives <= 0 @@ -819,21 +1014,34 @@ def lose( mazes: Query[Entity, Maze], queue: CommandQueue, ) -> None: + """ + System called when the player lose the game. + """ for maze in mazes: queue.queue(hierarchy.despawn_with_children(maze)) queue.queue(lambda world: world.add_raw_system(game_over)()) def game_over(queue: CommandQueue) -> None: + # FIXME + """ + System called when game is finish. + """ queue.queue(lambda world: world.spawn_resource(GameIsOver())) print("Game over") def update_game_timer(state: GameState) -> None: + """ + System updating the levels timer. + """ state.timer += 1 def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool: + """ + System cheking if the inital timer is done. + """ if timer.tick < config.initial_pos_timer: timer.tick += 1 return False @@ -841,19 +1049,32 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool: def print_score(state: GameState) -> str: + """ + Return a string with the player score. + """ return f"Score: {state.score}" def print_lives(state: GameState) -> str: + """ + Return a string with the player lives. + """ return f"Lives: {state.lives}" def print_timer(state: GameState, config: Config) -> str: + """ + Return a string with the level timer. + """ time = config.level_timer - state.timer return f"Time left: {time // 60}" def spawn_hud(world: World, fonts: Assets[Font]) -> None: + """ + System that print the Heads-Up Display in game with lives, levels and timer + informations. + """ world.spawn_many( tuple[ WindowPositionned, @@ -891,6 +1112,9 @@ def start_game( hierarchy: Hierarchy[tuple[()]], menus: Query[Entity, MainMenu], ) -> None: + """ + System closing main menu and starting the game and Heads-Up Display. + """ for menu in menus: hierarchy.despawn_with_children(menu)(world) world.despawn_resource(SelectedButton) @@ -901,12 +1125,19 @@ def start_game( def quit_game(world: World) -> None: + """ + System closing the game and quiting the program. + """ world.spawn_resource(WorldShouldExit()) def spawn_button( world: World, fonts: Assets[Font], sprites: Assets[Sprite2D] ) -> Callable[[WindowPositionned, str, System], Entity]: + """ + System creating and adding a button to the world. + """ + def inner(pos: WindowPositionned, s: str, cb: System) -> Entity: parent = world.spawn( tuple[ @@ -953,6 +1184,9 @@ def back_to_main_menu( gui: Query[Entity, GameGui], queue: CommandQueue, ) -> None: + """ + System closing the current game and opening the main menu. + """ world.run_raw_system(close_game_menu) world.despawn_resource(GameState) world.despawn_many(list(gui)) @@ -962,10 +1196,16 @@ def back_to_main_menu( def in_game_menu(menus: Query[GameMenu]) -> bool: + """ + System cheking if the main menu is running. + """ return next(menus.__iter__(), None) is not None def toggle_game_menu(world: World) -> None: + """ + System opening or closing the game menu. + """ if world.run_raw_system(in_game_menu): world.run_raw_system(close_game_menu) else: @@ -978,6 +1218,9 @@ def close_game_menu( menus: Query[Entity, GameMenu], animated: Query[Entity, Pacman | Ghost], ) -> None: + """ + System closing the game menu. + """ world.remove_many(StopAnimation, list(animated)) for menu in menus: hierarchy.despawn_with_children(menu)(world) @@ -987,6 +1230,9 @@ def close_game_menu( def setup_game_menu( world: World, animated: Query[Entity, Pacman | Ghost] ) -> None: + """ + System opening the game menu. + """ world.insert_many(StopAnimation, [(e, StopAnimation()) for e in animated]) selected = world.run_system( spawn_button, @@ -1022,6 +1268,9 @@ def setup_game_menu( def set_up_instructions(world: World, fonts: Assets[Font]) -> list[Entity]: + """ + System adding instructions to main menu. + """ text = """Use 'WASD'to move pacman in the maze. Collect all pacgums to go to the next level. Complete all levels to win the game. @@ -1079,6 +1328,9 @@ When you eat a super pacgum, you can eat ghost. def set_up_highsores( world: World, scores: Highscores, fonts: Assets[Font] ) -> list[Entity]: + """ + System adding highscores to main menu. + """ highscores = [(name, value) for name, value in scores.scores.items()] highscores.sort(key=lambda x: x[1], reverse=True) entities = [ @@ -1127,7 +1379,9 @@ def set_up_highsores( def setup_main_menu(world: World) -> None: - + """ + System creating the main menu by adding button, instruction and highscores. + """ selected = world.run_system( spawn_button, WindowPositionned(Vector2(0.5), Vector2(-150, -100)), @@ -1158,6 +1412,9 @@ def setup_main_menu(world: World) -> None: def run_game() -> None: + """ + Start with all necessary plugins and system to run the game. + """ World().with_plugins(graphics_plugins, minimal_plugins).with_systems( StartupSchedule, Systems(load_config, load_highscores, setup_main_menu)