From: = <=> Date: Tue, 22 Sep 2026 12:53:01 +0000 (+0200) Subject: Partial conversion, hierarchy missing to some extent still X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;h=8d19635061a3dee93d16ecb38664e3cca6bd14ab;p=axy%2Fft%2Fpacman.git Partial conversion, hierarchy missing to some extent still --- diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py index fb4577c..777dc12 100644 --- a/src/pacman/ecs/__init__.py +++ b/src/pacman/ecs/__init__.py @@ -16,8 +16,11 @@ from pacman.ecs.schedule import ( UpdateSchedule, ) from pacman.ecs.world import ( + Component, Entity, - Resource2, + Not, + Query, + Resource, Systems, World, WorldIsInit, @@ -25,10 +28,12 @@ from pacman.ecs.world import ( ) -def run_minimal_subschedules(world: World) -> None: - if Resource2(WorldIsInit) not in world: - world.tick(StartupSchedule) - world[Resource2(WorldIsInit)] = WorldIsInit() +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) @@ -38,28 +43,29 @@ PostUpdate = SystemSet("postupdate") def schedule_plugins(world: World) -> None: - world.with_systems(MainSchedule, run_minimal_subschedules).with_systems( + world.with_systems(MainSchedule, run_startup, run_update).with_systems( UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain() ) -class CoordAbs2D: +class CoordAbs2D(Component): _vec: Vector3 def __init__(self) -> None: self._vec = Vector3(0.0) -class Coord2D(Vector3): +class Coord2D(Vector3, Component): pass -def update_coord(world: World) -> None: - for abs_coord, coord in world.query(tuple[CoordAbs2D, Coord2D]): +def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None: + for abs_coord, coord in entities: abs_coord._vec = coord.xyz def update_coord_hierarchy(world: World) -> None: + return for parent, child in traverse_hierarchy(world, tuple[CoordAbs2D]): if parent is None: continue @@ -84,49 +90,46 @@ def minimal_plugins(world: World) -> None: ) -class ShouldRender: +class ShouldRender(Component): pass @dataclass -class Window: +class Window(Resource): surface: pygame.Surface clock: pygame.time.Clock @dataclass -class WindowEvents: +class WindowEvents(Resource): events: list[pygame.event.Event] def init_window(world: World) -> None: pygame.init() - world[Resource2(Window)] = Window( - pygame.display.set_mode(), pygame.time.Clock() - ) - world[Resource2(WindowEvents)] = WindowEvents([]) + world.spawn(Window, Window(pygame.display.set_mode(), pygame.time.Clock())) + world.spawn(WindowEvents, WindowEvents([])) @dataclass -class Sprite2D: +class Sprite2D(Component): surface: pygame.Surface -def poll_events(world: World) -> None: - world.res_s(WindowEvents).events = pygame.event.get() +def poll_events(events: WindowEvents) -> None: + events.events = pygame.event.get() -def close_on_close(world: World) -> None: - if pygame.QUIT in ( - event.type for event in world.res_s(WindowEvents).events - ): - world[Resource2(WorldShouldExit)] = WorldShouldExit() +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(world: World) -> None: - window = world.res_s(Window) +def render_sprite2d( + window: Window, sprites_q: Query[tuple[CoordAbs2D, Sprite2D, ShouldRender]] +) -> None: sprites_raw = sorted( - world[tuple[CoordAbs2D, Sprite2D, ShouldRender]], + sprites_q, key=lambda e: e[0]._vec.z, reverse=True, ) @@ -147,28 +150,28 @@ def graphics_plugins(world: World) -> None: ) -class WiggleSprite: +class WiggleSprite(Component): pass -def move_sprites(world: World) -> None: +def move_sprites(sprites: Query[Coord2D, WiggleSprite]) -> None: import math tick = pygame.time.get_ticks() / 100 - for coord, _ in world[tuple[Coord2D, WiggleSprite]]: + for coord in sprites: pos = max(tick - coord.z * 0.3, 0.0) coord.y = 500.0 + math.sin(pos) * 30.0 -class WiggleRoot: +class WiggleRoot(Component): pass -def move_root(world: World) -> None: +def move_root(root: Query[Coord2D, WiggleRoot]) -> None: import math tick = pygame.time.get_ticks() / 100 - for coord, _ in world[tuple[Coord2D, WiggleRoot]]: + for coord in root: coord.x = 30.0 + math.cos(tick) * 30.0 @@ -179,23 +182,35 @@ if __name__ == "__main__": .with_systems(UpdateSchedule, move_sprites, move_root) ) assets = list(Path("./asset").rglob("*.png")) - root = Entity() - world[root] = ( - Coord2D(100.0, 0.0, 0.0), - CoordAbs2D(), - WiggleRoot(), + root = world.spawn( + tuple[Coord2D, CoordAbs2D, WiggleRoot], + ( + Coord2D(100.0, 0.0, 0.0), + CoordAbs2D(), + WiggleRoot(), + ), ) for j in range(10): for i, asset in enumerate(assets): i += j i = i % len(assets) - world[Entity()] = ( - Coord2D(30.0 * i, 500.0 + 10.0, 1.0 * i + j), - CoordAbs2D(), - Sprite2D(pygame.image.load(asset)), - ShouldRender(), - WiggleSprite(), - Parent(root), + world.spawn( + tuple[ + Coord2D, + CoordAbs2D, + Sprite2D, + ShouldRender, + WiggleSprite, + Parent, + ], + ( + Coord2D(30.0 * i, 500.0 + 10.0, 1.0 * i + j), + CoordAbs2D(), + Sprite2D(pygame.image.load(asset)), + ShouldRender(), + WiggleSprite(), + Parent(root), + ), ) world.run_main() diff --git a/src/pacman/ecs/hierarchy.py b/src/pacman/ecs/hierarchy.py index 89bb858..8be7129 100644 --- a/src/pacman/ecs/hierarchy.py +++ b/src/pacman/ecs/hierarchy.py @@ -1,10 +1,10 @@ from collections.abc import Generator, Iterable from typing import cast, get_args -from pacman.ecs.world import Entity, World +from pacman.ecs.world import Component, Entity, World -class Children: +class Children(Component): def __init__(self, children: Iterable[Entity]) -> None: self._children = set(children) @@ -54,7 +54,7 @@ def children_remove_hook( __no_hook = False -class Parent: +class Parent(Component): def __init__(self, parent: Entity) -> None: self._parent = parent diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index ed0f53b..aa1dc2b 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -56,14 +56,14 @@ def components_iter(components: Components) -> Iterator["Component"]: def components_type_iter( components: type[Components], ) -> Iterator[type["Component"]]: - if issubclass(components, Component): - yield components - elif origin_or_cls(components) is tuple: + if origin_or_cls(components) is tuple: yield from ( e for sub in get_args(components) for e in components_type_iter(sub) ) + elif issubclass(components, Component): + yield components else: yield from cast(type[ComponentLike], components).components_type_iter() @@ -195,6 +195,8 @@ class Archetype: type RawSystem[T] = Callable[[], T] +type System[T] = Callable[..., T] + type Plugin = RecursiveTuple[Callable[["World"], None]] type ComponentHook[T] = Callable[["World", "Entity", T], None] @@ -216,8 +218,8 @@ class QueryFilter: @classmethod @abstractmethod def filter( - cls, archetype: "Archetype" - ) -> bool | Callable[[int], bool]: ... + cls, ty: type + ) -> Callable[[Archetype], bool | Callable[[int], bool]]: ... type QueryFilters = RecursiveTuple[QueryFilter] @@ -226,17 +228,21 @@ type QueryFilters = RecursiveTuple[QueryFilter] class QueryItem(QueryFilter): @override @classmethod - def filter(cls, archetype: "Archetype") -> bool | Callable[[int], bool]: - filter_fetch = cls.filter_fetch(archetype) - if filter_fetch is None: - return lambda _: False - return lambda i: filter_fetch(i) is not None + def filter( + cls, ty: type + ) -> Callable[[Archetype], bool | Callable[[int], bool]]: + preprocessed = cls.filter_fetch(ty) + return lambda archetype: ( + False + if (filter_fetch := preprocessed(archetype)) is None + else lambda i: filter_fetch(i) is not None + ) @classmethod @abstractmethod def filter_fetch( - cls, archetype: "Archetype" - ) -> None | Callable[[int], Self]: ... + cls, ty: type + ) -> Callable[[Archetype], None | Callable[[int], Self]]: ... type QueryItems = RecursiveTuple[QueryItem] @@ -245,35 +251,67 @@ type QueryItems = RecursiveTuple[QueryItem] class Component(QueryItem): @override @classmethod - def filter(cls, archetype: "Archetype") -> bool: - return cls in archetype._components + def filter(cls, ty: type) -> Callable[[Archetype], bool]: + return lambda archetype: ty in archetype._components @override @classmethod def filter_fetch( - cls, archetype: "Archetype" - ) -> None | Callable[[int], Self]: - idx = archetype._components.get(cls) - if idx is None: - return None - return cast(Callable[[int], Self], lambda i: idx[i]) + cls, ty: type + ) -> Callable[[Archetype], None | Callable[[int], Self]]: + return lambda archetype: ( + None + if (idx := archetype._components.get(ty)) is None + else cast(Callable[[int], Self], lambda i: idx[i]) + ) class Entity(QueryItem): @override @classmethod - def filter(cls, archetype: "Archetype") -> bool: - return True + def filter(cls, ty: type) -> Callable[[Archetype], bool]: + return lambda _: True @override @classmethod def filter_fetch( - cls, archetype: "Archetype" - ) -> None | Callable[[int], "Entity"]: - return lambda i: archetype._entities[i] + cls, ty: type + ) -> Callable[[Archetype], None | Callable[[int], "Entity"]]: + return lambda archetype: lambda i: archetype._entities[i] + + +class Not[T: FromWorld | Any](FromWorld, QueryFilter): + @override + @classmethod + def filter( + cls, ty: type + ) -> Callable[[Archetype], bool | Callable[[int], bool]]: + inner = cast(type[QueryFilters], get_args(ty)[0]) + preprocessed = Query.preprocess_filters(inner) + return lambda archetype: ( + not filt + if isinstance((filt := preprocessed(archetype)), bool) + else lambda i: not filt(i) + ) + + @override + @classmethod + def extract_from_world( + cls, ty: type, world: "World" + ) -> Callable[[], "Not[T] | None"]: + inner = cast(type[FromWorld], get_args(ty)[0]) + extractor = inner.extract_from_world(inner, world) + + def inner() -> Not[T] | None: + if (res := extractor()) is None: + return Not() + res.cleanup() + return None + + return inner -class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]]( +class Query[Items, Filters = tuple[()]]( FromWorld, Iterable[Items], Container[Entity] ): def __init__( @@ -334,7 +372,7 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]]( if origin_or_cls(fetcher) is Union: return union_constructor - return cast(QueryItem, fetcher).filter_fetch + return cast(QueryItem, fetcher).filter_fetch(fetcher) @staticmethod def preprocess_filters( @@ -386,7 +424,7 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]]( if origin_or_cls(filter) is Union: return union_constructor - return cast(QueryFilter, filter).filter + return cast(QueryFilter, filter).filter(filter) def add_archetype(self, archetype: Archetype) -> None: cond: Callable[[int], bool] @@ -514,14 +552,15 @@ class World(FromWorld): ) return res - def spawn[T: Components](self, ty: type[T], comp: T) -> Entity: + def spawn[T](self, ty: type[T], comp: T) -> Entity: return self.spawn_many(ty, [comp])[0] - def spawn_many[T: Components]( - self, ty: type[T], comps: list[T] - ) -> list[Entity]: + def spawn_many[T](self, ty: type[T], comps: list[T]) -> list[Entity]: ents = self.spawn_many_empty(len(comps)) - self._base_archetype.add_components(ty, list(zip(ents, comps))) + self._base_archetype.add_components( + cast(type[Components], ty), + list(zip(ents, cast(list[Components], comps))), + ) return ents def add_system_raw[T]( @@ -585,14 +624,14 @@ class World(FromWorld): def with_systems( self, schedule: ScheduleLabel, - *systems: "RawSystem | Systems", + *systems: "System | Systems", sets: set[SystemSet] | None = None, ) -> "World": if sets is None: sets = set() if schedule not in self._schedules: self._schedules[schedule] = Schedule() - Systems(*systems).in_set(*sets)._apply(self._schedules[schedule]) + Systems(*systems).in_set(*sets)._apply(self, self._schedules[schedule]) return self def with_hook[T]( @@ -635,22 +674,22 @@ _order_groups: int = 0 class Systems: - def __init__(self, *systems: "RawSystem | SystemSet | Systems") -> None: + def __init__(self, *systems: "System | SystemSet | Systems") -> None: self._systems = systems self._chain = False - self._pre: list[RawSystem | SystemSet] = [] - self._post: list[RawSystem | SystemSet] = [] + self._pre: list[System | SystemSet] = [] + self._post: list[System | SystemSet] = [] self._in_set: list[SystemSet] = [] def chain(self) -> "Systems": self._chain = True return self - def after(self, *systems: RawSystem | SystemSet) -> "Systems": + def after(self, *systems: System | SystemSet) -> "Systems": self._pre.extend(systems) return self - def before(self, *systems: RawSystem | SystemSet) -> "Systems": + def before(self, *systems: System | SystemSet) -> "Systems": self._post.extend(systems) return self @@ -658,12 +697,18 @@ class Systems: self._in_set.extend(sets) return self - def _apply(self, sched: Schedule[RawSystem]) -> SystemSet: + def _apply(self, world: World, sched: Schedule[RawSystem]) -> SystemSet: global _order_groups _order_groups += 1 label = SystemSet(f"order-group-{_order_groups}") systems = ( - e._apply(sched) if isinstance(e, Systems) else e + e._apply(world, sched) + if isinstance(e, Systems) + else ( + e + if isinstance(e, SystemSet) + else world.add_system_raw(e, default=lambda: None) + ) for e in self._systems ) sched.add_ordering((label,), self._pre, self._post, self._in_set) @@ -677,7 +722,7 @@ class Systems: return label -class WorldIsInit: +class WorldIsInit(Resource): pass @@ -695,6 +740,7 @@ class Marker(Component): def test_system(q: Query[Marker], queue: CommandQueue) -> None: queue.queue(lambda world: world.spawn(Marker, Marker())) + queue.queue(lambda world: world.spawn_empty()) print("ran") if len(list(q)) == 10: queue.queue( @@ -705,5 +751,5 @@ def test_system(q: Query[Marker], queue: CommandQueue) -> None: if __name__ == "__main__": world = World() # world.spawn(WorldShouldExit, WorldShouldExit()) - world.with_systems(MainSchedule, world.add_system_raw(test_system)) + world.with_systems(MainSchedule, test_system) world.run_main()