]> Untitled Git - axy/ft/pacman.git/commitdiff
Ghost pathfinding impl
author= <=>
Wed, 30 Sep 2026 12:51:54 +0000 (14:51 +0200)
committer= <=>
Wed, 30 Sep 2026 12:51:54 +0000 (14:51 +0200)
src/pacman/ecs/graphics.py
src/pacman/game/__init__.py

index 1eb2bf8c7fad4452c61bd1012a72983ea141d831..dcc587128d159c5c9bc2c95577c940462401f675 100644 (file)
@@ -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),
     )
index 4b5af6df2902109b862c3152b0ed843f05af78e7..c5a8edfc8529f680638ed4961b2967a44d09f463 100644 (file)
@@ -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()