From: = <=> Date: Mon, 28 Sep 2026 11:48:19 +0000 (+0200) Subject: Minor refactor X-Git-Url: https://git.uwuaxy.net/?a=commitdiff_plain;h=1e37a1f3c2a03ee1f729ad14dcebf1fb18406526;p=axy%2Fft%2Fpacman.git Minor refactor --- diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py index 6b3c0e2..cce709c 100644 --- a/src/pacman/ecs/__init__.py +++ b/src/pacman/ecs/__init__.py @@ -1,166 +1,36 @@ -from dataclasses import dataclass from pathlib import Path -from typing import override import pygame -from pygame import Vector3 -from pacman.ecs.assets import Asset, Assets +from pacman.ecs.assets import Assets +from pacman.ecs.coord import ( + Coord2D, + CoordAbs2D, + coord_update_plugins, +) +from pacman.ecs.graphics import ShouldRender, Sprite2D, graphics_plugins from pacman.ecs.hierarchy import ( ChildOf, - Hierarchy, hierachy_plugins, ) from pacman.ecs.schedule import ( - MainSchedule, StartupSchedule, - SystemSet, UpdateSchedule, ) +from pacman.ecs.update import schedule_plugins from pacman.ecs.world import ( Component, - Not, Query, - Resource, - Systems, World, - WorldShouldExit, ) -class WorldIsInit(Resource): - pass - - -def run_startup(world: World, _: Not[WorldIsInit]) -> None: - world.tick(StartupSchedule) - world.spawn(WorldIsInit, WorldIsInit()) - - -def run_update(world: World, _: WorldIsInit) -> None: - world.tick(UpdateSchedule) - - -PreUpdate = SystemSet("preupdate") -Update = SystemSet("update") -PostUpdate = SystemSet("postupdate") - - -def schedule_plugins(world: World) -> None: - world.with_systems(MainSchedule, run_startup, run_update).with_systems( - UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain() - ) - - -class CoordAbs2D(Component): - _vec: Vector3 - - def __init__(self) -> None: - self._vec = Vector3(0.0) - - -class Coord2D(Vector3, Component): - pass - - -def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None: - for abs_coord, coord in entities: - abs_coord._vec = coord.xyz - - -def update_coord_hierarchy(hierarchy: Hierarchy[CoordAbs2D]) -> None: - for parent, child in hierarchy.top_down(): - if parent is None: - continue - child._vec += parent._vec - - -PropagateTransform = SystemSet("propagate-transform") - - -def coord_update_plugins(world: World) -> None: - world.with_systems( - UpdateSchedule, - Systems(update_coord, update_coord_hierarchy) - .chain() - .in_set(PostUpdate, PropagateTransform), - ) - world.with_systems(UpdateSchedule, update_coord) - - def minimal_plugins(world: World) -> None: world.with_plugins( schedule_plugins, hierachy_plugins, coord_update_plugins ) -class ShouldRender(Component): - pass - - -@dataclass -class Window(Resource): - surface: pygame.Surface - clock: pygame.time.Clock - - -@dataclass -class WindowEvents(Resource): - events: list[pygame.event.Event] - - -def init_window(world: World) -> None: - pygame.init() - world.spawn(Window, Window(pygame.display.set_mode(), pygame.time.Clock())) - world.spawn(WindowEvents, WindowEvents([])) - - -@dataclass -class Sprite2D(Component, Asset): - surface: pygame.Surface - - @override - @classmethod - def load(cls, ty: type, path: str) -> "Sprite2D": - del ty - return Sprite2D(pygame.image.load(path)) - - -def poll_events(events: WindowEvents) -> None: - events.events = pygame.event.get() - - -def close_on_close(world: World, events: WindowEvents) -> None: - if pygame.QUIT in (event.type for event in events.events): - world.spawn(WorldShouldExit, WorldShouldExit()) - - -def render_sprite2d( - window: Window, - sprites_q: Query[tuple[CoordAbs2D, Sprite2D], ShouldRender], -) -> None: - sprites_raw = sorted( - sprites_q, - key=lambda e: e[0]._vec.z, - reverse=True, - ) - sprites = [ - (sprite.surface, coord._vec.xy) for coord, sprite in sprites_raw - ] - window.surface.fill((0, 0, 0)) - window.surface.blits(sprites) - window.clock.tick(60) - pygame.display.flip() - - -def graphics_plugins(world: World) -> None: - world.with_systems(StartupSchedule, init_window).with_systems( - UpdateSchedule, - Systems(render_sprite2d).in_set(PostUpdate).after(PropagateTransform), - Systems(poll_events, close_on_close).chain().in_set(PreUpdate), - ) - - class WiggleSprite(Component): pass @@ -222,7 +92,6 @@ def setup_world(world: World, assets: Assets[Sprite2D]) -> None: for i, asset_path in enumerate(asset_paths) ], ) - pass if __name__ == "__main__": diff --git a/src/pacman/ecs/assets.py b/src/pacman/ecs/assets.py index 0b80925..ffb47d1 100644 --- a/src/pacman/ecs/assets.py +++ b/src/pacman/ecs/assets.py @@ -2,7 +2,7 @@ from abc import ABC, abstractmethod from collections.abc import Callable from os import PathLike from os.path import abspath -from typing import Self, cast, get_args, override +from typing import Self, get_args, override from pacman.ecs.world import Resource, World from pacman.utils.type_resolve import resolve_type_aliases @@ -27,12 +27,8 @@ class Assets[T: Asset](Resource): @override @classmethod - def extract_from_world( + def extract_default( cls, ty: type, world: World ) -> Callable[[], Self | None]: - inner = cast( - Callable[[], Self | None], Resource.extract_from_world(ty, world) - ) - return lambda: cast( - Self, (res if (res := inner()) is not None else Assets(ty)) - ) + del world + return lambda: cls(ty) diff --git a/src/pacman/ecs/coord.py b/src/pacman/ecs/coord.py new file mode 100644 index 0000000..2b8108e --- /dev/null +++ b/src/pacman/ecs/coord.py @@ -0,0 +1,42 @@ +from pygame import Vector3 + +from pacman.ecs.hierarchy import Hierarchy +from pacman.ecs.schedule import SystemSet, UpdateSchedule +from pacman.ecs.update import PostUpdate +from pacman.ecs.world import Component, Query, Systems, World + + +class CoordAbs2D(Component): + _vec: Vector3 + + def __init__(self) -> None: + self._vec = Vector3(0.0) + + +class Coord2D(Vector3, Component): + pass + + +def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None: + for abs_coord, coord in entities: + abs_coord._vec = coord.xyz + + +def update_coord_hierarchy(hierarchy: Hierarchy[CoordAbs2D]) -> None: + for parent, child in hierarchy.top_down(): + if parent is None: + continue + child._vec += parent._vec + + +PropagateTransform = SystemSet("propagate-transform") + + +def coord_update_plugins(world: World) -> None: + world.with_systems( + UpdateSchedule, + Systems(update_coord, update_coord_hierarchy) + .chain() + .in_set(PostUpdate, PropagateTransform), + ) + world.with_systems(UpdateSchedule, update_coord) diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py new file mode 100644 index 0000000..15e911d --- /dev/null +++ b/src/pacman/ecs/graphics.py @@ -0,0 +1,84 @@ +from dataclasses import dataclass +from typing import override + +import pygame + +from pacman.ecs.assets import Asset +from pacman.ecs.coord import CoordAbs2D, PropagateTransform +from pacman.ecs.schedule import StartupSchedule, UpdateSchedule +from pacman.ecs.update import PostUpdate, PreUpdate +from pacman.ecs.world import ( + Component, + Query, + Resource, + Systems, + World, + WorldShouldExit, +) + + +class ShouldRender(Component): + pass + + +@dataclass +class Window(Resource): + surface: pygame.Surface + clock: pygame.time.Clock + + +@dataclass +class WindowEvents(Resource): + events: list[pygame.event.Event] + + +def init_window(world: World) -> None: + pygame.init() + world.spawn(Window, Window(pygame.display.set_mode(), pygame.time.Clock())) + world.spawn(WindowEvents, WindowEvents([])) + + +@dataclass +class Sprite2D(Component, Asset): + surface: pygame.Surface + + @override + @classmethod + def load(cls, ty: type, path: str) -> "Sprite2D": + del ty + return Sprite2D(pygame.image.load(path)) + + +def poll_events(events: WindowEvents) -> None: + events.events = pygame.event.get() + + +def close_on_close(world: World, events: WindowEvents) -> None: + if pygame.QUIT in (event.type for event in events.events): + world.spawn(WorldShouldExit, WorldShouldExit()) + + +def render_sprite2d( + window: Window, + sprites_q: Query[tuple[CoordAbs2D, Sprite2D], ShouldRender], +) -> None: + sprites_raw = sorted( + sprites_q, + key=lambda e: e[0]._vec.z, + reverse=True, + ) + sprites = [ + (sprite.surface, coord._vec.xy) for coord, sprite in sprites_raw + ] + window.surface.fill((0, 0, 0)) + window.surface.blits(sprites) + window.clock.tick(60) + pygame.display.flip() + + +def graphics_plugins(world: World) -> None: + world.with_systems(StartupSchedule, init_window).with_systems( + UpdateSchedule, + Systems(render_sprite2d).in_set(PostUpdate).after(PropagateTransform), + Systems(poll_events, close_on_close).chain().in_set(PreUpdate), + ) diff --git a/src/pacman/ecs/update.py b/src/pacman/ecs/update.py new file mode 100644 index 0000000..503e7eb --- /dev/null +++ b/src/pacman/ecs/update.py @@ -0,0 +1,31 @@ +from pacman.ecs.schedule import ( + MainSchedule, + StartupSchedule, + SystemSet, + UpdateSchedule, +) +from pacman.ecs.world import Not, Resource, Systems, World + + +class WorldIsInit(Resource): + pass + + +def run_startup(world: World, _: Not[WorldIsInit]) -> None: + world.tick(StartupSchedule) + world.spawn(WorldIsInit, WorldIsInit()) + + +def run_update(world: World, _: WorldIsInit) -> None: + world.tick(UpdateSchedule) + + +PreUpdate = SystemSet("preupdate") +Update = SystemSet("update") +PostUpdate = SystemSet("postupdate") + + +def schedule_plugins(world: World) -> None: + world.with_systems(MainSchedule, run_startup, run_update).with_systems( + UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain() + ) diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index f81f768..d663e30 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -629,7 +629,7 @@ class Query[Items, Filters = tuple[()]]( itr: Iterator[Items] = self.__iter__() res = next(itr, None) if next(itr, None) is not None: - return None + raise Exception("More than one entity matches single query") return res @@ -643,7 +643,17 @@ class Resource(FromWorld, Component): query: Callable[[], Query[Self]] = cast( Callable[[], Query[Self]], _query.extract_from_world(_query, world) ) - return lambda: query().single() + default: Callable[[], Self | None] = cls.extract_default(ty, world) + return lambda: ( + res if (res := query().single()) is not None else default() + ) + + @classmethod + def extract_default( + cls, ty: type, world: "World" + ) -> Callable[[], Self | None]: + del ty, world + return lambda: None class CommandQueue(FromWorld):