From: = <=> Date: Tue, 22 Sep 2026 11:52:00 +0000 (+0200) Subject: holy crap it works X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;h=a40ca6b4b16d3bb5d8861108d7b43b64498a6fb5;p=axy%2Fft%2Fpacman.git holy crap it works --- diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py index 7cef6eb..ed0f53b 100644 --- a/src/pacman/ecs/world.py +++ b/src/pacman/ecs/world.py @@ -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 index 0000000..19df370 --- /dev/null +++ b/src/pacman/utils/panic.py @@ -0,0 +1,5 @@ +from typing import Never + + +def panic(e: Exception) -> Never: + raise e diff --git a/src/pacman/utils/type_resolve.py b/src/pacman/utils/type_resolve.py index 210cc94..1367910 100644 --- a/src/pacman/utils/type_resolve.py +++ b/src/pacman/utils/type_resolve.py @@ -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]]]