From: = <=> Date: Wed, 14 Oct 2026 12:16:14 +0000 (+0200) Subject: Schedule fixes and window relative layout X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=54fbcbf18a588e06d4ed1ef90fe2aae7819ae9a3;p=axy%2Fft%2Fpacman.git Schedule fixes and window relative layout --- diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py index 00f684d..8cfb6e3 100644 --- a/src/pacman/ecs/graphics.py +++ b/src/pacman/ecs/graphics.py @@ -4,17 +4,20 @@ from time import time 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, @@ -32,6 +35,12 @@ class Window(Resource): 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} @@ -86,10 +95,49 @@ class Sprite2D(Component, Asset): 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({})) @@ -182,7 +230,12 @@ def graphics_plugins(world: World) -> None: ) .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 + ) diff --git a/src/pacman/ecs/gui.py b/src/pacman/ecs/gui.py index 6fcac7a..b0d2f9e 100644 --- a/src/pacman/ecs/gui.py +++ b/src/pacman/ecs/gui.py @@ -10,18 +10,16 @@ from pacman.ecs.assets import Asset 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, @@ -144,7 +142,7 @@ def update_computed_text( 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( @@ -186,5 +184,5 @@ def gui_plugins(world: World) -> None: update_text_sprites, ) .chain() - .in_set(PreUpdate) + .in_set(PreUpdate), ) diff --git a/src/pacman/ecs/schedule.py b/src/pacman/ecs/schedule.py index 58fe9b5..8536ae9 100644 --- a/src/pacman/ecs/schedule.py +++ b/src/pacman/ecs/schedule.py @@ -1,6 +1,7 @@ from collections import deque from collections.abc import Callable, Generator, Iterable from dataclasses import dataclass +from sys import stderr from typing import override @@ -23,9 +24,44 @@ class DAG[T]: 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]) diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index ddf30eb..ce88ecd 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -733,7 +733,8 @@ class World(FromWorld): 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], ...] @@ -772,15 +773,13 @@ class World(FromWorld): 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] @@ -936,7 +935,7 @@ class World(FromWorld): if schedule not in self._schedules: return for system in self._schedules[schedule].traverse(): - system() + self._systems[system]() def run( self, @@ -946,6 +945,11 @@ class World(FromWorld): 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, @@ -1016,7 +1020,7 @@ class Systems: 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}") @@ -1026,7 +1030,7 @@ class Systems: 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 ) @@ -1036,7 +1040,7 @@ class 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,) diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index bc4e0bd..7a604f7 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -23,6 +23,7 @@ from pacman.ecs.graphics import ( Inputs, ShouldRender, Sprite2D, + WindowPositionned, graphics_plugins, ) from pacman.ecs.gui import ( @@ -31,6 +32,7 @@ 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 @@ -93,6 +95,7 @@ class InitialPauseTimer(Resource): def extract_default( cls, ty: type, world: "World" ) -> Callable[[], "InitialPauseTimer | None"]: + del ty, world return InitialPauseTimer @@ -216,11 +219,21 @@ def create_maze( ) -> 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), @@ -783,7 +796,6 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool: def setup_resources(world: World) -> None: - world.spawn_resource(GameState()) world.spawn_resource(Config()) @@ -797,7 +809,40 @@ def print_lives(state: GameState) -> str: 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( @@ -805,11 +850,14 @@ 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()) @@ -817,12 +865,19 @@ def quit_game(world: World) -> None: 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( [ @@ -850,50 +905,17 @@ def spawn_button( 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, ) @@ -909,7 +931,9 @@ def setup_text(world: World, fonts: Assets[Font]) -> None: 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(