]> Untitled Git - axy/ft/pacman.git/commitdiff
holy crap it works
author= <=>
Tue, 22 Sep 2026 11:52:00 +0000 (13:52 +0200)
committer= <=>
Tue, 22 Sep 2026 11:52:00 +0000 (13:52 +0200)
src/pacman/ecs/world.py
src/pacman/utils/panic.py [new file with mode: 0644]
src/pacman/utils/type_resolve.py

index 7cef6eb9e71e41d38937300668e42888e11b7fea..ed0f53ba5f5237e51e2ac3027279b87df0adfbd8 100644 (file)
@@ -10,7 +10,6 @@ from typing import (
     Union,
     cast,
     get_args,
-    get_origin,
     override,
     runtime_checkable,
 )
@@ -21,6 +20,8 @@ from pacman.ecs.schedule import (
     ScheduleLabel,
     SystemSet,
 )
+from pacman.utils.panic import panic
+from pacman.utils.type_resolve import origin_or_cls, resolve_type_aliases
 from pacman.utils.variadics_please import (
     RecursiveTuple,
     fast_tuple_constructor,
@@ -32,17 +33,39 @@ class Resource2[T]:
     storage: type[T]
 
 
-type Components = RecursiveTuple["Component"]
+class ComponentLike(Protocol):
+    @abstractmethod
+    def components_iter(self) -> Iterator["Component"]: ...
+    @classmethod
+    @abstractmethod
+    def components_type_iter(cls) -> Iterator[type["Component"]]: ...
+
+
+type Components = RecursiveTuple["Component | ComponentLike"]
 
 
 def components_iter(components: Components) -> Iterator["Component"]:
-    pass
+    if isinstance(components, Component):
+        yield components
+    elif isinstance(components, tuple):
+        yield from (e for sub in components for e in components_iter(sub))
+    else:
+        yield from components.components_iter()
 
 
 def components_type_iter(
     components: type[Components],
 ) -> Iterator[type["Component"]]:
-    pass
+    if issubclass(components, Component):
+        yield components
+    elif origin_or_cls(components) is tuple:
+        yield from (
+            e
+            for sub in get_args(components)
+            for e in components_type_iter(sub)
+        )
+    else:
+        yield from cast(type[ComponentLike], components).components_type_iter()
 
 
 class Archetype:
@@ -72,7 +95,7 @@ class Archetype:
         ptr = id(ty)
         if res := self._add_cache.get(ptr):
             return (res[1], res[2])
-        comps = list(components_type_iter(ty))
+        comps = list(components_type_iter(resolve_type_aliases(ty)))
         res_arch: Archetype
         types = frozenset(comps) | self._types
         if prev := self._world._archetypes_lookup.get(types):
@@ -88,7 +111,7 @@ class Archetype:
         ptr = id(ty)
         if res := self._remove_cache.get(ptr):
             return (res[1], res[2])
-        comps = list(components_type_iter(ty))
+        comps = list(components_type_iter(resolve_type_aliases(ty)))
         res_arch: Archetype
         types = self._types - frozenset(comps)
         if prev := self._world._archetypes_lookup.get(types):
@@ -99,7 +122,7 @@ class Archetype:
         return (res_arch, comps)
 
     def add_components[T: Components](
-        self, ty: type[T], components: Iterable[tuple["Entity", T]]
+        self, ty: type[T], components: list[tuple["Entity", T]]
     ) -> None:
         dst, comps_ty = self._compute_add(ty)
         comps = [components_iter(e) for _, e in components]
@@ -143,7 +166,8 @@ class Archetype:
             self._positions[entity] = len(self._entities)
             self._entities.append(entity)
         for ty, comp in self._components.items():
-            comp.extend(cb(ty))
+            itr = cb(ty)
+            comp.extend(itr)
         assert all(
             len(comp) == len(self._entities)
             for comp in self._components.values()
@@ -181,7 +205,7 @@ class FromWorld(Protocol):
     @classmethod
     @abstractmethod
     def extract_from_world(
-        cls, world: "World"
+        cls, ty: type, world: "World"
     ) -> Callable[[], Self | None]: ...
 
     def cleanup(self) -> None:
@@ -253,14 +277,21 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
     FromWorld, Iterable[Items], Container[Entity]
 ):
     def __init__(
-        self, world: "World", fetchers: type[Items], filters: type[Filters]
+        self,
+        world: "World",
+        fetchers: type[Items],
+        filters: type[Filters] | None = None,
     ) -> None:
+        if filters is None:
+            filters = cast(type[Filters], tuple[()])
         self._world: World = world
         self._fetchers_raw = cast(
             Callable[[Archetype], Callable[[int], Items] | None],
-            Query.preprocess_fetchers(fetchers),
+            Query.preprocess_fetchers(resolve_type_aliases(fetchers)),
+        )
+        self._filters_raw = Query.preprocess_filters(
+            resolve_type_aliases(filters)
         )
-        self._filters_raw = Query.preprocess_filters(filters)
         self._seen_archetypes: int = 0
         self._archetypes_idx: dict[Archetype, int] = {}
         self._archetypes: list[Archetype] = []
@@ -298,9 +329,9 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
                 return None
             return lst.pop()
 
-        if get_origin(fetcher) is tuple:
+        if origin_or_cls(fetcher) is tuple:
             return tuple_constructor
-        if get_origin(fetcher) is Union:
+        if origin_or_cls(fetcher) is Union:
             return union_constructor
 
         return cast(QueryItem, fetcher).filter_fetch
@@ -350,9 +381,9 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
                 return lambda _: False
             return cond
 
-        if get_origin(filter) is tuple:
+        if origin_or_cls(filter) is tuple:
             return tuple_constructor
-        if get_origin(filter) is Union:
+        if origin_or_cls(filter) is Union:
             return union_constructor
 
         return cast(QueryFilter, filter).filter
@@ -383,8 +414,10 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
 
     @override
     @classmethod
-    def extract_from_world(cls, world: "World") -> Callable[[], Self]:
-        slf: Self = cls(world, *get_args(cls))
+    def extract_from_world(
+        cls, ty: type, world: "World"
+    ) -> Callable[[], Self]:
+        slf: Self = cls(world, *get_args(ty))
         return slf.updated_from_world
 
     @override
@@ -417,14 +450,37 @@ class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
 class Resource(FromWorld, Component):
     @override
     @classmethod
-    def extract_from_world(cls, world: "World") -> Callable[[], Self | None]:
-        _query = Query[cls]  # type: ignore
+    def extract_from_world(
+        cls, ty: type, world: "World"
+    ) -> Callable[[], Self | None]:
+        _query = Query[ty]  # type: ignore
         query: Callable[[], Query[Self]] = cast(
             type[Query[Self]], _query
-        ).extract_from_world(world)
+        ).extract_from_world(_query, world)
         return lambda: query().single()
 
 
+class CommandQueue(FromWorld):
+    def __init__(self, world: "World"):
+        self._world = world
+        self._queue: list[Callable[[World], Any]] = []
+
+    @override
+    @classmethod
+    def extract_from_world(
+        cls, ty: type, world: "World"
+    ) -> Callable[[], Self]:
+        return lambda: cls(world)
+
+    @override
+    def cleanup(self) -> None:
+        for sys in self._queue:
+            sys(self._world)
+
+    def queue(self, sys: Callable[["World"], Any]) -> None:
+        self._queue.append(sys)
+
+
 def _system_failed[T]() -> T:
     raise ValueError("Failed to fetch arguments for system")
 
@@ -444,6 +500,30 @@ class World(FromWorld):
     def _run_cond(self, _: RawSystem | SystemSet) -> bool:
         return True
 
+    def spawn_empty(self) -> Entity:
+        return self.spawn_many_empty(1)[0]
+
+    def spawn_many_empty(self, n: int) -> list[Entity]:
+        res = [Entity() for _ in range(n)]
+        self._base_archetype._extend(
+            lambda ty: (
+                res
+                if ty is Entity
+                else panic(Exception("Should be unreachable"))
+            )
+        )
+        return res
+
+    def spawn[T: Components](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]:
+        ents = self.spawn_many_empty(len(comps))
+        self._base_archetype.add_components(ty, list(zip(ents, comps)))
+        return ents
+
     def add_system_raw[T](
         self,
         system: Callable[..., T],
@@ -453,7 +533,7 @@ class World(FromWorld):
         if err := next(
             filter(
                 lambda kv: (
-                    not issubclass(get_origin(kv[1].annotation), FromWorld)
+                    not issubclass(origin_or_cls(kv[1].annotation), FromWorld)
                 ),
                 sig.parameters.items(),
             ),
@@ -461,7 +541,9 @@ class World(FromWorld):
         ):
             raise TypeError(f"System {system}'s {err[0]} parameter is invalid")
         extractors = tuple(
-            cast(type[FromWorld], param.annotation).extract_from_world(self)
+            cast(type[FromWorld], param.annotation).extract_from_world(
+                param.annotation, self
+            )
             for param in sig.parameters.values()
         )
 
@@ -481,7 +563,9 @@ class World(FromWorld):
 
     @classmethod
     @override
-    def extract_from_world(cls, world: "World") -> Callable[[], "World"]:
+    def extract_from_world(
+        cls, ty: type, world: "World"
+    ) -> Callable[[], "World"]:
         return lambda: world
 
     def tick(self, schedule: ScheduleLabel) -> None:
@@ -603,3 +687,23 @@ class WorldShouldExit(Resource):
 
 def should_exit(_: WorldShouldExit) -> bool:
     return True
+
+
+class Marker(Component):
+    pass
+
+
+def test_system(q: Query[Marker], queue: CommandQueue) -> None:
+    queue.queue(lambda world: world.spawn(Marker, Marker()))
+    print("ran")
+    if len(list(q)) == 10:
+        queue.queue(
+            lambda world: world.spawn(WorldShouldExit, WorldShouldExit())
+        )
+
+
+if __name__ == "__main__":
+    world = World()
+    # world.spawn(WorldShouldExit, WorldShouldExit())
+    world.with_systems(MainSchedule, world.add_system_raw(test_system))
+    world.run_main()
diff --git a/src/pacman/utils/panic.py b/src/pacman/utils/panic.py
new file mode 100644 (file)
index 0000000..19df370
--- /dev/null
@@ -0,0 +1,5 @@
+from typing import Never
+
+
+def panic(e: Exception) -> Never:
+    raise e
index 210cc94862439a7fa9e3adfac7b6f9f4636bc4bb..1367910001005e74a311725364cb16599016a124 100644 (file)
@@ -60,6 +60,13 @@ def resolve_ty_to_tup[T](
     return recursive_tuple_create(tup, inner)
 
 
+def origin_or_cls(ty: Any) -> Any:
+    res = get_origin(ty)
+    if res is None:
+        res = ty
+    return res
+
+
 type B[T] = list[list[T]]
 
 type A[T] = list[tuple[T, B[int]]]