from pacman.ecs.update import PostUpdate, PreUpdate
from pacman.ecs.world import (
Component,
+ Entity,
+ Not,
Query,
Resource,
Systems,
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),
)
-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):
@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):
(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"),
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),
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,
).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()