from typing import cast, override
import pygame
-from pygame import K_RETURN, K_a, K_d, K_s, K_w
+from pygame import K_RETURN, K_a, K_d, K_s, K_w, Vector2
from pacman.ecs.assets import Asset
-from pacman.ecs.coord import CoordAbs2D, PropagateTransform
+from pacman.ecs.coord import Coord2D, CoordAbs2D, PropagateTransform
from pacman.ecs.schedule import StartupSchedule, SystemSet, UpdateSchedule
from pacman.ecs.update import PostUpdate, PreUpdate
from pacman.ecs.world import (
Component,
Entity,
+ Maybe,
Not,
Query,
+ RawComponentArg,
+ RawComponentHook,
Resource,
Systems,
World,
clock: pygame.time.Clock
+@dataclass
+class WindowPositionned(Component):
+ pos: Vector2
+ offset: Vector2
+
+
dir_lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3}
return Sprite2D(self.surface)
+def window_positionned_insert_hook(
+ elems: Query[tuple[Coord2D | None, CoordAbs2D | None]],
+ window: Maybe[Window],
+ world: World,
+) -> RawComponentHook[WindowPositionned]:
+ def inner(added: RawComponentArg[WindowPositionned]) -> None:
+ dims = Vector2(0, 0)
+ if (win := window.v) is not None:
+ dims = Vector2(win.surface.get_size())
+
+ added_coords = []
+ added_abs = []
+ for e, comp in added:
+ coord, coord_abs = elems[e]
+ pos = dims.elementwise() * comp.pos + comp.offset
+ if coord is not None:
+ coord.xy = pos
+ else:
+ added_coords.append((e, Coord2D(pos.x, pos.y, -1)))
+ if coord_abs is None:
+ added_abs.append((e, CoordAbs2D()))
+
+ world.insert_many(Coord2D, added_coords)
+ world.insert_many(CoordAbs2D, added_abs)
+
+ return inner
+
+
+def update_window_positionned(
+ elems: Query[tuple[WindowPositionned, Coord2D]], window: Window
+) -> None:
+ dims = Vector2(window.surface.get_size())
+ for pos, e in elems:
+ e.xy = dims.elementwise() * pos.pos + pos.offset
+
+
def init_graphics(world: World) -> None:
pygame.init()
world.spawn_resource(
- Window(pygame.display.set_mode((1600, 1200)), pygame.time.Clock()),
+ Window(
+ pygame.display.set_mode((1600, 1200), pygame.RESIZABLE),
+ pygame.time.Clock(),
+ ),
)
world.spawn_resource(WindowEvents([]))
world.spawn_resource(Inputs({}))
)
.chain()
.in_set(PostUpdate, GraphicsSystems),
+ Systems(update_window_positionned)
+ .before(PropagateTransform)
+ .in_set(PostUpdate),
Systems(poll_events, close_on_close, update_inputs)
.chain()
.in_set(PreUpdate, InputsSystems),
- ).with_plugins(gui_plugins)
+ ).with_plugins(gui_plugins).with_hook(
+ "insert", WindowPositionned, window_positionned_insert_hook
+ )
from pacman.ecs.coord import (
CoordAbs2D,
CoordBundle,
- PropagateTransform,
)
from pacman.ecs.graphics import (
AnimationSet,
- GraphicsSystems,
Inputs,
Sprite2D,
WindowEvents,
dir_lookup,
)
from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
-from pacman.ecs.update import PostUpdate, PreUpdate
+from pacman.ecs.update import PreUpdate
from pacman.ecs.world import (
CommandQueue,
Component,
world: World,
) -> None:
for display, compute in elems:
- display.text = world.run_raw_system(compute.cb)
+ display.text = world.run_raw_system(compute.cb, lambda: display.text)
def update_text_sprites(
update_text_sprites,
)
.chain()
- .in_set(PreUpdate)
+ .in_set(PreUpdate),
)
from collections import deque
from collections.abc import Callable, Generator, Iterable
from dataclasses import dataclass
+from sys import stderr
from typing import override
return self._nodes[node]
def add_edge(self, parent: T, child: T) -> None:
+ parents = self.node_parents(parent)
+ children = self.node_children(child)
+ shared = parents & children
+ if len(shared) != 0:
+ print(
+ f"Warning: adding edge between {parent} and {child} "
+ + f"create cycle at {shared}",
+ file=stderr,
+ )
self._get_node(parent).children.add(child)
self._get_node(child).parents.add(parent)
+ def node_parents(self, node: T) -> set[T]:
+ if node not in self._nodes:
+ return set()
+ res = set()
+ queue = [node]
+ while len(queue) != 0:
+ curr = queue.pop()
+ if curr in res:
+ continue
+ res.add(curr)
+ queue.extend(self._nodes[curr].parents)
+ return res
+
+ def node_children(self, node: T) -> set[T]:
+ if node not in self._nodes:
+ return set()
+ res = set()
+ queue = [node]
+ while len(queue) != 0:
+ curr = queue.pop()
+ if curr in res:
+ continue
+ res.add(curr)
+ queue.extend(self._nodes[curr].children)
+ return res
+
def traverse_from(self, root: T) -> Generator[T]:
visited: set[T] = set()
queue = deque([root])
tuple[type, ComponentHooks[Component]]
] = []
self._components_lookup: dict[type, ComponentID] = {}
- self._schedules: dict[ScheduleLabel, Schedule[RawSystem[Any]]] = {}
+ self._schedules: dict[ScheduleLabel, Schedule[System[Any]]] = {}
+ self._systems: dict[System[Any], RawSystem[Any]] = {}
self._base_archetype: Archetype = Archetype(self, ())
self._extractors_cache: dict[
System[Any], tuple[Callable[[], FromWorld | None], ...]
self.despawn_resource(ty)
return self.spawn(ty, comp)
- def despawn_resource[T: Resource](
- self, ty: type[T]
- ) -> None:
- type Ty = ty # type: ignore
-
- def remove(query: Query[Entity, Ty], world: World) -> None:
- world.remove_many(ty, list(query))
-
- self.run_raw_system(remove)
+ def despawn_resource[T: Resource](self, ty: type[T]) -> None:
+ type Ty = ty # type: ignore
+
+ def remove(query: Query[Entity, Ty], world: World) -> None:
+ world.remove_many(ty, list(query))
+
+ self.run_raw_system(remove)
def spawn_many[T: Components](
self, ty: type[T], comps: list[T]
if schedule not in self._schedules:
return
for system in self._schedules[schedule].traverse():
- system()
+ self._systems[system]()
def run(
self,
while not stop_cond():
self.tick(schedule)
+ def _register_schedule_system[T](self, system: System[T]) -> System[T]:
+ if system not in self._systems:
+ self._systems[system] = self.add_raw_system(system, lambda: None)
+ return system
+
def with_systems(
self,
schedule: ScheduleLabel,
self._conds.extend(conds)
return self
- def _apply(self, world: World, sched: Schedule[RawSystem]) -> SystemSet:
+ def _apply(self, world: World, sched: Schedule[System[Any]]) -> SystemSet:
global _order_groups
_order_groups += 1
label = SystemSet(f"order-group-{_order_groups}")
else (
e
if isinstance(e, SystemSet)
- else world.add_raw_system(e, default=lambda: None)
+ else world._register_schedule_system(e)
)
for e in self._systems
)
)
sched.add_ordering((label,), self._pre, self._post, self._in_set, None)
if self._chain:
- prev: tuple[SystemSet | RawSystem] | tuple[()] = ()
+ prev: tuple[SystemSet | System[Any]] | tuple[()] = ()
for system in systems:
sched.add_ordering((system,), prev, (), (label,), None)
prev = (system,)
Inputs,
ShouldRender,
Sprite2D,
+ WindowPositionned,
graphics_plugins,
)
from pacman.ecs.gui import (
Font,
SelectedButton,
TextDisplay,
+ setup_gui_resources,
)
from pacman.ecs.hierarchy import ChildOf, Children, Hierarchy
from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
def extract_default(
cls, ty: type, world: "World"
) -> Callable[[], "InitialPauseTimer | None"]:
+ del ty, world
return InitialPauseTimer
) -> Callable[[MazeGenerator], Entity]:
def inner(maze: MazeGenerator) -> Entity:
root_maze = Maze(len(maze.maze[0]), len(maze.maze))
+ cell_dims = Vector2(64, 64)
root = world.spawn(
- tuple[Maze, CoordBundle],
- (root_maze, CoordBundle(100, 100)),
+ tuple[Maze, WindowPositionned],
+ (
+ root_maze,
+ WindowPositionned(
+ Vector2(0.5),
+ -(
+ Vector2(root_maze._x, root_maze._y).elementwise()
+ * cell_dims
+ / 2
+ ),
+ ),
+ ),
)
- cell_dims = Vector2(64, 64)
cells = {
(x, y): (
ChildOf(root),
def setup_resources(world: World) -> None:
- world.spawn_resource(GameState())
world.spawn_resource(Config())
def print_timer(state: GameState, config: Config) -> str:
time = config.level_timer - state.timer
- return f"{time // 60}"
+ return f"Time left: {time // 60}"
+
+
+def spawn_hud(world: World, fonts: Assets[Font]) -> None:
+ world.spawn_many(
+ tuple[
+ WindowPositionned,
+ CoordBundle,
+ ShouldRender,
+ ComputedText,
+ TextDisplay,
+ GameGui,
+ ],
+ [
+ (
+ WindowPositionned(Vector2(), Vector2(20, y * 100 + 20)),
+ CoordBundle(depth=-20),
+ ShouldRender(),
+ ComputedText(fn),
+ TextDisplay(
+ "",
+ fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
+ ),
+ GameGui(),
+ )
+ for y, fn in enumerate(
+ [
+ print_score,
+ print_lives,
+ print_timer,
+ ],
+ )
+ ],
+ )
def start_game(
hierarchy: Hierarchy[tuple[()]],
menus: Query[Entity, MainMenu],
) -> None:
- world.run_raw_system(setup_new_maze)
for menu in menus:
hierarchy.despawn_with_children(menu)(world)
world.despawn_resource(SelectedButton)
-
+
+ world.spawn_resource(GameState())
+ world.run_raw_system(setup_new_maze)
+ world.run_raw_system(spawn_hud)
+
def quit_game(world: World) -> None:
world.spawn_resource(WorldShouldExit())
def spawn_button(
world: World, fonts: Assets[Font], sprites: Assets[Sprite2D]
-) -> Callable[[Vector2, str, System], Entity]:
- def inner(pos: Vector2, s: str, cb: System) -> Entity:
+) -> Callable[[WindowPositionned, str, System], Entity]:
+ def inner(pos: WindowPositionned, s: str, cb: System) -> Entity:
parent = world.spawn(
- tuple[CoordBundle, ShouldRender, AnimationSet, Button],
+ tuple[
+ WindowPositionned,
+ CoordBundle,
+ ShouldRender,
+ AnimationSet,
+ Button,
+ ],
(
- CoordBundle(*pos.xy, depth=-100),
+ pos,
+ CoordBundle(depth=-100),
ShouldRender(),
AnimationSet(
[
return inner
-def setup_text(world: World, fonts: Assets[Font]) -> None:
- world.spawn_many(
- tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay],
- [
- (
- CoordBundle(depth=-20),
- ShouldRender(),
- ComputedText(print_score),
- TextDisplay(
- "",
- fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
- ),
- ),
- (
- CoordBundle(y=100, depth=-20),
- ShouldRender(),
- ComputedText(print_lives),
- TextDisplay(
- "",
- fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
- ),
- ),
- (
- CoordBundle(y=200, depth=-20),
- ShouldRender(),
- ComputedText(print_timer),
- TextDisplay(
- "",
- fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
- ),
- ),
- ],
- )
-
+def setup_main_menu(world: World) -> None:
selected = world.run_system(
spawn_button,
- Vector2((1600 - 300) / 2, (1200 - 200) / 2),
+ WindowPositionned(Vector2(0.5), Vector2(-150, -100)),
"Start game",
start_game,
)
quit_button = world.run_system(
spawn_button,
- Vector2((1600 - 300) / 2, (1200 - 200) / 2 + 120),
+ WindowPositionned(Vector2(0.5), Vector2(-150, 20)),
"Quit",
quit_game,
)
def run_game() -> None:
World().with_plugins(graphics_plugins, minimal_plugins).with_systems(
StartupSchedule,
- Systems(setup_resources, setup_text).chain(),
+ Systems(setup_resources, setup_main_menu)
+ .chain()
+ .after(setup_gui_resources),
).with_systems(
UpdateSchedule,
Systems(