From: = <=> Date: Wed, 30 Sep 2026 12:51:54 +0000 (+0200) Subject: Ghost pathfinding impl X-Git-Url: https://git.uwuaxy.net/Project%20Management.png?a=commitdiff_plain;h=7d2ebebf8a275ea755c96cc08b53daecefe89bd9;p=axy%2Fft%2Fpacman.git Ghost pathfinding impl --- diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py index 1eb2bf8..dcc5871 100644 --- a/src/pacman/ecs/graphics.py +++ b/src/pacman/ecs/graphics.py @@ -10,6 +10,8 @@ from pacman.ecs.schedule import StartupSchedule, SystemSet, UpdateSchedule from pacman.ecs.update import PostUpdate, PreUpdate from pacman.ecs.world import ( Component, + Entity, + Not, Query, Resource, Systems, @@ -92,14 +94,54 @@ def render_sprite2d( pygame.display.flip() +@dataclass +class AnimationSet(Component): + anims: list[list[Sprite2D]] + ticks_per_frame: int + idx: int = 0 + frame: int = 0 + curr_tick: int = 0 + + @property + def curr_frame(self) -> Sprite2D: + return self.anims[self.idx][self.frame] + + def tick(self) -> None: + self.curr_tick += 1 + self.frame += self.curr_tick // self.ticks_per_frame + self.curr_tick %= self.ticks_per_frame + self.frame %= len(self.anims[self.idx]) + + +def insert_animation_sprites( + world: World, query: Query[tuple[Entity, AnimationSet], Not[Sprite2D]] +) -> None: + inserts = [] + for ent, anim in query: + inserts.append((ent, anim.curr_frame)) + world.insert_many(Sprite2D, inserts) + + +def update_animations( + query: Query[tuple[Sprite2D, AnimationSet], ShouldRender], +) -> None: + for sprite, anim in query: + sprite.surface = anim.curr_frame.surface + anim.tick() + + GraphicsSystems = SystemSet("graphics-systems") def graphics_plugins(world: World) -> None: world.with_systems(StartupSchedule, init_window).with_systems( UpdateSchedule, - Systems(render_sprite2d) - .in_set(PostUpdate, GraphicsSystems) - .after(PropagateTransform), + Systems( + insert_animation_sprites, + update_animations, + Systems(render_sprite2d).after(PropagateTransform), + ) + .chain() + .in_set(PostUpdate, GraphicsSystems), Systems(poll_events, close_on_close).chain().in_set(PreUpdate), ) diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index 4b5af6d..c5a8edf 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -1,28 +1,24 @@ -import math from collections.abc import Callable from dataclasses import dataclass -from pathlib import Path from random import random -import pygame from mazegenerator import MazeGenerator -from pygame import K_a, K_d, K_s, K_w, Vector2, Vector3 +from pygame import Vector3 +from pygame.math import clamp from pacman.ecs import minimal_plugins from pacman.ecs.assets import Assets from pacman.ecs.coord import Coord2D, CoordAbs2D from pacman.ecs.graphics import ( - GraphicsSystems, + AnimationSet, ShouldRender, Sprite2D, - WindowEvents, graphics_plugins, ) from pacman.ecs.hierarchy import ChildOf from pacman.ecs.schedule import StartupSchedule, UpdateSchedule -from pacman.ecs.update import PostUpdate, PreUpdate -from pacman.ecs.world import Component, Entity, Not, Query, Systems, World -from pacman.utils.panic import panic +from pacman.ecs.update import Update +from pacman.ecs.world import Component, Entity, Query, Systems, World class Maze(Component): @@ -35,7 +31,14 @@ class Maze(Component): @dataclass class Cell(Component): n: int - pass + grid_pos: tuple[int, int] + + def neighbor(self, idx: int) -> tuple[int, int]: + dx, dy = [(0, -1), (1, 0), (0, 1), (-1, 0)][idx] + return (self.grid_pos[0] + dx, self.grid_pos[1] + dy) + + def walls(self) -> list[bool]: + return [(1 & (self.n >> i)) == 1 for i in range(4)] class PacGum(Component): @@ -60,7 +63,7 @@ def create_maze( (x, y): ( ChildOf(root), Coord2D(cell_dims.elementwise() * Vector3(x, y, 0)), - Cell(cell), + Cell(cell, (x, y)), ShouldRender(), CoordAbs2D(), assets.load(f"./asset/walls/{cell}.png"), @@ -143,100 +146,107 @@ def setup_maze(world: World) -> None: world.add_system(create_maze)(MazeGenerator()) -class PlayerControlled(Component): - def __init__(self) -> None: - self._dir: int = 0 +@dataclass +class TargetTile(Component): + pos: Entity - @property - def direction(self) -> Vector2: - match self._dir: - case 0: - return Vector2(0, -1) - case 1: - return Vector2(1, 0) - case 2: - return Vector2(0, 1) - case 3: - return Vector2(-1, 0) - case _: - raise Exception() +@dataclass +class TilePos(Component): + pos: Entity -def update_player_controlled( - entities: Query[PlayerControlled], events: WindowEvents -) -> None: - vec = None - for key in events.keys(): - if key == K_w: - vec = 0 - if key == K_d: - vec = 1 - if key == K_s: - vec = 2 - if key == K_a: - vec = 3 - if vec is not None: - for ctl in entities: - ctl._dir = vec - - -def move_player_controlled( - entities: Query[tuple[Coord2D, PlayerControlled]], -) -> None: - for pos, ctl in entities: - direction = ctl.direction * 4 - pos += Vector3(direction.x, direction.y, 0) + +@dataclass +class InMotion(Component): + src: Entity + pos: Entity @dataclass -class AnimationSet(Component): - anims: list[list[Sprite2D]] - ticks_per_frame: int - idx: int = 0 - frame: int = 0 - curr_tick: int = 0 - - @property - def curr_frame(self) -> Sprite2D: - return self.anims[self.idx][self.frame] - - def tick(self) -> None: - self.curr_tick += 1 - self.frame += self.curr_tick // self.ticks_per_frame - self.curr_tick %= self.ticks_per_frame - self.frame %= len(self.anims[self.idx]) - - -def insert_animation_sprites( - world: World, query: Query[tuple[Entity, AnimationSet], Not[Sprite2D]] +class MoveSpeed(Component): + pixels_per_frame: float + + +def move_in_motion( + entities: Query[tuple[InMotion, Coord2D, MoveSpeed]], + positions: Query[Coord2D], +) -> None: + for motion, pos, speed in entities: + if (dst := positions.get(motion.pos)) is None: + continue + ppf = speed.pixels_per_frame + dx = clamp(dst.x - pos.x, -ppf, ppf) + dy = clamp(dst.y - pos.y, -ppf, ppf) + pos.x += dx + pos.y += dy + + +def update_tile_pos( + entities: Query[tuple[InMotion, Coord2D, TilePos]], + cells: Query[Coord2D], ) -> None: - inserts = [] - for ent, anim in query: - inserts.append((ent, anim.curr_frame)) - world.insert_many(Sprite2D, inserts) + for motion, coord, pos in entities: + src = cells[motion.src] + dst = cells[motion.pos] + if src.distance_squared_to(coord) < dst.distance_squared_to(coord): + continue + pos.pos = motion.pos -def update_animations( - query: Query[tuple[Sprite2D, AnimationSet], ShouldRender], +def move_to_target( + entities: Query[tuple[TargetTile, InMotion, Coord2D]], + cells: Query[tuple[Cell, Coord2D]], + maze_q: Query[Maze], ) -> None: - for sprite, anim in query: - sprite.surface = anim.curr_frame.surface - anim.tick() + maze = maze_q.single() + if maze is None: + return + for target, motion, pos in entities: + 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) + candidates.sort( + key=lambda e: ( + 2 << 32 + if e is motion.src + else cells[e][1].xy.distance_squared_to( + cells[target.pos][1].xy + ) + ), + ) + nxt = candidates[0] + motion.src = motion.pos + motion.pos = nxt class Pacman(Component): pass -def spawn_pacman(world: World, assets: Assets[Sprite2D]) -> None: +def spawn_pacman( + world: World, assets: Assets[Sprite2D], maze_q: Query[Maze] +) -> None: + maze = maze_q.single() + if maze is None: + return + world.spawn( tuple[ Coord2D, CoordAbs2D, AnimationSet, Pacman, - PlayerControlled, ShouldRender, + TargetTile, + InMotion, + MoveSpeed, ], ( Coord2D(0, 0, -10), @@ -254,19 +264,14 @@ def spawn_pacman(world: World, assets: Assets[Sprite2D]) -> None: 4, ), Pacman(), - PlayerControlled(), ShouldRender(), + TargetTile(maze._cells[(14, 14)]), + InMotion(maze._cells[(0, 0)], maze._cells[(0, 0)]), + MoveSpeed(4.0), ), ) -def update_pacman_direction( - query: Query[tuple[AnimationSet, PlayerControlled], Pacman], -) -> None: - for anims, ctl in query: - anims.idx = ctl._dir - - def run_game() -> None: World().with_plugins(graphics_plugins, minimal_plugins).with_systems( StartupSchedule, @@ -275,15 +280,7 @@ def run_game() -> None: ).chain(), ).with_systems( UpdateSchedule, - Systems(update_player_controlled, move_player_controlled) - .chain() - .in_set(PreUpdate), - Systems( - insert_animation_sprites, - update_pacman_direction, - update_animations, - ) + Systems(move_in_motion, update_tile_pos, move_to_target) .chain() - .before(GraphicsSystems) - .in_set(PostUpdate), + .in_set(Update), ).run_main()