]> Untitled Git - axy/ft/pacman.git/commitdiff
Partial conversion, hierarchy missing to some extent still
author= <=>
Tue, 22 Sep 2026 12:53:01 +0000 (14:53 +0200)
committer= <=>
Tue, 22 Sep 2026 12:53:01 +0000 (14:53 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/hierarchy.py
src/pacman/ecs/world.py

index fb4577c9f768876c39e1c241e316e7c887062367..777dc1298fad5bfe0ac37f6f1416f6d6e73a5bcb 100644 (file)
@@ -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()
index 89bb8583ef5aaaf7824dfeb98105a92ab33963e4..8be7129ef32a591ba4e63a92ce8aa8419e2510a5 100644 (file)
@@ -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
 
index ed0f53ba5f5237e51e2ac3027279b87df0adfbd8..aa1dc2bffad521f93d3960b51c6b9dee13f969c1 100644 (file)
@@ -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[ItemsFilters = 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()