from collections.abc import Callable
from dataclasses import dataclass
+from enum import Enum, auto
from random import random
from mazegenerator import MazeGenerator
-from pygame import K_a, K_d, K_s, K_w, Vector3
+from pygame import K_a, K_d, K_s, K_w, Vector2, Vector3
from pygame.math import clamp
from pacman.ecs import minimal_plugins
self._y = y
+_DIR_OFFSETS = [(0, -1), (1, 0), (0, 1), (-1, 0)]
+
+
@dataclass
class Cell(Component):
n: int
grid_pos: tuple[int, int]
def neighbor(self, idx: int) -> tuple[int, int]:
- dx, dy = [(0, -1), (1, 0), (0, 1), (-1, 0)][idx]
+ dx, dy = _DIR_OFFSETS[idx]
return (self.grid_pos[0] + dx, self.grid_pos[1] + dy)
def walls(self) -> list[bool]:
@dataclass
class TargetTile(Component):
- pos: Entity
+ pos: tuple[int, int]
@dataclass
class InMotion(Component):
src: Entity
pos: Entity
+ direction: int
@dataclass
continue
dst_next = cell.neighbor(direction)
dst_ent = maze._cells[dst_next]
- candidates.append(dst_ent)
+ candidates.append((dst_ent, direction))
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
+ if e[0] is motion.src
+ else Vector2(cells[e[0]][0].grid_pos).distance_squared_to(
+ target.pos
)
),
)
- nxt = candidates[0]
+ nxt, nxt_dir = candidates[0]
motion.src = motion.pos
motion.pos = nxt
+ motion.direction = nxt_dir
@dataclass
@dataclass
class Pacman(Component):
- current_direction: int
+ pass
def spawn_pacman(
maze = maze_q.single()
if maze is None:
return
-
world.spawn(
tuple[
Coord2D,
],
6,
),
- Pacman(1),
+ Pacman(),
ShouldRender(),
- InMotion(maze._cells[(0, 0)], maze._cells[(0, 0)]),
+ InMotion(maze._cells[(0, 0)], maze._cells[(0, 0)], 1),
MoveSpeed(3.0),
TilePos(maze._cells[(0, 0)]),
),
)
+class GhostVariant(Enum):
+ A = auto()
+ B = auto()
+ C = auto()
+ D = auto()
+
+ def compute_target_tile(
+ self,
+ player: tuple[int, int],
+ player_dir: int,
+ pos: tuple[int, int],
+ slf: "Ghost",
+ blinky_pos: tuple[int, int],
+ ) -> tuple[int, int]:
+ match self:
+ case GhostVariant.A:
+ return player
+ case GhostVariant.B:
+ if Vector2(player).distance_squared_to(pos) >= 64:
+ return player
+ return slf.home
+ case GhostVariant.C:
+ dx, dy = _DIR_OFFSETS[player_dir]
+ x, y = player
+ return (x + 2 * dx, y + 2 * dy)
+ case GhostVariant.D:
+ dx, dy = _DIR_OFFSETS[player_dir]
+ x, y = player
+ x_target, y_target = (x + 2 * dx, y + 2 * dy)
+ x_blinky, y_blinky = blinky_pos
+ return ((x_target - x_blinky) * 2, (y_target - y_blinky) * 2)
+
+
+@dataclass
+class Ghost(Component):
+ variant: GhostVariant
+ home: tuple[int, int]
+
+
+def spawn_ghosts(
+ world: World, assets: Assets[Sprite2D], maze_q: Query[Maze]
+) -> None:
+ maze = maze_q.single()
+ if maze is None:
+ return
+ world.spawn_many(
+ tuple[
+ Coord2D,
+ CoordAbs2D,
+ AnimationSet,
+ ShouldRender,
+ InMotion,
+ MoveSpeed,
+ TilePos,
+ Ghost,
+ TargetTile,
+ ],
+ [
+ (
+ Coord2D(pos[0] * 64, pos[1] * 64, -10),
+ CoordAbs2D(),
+ AnimationSet(
+ [
+ [
+ assets.load(
+ f"./asset/enemy/ghost{variant.name}_{direction}.png"
+ )
+ ]
+ for direction in "URDL"
+ ],
+ 6,
+ ),
+ ShouldRender(),
+ InMotion(maze._cells[pos], maze._cells[pos], 1),
+ MoveSpeed(2.5),
+ TilePos(maze._cells[pos]),
+ Ghost(variant, pos),
+ TargetTile(pos),
+ )
+ for variant, pos in [
+ (GhostVariant.A, (0, 0)),
+ (GhostVariant.B, (maze._x - 1, 0)),
+ (GhostVariant.C, (0, maze._y - 1)),
+ (GhostVariant.D, (maze._x - 1, maze._y - 1)),
+ ]
+ ],
+ )
+
+
def move_pacman(
- pacman_q: Query[tuple[Pacman, Coord2D, InMotion, TilePos]],
+ pacman_q: Query[tuple[Coord2D, InMotion, TilePos], Pacman],
inputs: Inputs,
cells: Query[tuple[Cell, Coord2D]],
maze_q: Query[Maze],
maze = maze_q.single()
if maze is None:
return
- for pacman, coord, motion, pos in pacman_q:
+ for coord, motion, pos in pacman_q:
direction = inputs.get_dir()
cell, cell_pos = cells[pos.pos]
if coord.xy.distance_to(cell_pos.xy) < 12.0:
if direction is None:
- direction = pacman.current_direction
+ direction = motion.direction
if cell.walls()[direction]:
- direction = pacman.current_direction
+ direction = motion.direction
if not cell.walls()[direction]:
motion.src = pos.pos
motion.pos = maze._cells[cell.neighbor(direction)]
- pacman.current_direction = direction
- elif ((pacman.current_direction + 2) % 4) == direction:
+ motion.direction = direction
+ elif ((motion.direction + 2) % 4) == direction:
tmp = motion.pos
motion.pos = motion.src
motion.src = tmp
- pacman.current_direction = direction
+ motion.direction = direction
-def update_pacman_anim(pacman_q: Query[tuple[Pacman, AnimationSet]]) -> None:
- for pacman, anim in pacman_q:
- anim.idx = pacman.current_direction
+@dataclass
+class ChaseStatus(Resource):
+ tick: int = 0
+ cycle_chase: int = 60 * 10
+ cycle_wander: int = 60 * 10
+ chase: bool = False
+
+
+def update_ghost_targets(
+ ghosts: Query[tuple[Ghost, TilePos, TargetTile]],
+ pacman_q: Query[tuple[InMotion, TilePos], Pacman],
+ cells: Query[Cell],
+ chase: ChaseStatus,
+) -> None:
+ player = pacman_q.single()
+ if player is None:
+ return
+ player_motion, player_pos = player
+ blinky_pos = next(
+ pos.pos for ghost, pos, _ in ghosts if ghost.variant == GhostVariant.A
+ )
+ for ghost, pos, target in ghosts:
+ if not chase.chase:
+ target.pos = ghost.home
+ continue
+ target.pos = ghost.variant.compute_target_tile(
+ cells[player_pos.pos].grid_pos,
+ player_motion.direction,
+ cells[pos.pos].grid_pos,
+ ghost,
+ cells[blinky_pos].grid_pos,
+ )
+
+
+def update_chase_status(status: ChaseStatus) -> None:
+ status.tick += 1
+ status.chase = status.tick > status.cycle_wander
+ if status.tick < (status.cycle_wander + status.cycle_chase):
+ return
+ status.tick = 0
+ if status.cycle_wander > 60 * 4:
+ status.cycle_wander -= 60
+ if status.cycle_chase > 60 * 7:
+ status.cycle_chase -= 60
+
+
+def update_pacman_anim(
+ pacman_q: Query[tuple[InMotion, AnimationSet], Pacman],
+) -> None:
+ for motion, anim in pacman_q:
+ anim.idx = motion.direction
+
+
+def update_ghost_anim(
+ pacman_q: Query[tuple[InMotion, AnimationSet], Ghost],
+) -> None:
+ for motion, anim in pacman_q:
+ anim.idx = motion.direction
def setup_resources(world: World) -> None:
world.spawn(Inputs, Inputs(set()))
+ world.spawn(ChaseStatus, ChaseStatus())
def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
spawn_superpacgums,
spawn_pacgums,
spawn_pacman,
+ spawn_ghosts,
).chain(),
).with_systems(
UpdateSchedule,
- Systems(move_pacman, move_in_motion, update_tile_pos, move_to_target)
+ Systems(
+ move_pacman,
+ update_tile_pos,
+ update_ghost_targets,
+ move_to_target,
+ move_in_motion,
+ update_chase_status,
+ )
.chain()
.in_set(Update),
Systems(update_inputs).in_set(PreUpdate).after(InputsSystems),
- Systems(update_pacman_anim).before(GraphicsSystems).in_set(PostUpdate),
+ Systems(update_pacman_anim, update_ghost_anim)
+ .before(GraphicsSystems)
+ .in_set(PostUpdate),
).run_main()