]> Untitled Git - axy/ft/pacman.git/commitdiff
Start of work for moving to type-based systems
authorAxy <gilliardmarthey.axel@gmail.com>
Sat, 19 Sep 2026 19:42:37 +0000 (21:42 +0200)
committerAxy <gilliardmarthey.axel@gmail.com>
Sat, 19 Sep 2026 19:42:37 +0000 (21:42 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/collide.py [new file with mode: 0644]
src/pacman/ecs/world.py
src/pacman/utils/__init__.py [new file with mode: 0644]
src/pacman/utils/type_resolve.py [new file with mode: 0644]
src/pacman/utils/variadics_please.py [new file with mode: 0644]

index 1bee6e4e3e86f487673a8d64421e0308e4a26bd8..fb4577c9f768876c39e1c241e316e7c887062367 100644 (file)
@@ -2,7 +2,7 @@ from dataclasses import dataclass
 from pathlib import Path
 
 import pygame
-from pygame import Vector2, Vector3
+from pygame import Vector3
 
 from pacman.ecs.hierarchy import (
     Parent,
@@ -17,7 +17,7 @@ from pacman.ecs.schedule import (
 )
 from pacman.ecs.world import (
     Entity,
-    Resource,
+    Resource2,
     Systems,
     World,
     WorldIsInit,
@@ -26,9 +26,9 @@ from pacman.ecs.world import (
 
 
 def run_minimal_subschedules(world: World) -> None:
-    if Resource(WorldIsInit) not in world:
+    if Resource2(WorldIsInit) not in world:
         world.tick(StartupSchedule)
-        world[Resource(WorldIsInit)] = WorldIsInit()
+        world[Resource2(WorldIsInit)] = WorldIsInit()
     world.tick(UpdateSchedule)
 
 
@@ -44,12 +44,10 @@ def schedule_plugins(world: World) -> None:
 
 
 class CoordAbs2D:
-    vec: Vector2
-    depth: float
+    _vec: Vector3
 
     def __init__(self) -> None:
-        self.vec = Vector2(0.0)
-        self.depth = 0.0
+        self._vec = Vector3(0.0)
 
 
 class Coord2D(Vector3):
@@ -58,15 +56,14 @@ class Coord2D(Vector3):
 
 def update_coord(world: World) -> None:
     for abs_coord, coord in world.query(tuple[CoordAbs2D, Coord2D]):
-        abs_coord.vec = coord.xy
-        abs_coord.depth = coord.z
+        abs_coord._vec = coord.xyz
 
 
 def update_coord_hierarchy(world: World) -> None:
     for parent, child in traverse_hierarchy(world, tuple[CoordAbs2D]):
         if parent is None:
             continue
-        child[0].vec += parent[0].vec
+        child[0]._vec += parent[0]._vec
 
 
 PropagateTransform = SystemSet("propagate-transform")
@@ -104,10 +101,10 @@ class WindowEvents:
 
 def init_window(world: World) -> None:
     pygame.init()
-    world[Resource(Window)] = Window(
+    world[Resource2(Window)] = Window(
         pygame.display.set_mode(), pygame.time.Clock()
     )
-    world[Resource(WindowEvents)] = WindowEvents([])
+    world[Resource2(WindowEvents)] = WindowEvents([])
 
 
 @dataclass
@@ -123,17 +120,19 @@ def close_on_close(world: World) -> None:
     if pygame.QUIT in (
         event.type for event in world.res_s(WindowEvents).events
     ):
-        world[Resource(WorldShouldExit)] = WorldShouldExit()
+        world[Resource2(WorldShouldExit)] = WorldShouldExit()
 
 
 def render_sprite2d(world: World) -> None:
     window = world.res_s(Window)
     sprites_raw = sorted(
         world[tuple[CoordAbs2D, Sprite2D, ShouldRender]],
-        key=lambda e: e[0].depth,
+        key=lambda e: e[0]._vec.z,
         reverse=True,
     )
-    sprites = [(sprite.surface, coord.vec) for coord, sprite, _ in sprites_raw]
+    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)
diff --git a/src/pacman/ecs/collide.py b/src/pacman/ecs/collide.py
new file mode 100644 (file)
index 0000000..e69de29
index bdbf8c2adf6175ac96af5f55aacd3a4af8622ca4..b92df32967fdd43368165983bbda0e36e9b193bc 100644 (file)
@@ -1,7 +1,17 @@
 import contextlib
-from collections.abc import Callable, Generator
+from abc import abstractmethod
+from collections.abc import Callable, Generator, Iterable, Iterator
 from dataclasses import dataclass
-from typing import Any, Literal, cast, get_args, overload
+from typing import (
+    Any,
+    Literal,
+    Protocol,
+    Self,
+    cast,
+    get_args,
+    overload,
+    override,
+)
 
 from pacman.ecs.schedule import (
     MainSchedule,
@@ -9,6 +19,7 @@ from pacman.ecs.schedule import (
     ScheduleLabel,
     SystemSet,
 )
+from pacman.utils.variadics_please import VarargCallable, VarargTuple
 
 
 class Entity:
@@ -16,7 +27,7 @@ class Entity:
 
 
 @dataclass
-class Resource[T]:
+class Resource2[T]:
     storage: type[T]
 
 
@@ -24,13 +35,65 @@ class SkipSystemError(Exception):
     pass
 
 
-type System = Callable[["World"], None]
+type RawSystem = Callable[["World"], None]
 
 type Plugin = Callable[["World"], None] | tuple[Plugin, ...]
 
 type ComponentHook[T] = Callable[["World", "Entity", T], None]
 
 
+class FromWorld(Protocol):
+    @classmethod
+    @abstractmethod
+    def extract_from_world(cls, world: "World") -> Self | None: ...
+
+
+type SystemParam = VarargTuple[SystemParam] | World | FromWorld
+
+type System = VarargCallable[SystemParam, None]
+
+
+class QueryItem:
+    pass
+
+
+type QueryItems = VarargTuple[QueryItems] | QueryItem
+
+
+class QueryFilter:
+    pass
+
+
+type QueryFilters = VarargTuple[QueryFilters] | QueryFilter
+
+
+class Query[Items: QueryItems, Filters: QueryFilters = tuple[()]](
+    FromWorld, Iterable[Items]
+):
+    def __init__(
+        self, world: "World", items: type[Items], filters: type[Filters]
+    ) -> None:
+        pass
+
+    @override
+    @classmethod
+    def extract_from_world(cls, world: "World") -> Self | None:
+        return cls(world, *get_args(cls))
+
+    @override
+    def __iter__(self) -> Iterator[Items]:
+        if False:
+            yield
+
+
+class Resource(FromWorld):
+    @override
+    @classmethod
+    def extract_from_world(cls, world: "World") -> Self | None:
+
+        pass
+
+
 @dataclass
 class EntityThunk:
     world: "World"
@@ -73,7 +136,7 @@ class World:
         self._entities: dict[Entity, set[type]] = {}
         self._components: dict[type, dict[Entity, Any]] = {}
         self._resources: dict[type, Any] = {}
-        self._schedules: dict[ScheduleLabel, Schedule[System]] = {}
+        self._schedules: dict[ScheduleLabel, Schedule[RawSystem]] = {}
         self._insert_hooks: dict[type, ComponentHook[Any]] = {}
         self._remove_hooks: dict[type, ComponentHook[Any]] = {}
 
@@ -125,7 +188,7 @@ class World:
             )
 
     @overload
-    def __getitem__[T](self, arg: Resource[T]) -> T: ...
+    def __getitem__[T](self, arg: Resource2[T]) -> T: ...
 
     @overload
     def __getitem__[*T](
@@ -141,12 +204,12 @@ class World:
     ) -> Any:
         if isinstance(arg, Entity):
             return self.entity(arg)
-        if isinstance(arg, Resource):
+        if isinstance(arg, Resource2):
             return self._resources[arg.storage]
         return self.query(arg)
 
     @overload
-    def __setitem__[T](self, key: Resource[T], val: T) -> None: ...
+    def __setitem__[T](self, key: Resource2[T], val: T) -> None: ...
 
     @overload
     def __setitem__[*T](self, key: Entity, val: tuple[*T]) -> None: ...
@@ -158,18 +221,18 @@ class World:
             self._entities[key] = set()
             for component in val:
                 self[key][type(component)] = component
-        elif isinstance(key, Resource) and isinstance(val, key.storage):
+        elif isinstance(key, Resource2) and isinstance(val, key.storage):
             self._resources[key.storage] = val
         else:
             raise TypeError()
 
     @overload
-    def __delitem__[T](self, key: Resource[T]) -> None: ...
+    def __delitem__[T](self, key: Resource2[T]) -> None: ...
 
     @overload
     def __delitem__(self, key: Entity) -> None: ...
 
-    def __delitem__[T](self, key: Entity | Resource[T]) -> None:
+    def __delitem__[T](self, key: Entity | Resource2[T]) -> None:
         if isinstance(key, Entity):
             components = self._entities[key]
             del self._entities[key]
@@ -179,16 +242,16 @@ class World:
             del self._resources[key.storage]
 
     @overload
-    def __contains__[T](self, item: Resource[T]) -> bool: ...
+    def __contains__[T](self, item: Resource2[T]) -> bool: ...
     @overload
     def __contains__(self, item: Entity) -> bool: ...
 
-    def __contains__[T](self, item: Entity | Resource[T]) -> bool:
-        if isinstance(item, Resource):
+    def __contains__[T](self, item: Entity | Resource2[T]) -> bool:
+        if isinstance(item, Resource2):
             return item.storage in self._resources
         return item in self._entities
 
-    def _run_cond(self, _: System | SystemSet) -> bool:
+    def _run_cond(self, _: RawSystem | SystemSet) -> bool:
         return True
 
     def tick(self, schedule: ScheduleLabel) -> None:
@@ -199,9 +262,9 @@ class World:
                 system(self)
 
     def res_s[T](self, ty: type[T]) -> T:
-        if Resource(ty) not in self:
+        if Resource2(ty) not in self:
             raise SkipSystemError()
-        return self[Resource(ty)]
+        return self[Resource2(ty)]
 
     def run(
         self,
@@ -214,7 +277,7 @@ class World:
     def with_systems(
         self,
         schedule: ScheduleLabel,
-        *systems: "System | Systems",
+        *systems: "RawSystem | Systems",
         sets: set[SystemSet] | None = None,
     ) -> "World":
         if sets is None:
@@ -258,7 +321,7 @@ class World:
 
     def run_main(self) -> None:
         self.run(
-            MainSchedule, lambda world: Resource(WorldShouldExit) in world
+            MainSchedule, lambda world: Resource2(WorldShouldExit) in world
         )
 
 
@@ -266,22 +329,22 @@ _order_groups: int = 0
 
 
 class Systems:
-    def __init__(self, *systems: "System | SystemSet | Systems") -> None:
+    def __init__(self, *systems: "RawSystem | SystemSet | Systems") -> None:
         self._systems = systems
         self._chain = False
-        self._pre: list[System | SystemSet] = []
-        self._post: list[System | SystemSet] = []
+        self._pre: list[RawSystem | SystemSet] = []
+        self._post: list[RawSystem | SystemSet] = []
         self._in_set: list[SystemSet] = []
 
     def chain(self) -> "Systems":
         self._chain = True
         return self
 
-    def after(self, *systems: System | SystemSet) -> "Systems":
+    def after(self, *systems: RawSystem | SystemSet) -> "Systems":
         self._pre.extend(systems)
         return self
 
-    def before(self, *systems: System | SystemSet) -> "Systems":
+    def before(self, *systems: RawSystem | SystemSet) -> "Systems":
         self._post.extend(systems)
         return self
 
@@ -289,7 +352,7 @@ class Systems:
         self._in_set.extend(sets)
         return self
 
-    def _apply(self, sched: Schedule[System]) -> SystemSet:
+    def _apply(self, sched: Schedule[RawSystem]) -> SystemSet:
         global _order_groups
         _order_groups += 1
         label = SystemSet(f"order-group-{_order_groups}")
@@ -299,7 +362,7 @@ class Systems:
         )
         sched.add_ordering((label,), self._pre, self._post, self._in_set)
         if self._chain:
-            prev: tuple[SystemSet | System] | tuple[()] = ()
+            prev: tuple[SystemSet | RawSystem] | tuple[()] = ()
             for system in systems:
                 sched.add_ordering((system,), prev, (), (label,))
                 prev = (system,)
diff --git a/src/pacman/utils/__init__.py b/src/pacman/utils/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pacman/utils/type_resolve.py b/src/pacman/utils/type_resolve.py
new file mode 100644 (file)
index 0000000..d555c13
--- /dev/null
@@ -0,0 +1,52 @@
+# ruff: noqa: ANN401
+
+from collections.abc import Iterable
+from typing import (
+    Any,
+    get_args,
+    get_origin,
+)
+
+
+def map_type_generics(ty: Any, mapping: dict[Any, type]) -> Any:
+    """Map generics for this type, maybe creating a new type."""
+    if ty in mapping:
+        return mapping[ty]
+    if (orig := get_origin(ty)) is None:
+        return ty
+    args = tuple(map_type_generics(e, mapping) for e in get_args(ty))
+    if hasattr(ty, "copy_with") and callable(ty.copy_with):
+        return ty.copy_with(args)
+    return orig[args]
+
+
+def resolve_type_aliases(ty: Any) -> Any:
+    """Resolve any type aliases for a given type, recursively."""
+    while hasattr(ty, "__value__"):
+        if not (
+            hasattr(ty, "__type_params__")
+            and hasattr(ty, "__args__")
+            and isinstance(ty.__args__, Iterable)
+        ):
+            return ty.__value__
+        mapping = dict(zip(ty.__type_params__, ty.__args__, strict=True))
+        ty = map_type_generics(ty.__value__, mapping)
+    return map_type_generics(
+        ty, {e: resolve_type_aliases(e) for e in get_args(ty)}
+    )
+
+
+type B[T] = list[list[T]]
+
+type A[T] = list[tuple[T, B[int]]]
+
+if __name__ == "__main__":
+    alp = A[str]
+    print(alp)
+    al = resolve_type_aliases(alp)
+    print(al)
+    al2 = list[tuple[str, list[list[int]]]]
+    print(al2)
+    al3 = resolve_type_aliases(al2)
+    print(al3)
+    print(al == al2)
diff --git a/src/pacman/utils/variadics_please.py b/src/pacman/utils/variadics_please.py
new file mode 100644 (file)
index 0000000..bd773fa
--- /dev/null
@@ -0,0 +1,37 @@
+from collections.abc import Callable
+
+type VarargCallable[T, U] = (
+    Callable[[T], U]
+    | Callable[[T, T], U]
+    | Callable[[T, T, T], U]
+    | Callable[[T, T, T, T], U]
+    | Callable[[T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T, T, T, T, T], U]
+    | Callable[[T, T, T, T, T, T, T, T, T, T, T, T, T, T, T], U]
+)
+type VarargTuple[T] = (
+    tuple[()]
+    | tuple[T]
+    | tuple[T, T]
+    | tuple[T, T, T]
+    | tuple[T, T, T, T]
+    | tuple[T, T, T, T, T]
+    | tuple[T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T, T, T, T, T]
+    | tuple[T, T, T, T, T, T, T, T, T, T, T, T, T, T, T]
+)