]> Untitled Git - axy/ft/pacman.git/commitdiff
Refactor and untested work on hierarchy
author= <=>
Tue, 15 Sep 2026 11:43:12 +0000 (13:43 +0200)
committer= <=>
Tue, 15 Sep 2026 11:43:12 +0000 (13:43 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/hierarchy.py [new file with mode: 0644]
src/pacman/ecs/schedule.py
src/pacman/ecs/world.py [new file with mode: 0644]

index ad7e3bb849bd992eea20ad8c6e773550db798112..ebe82f343de65b106906a2131cbc7d2cec329f2e 100644 (file)
 from collections.abc import Callable, Generator, Iterable
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Any, cast, get_args, overload
+from typing import Any, Literal, cast, get_args, overload
 
 import pygame.sprite
 from pygame import Vector2
 
-from pacman.ecs.schedule import Schedule, ScheduleLabel, SystemSet
-
-
-class Entity:
-    pass
-
-
-@dataclass
-class Resource[T]:
-    storage: type[T]
-
-
-class SkipSystem(Exception):
-    pass
-
-
-type System = Callable[["World"], None]
-
-type Plugin = Callable[["World"], None] | tuple[Plugin, ...]
-
-
-@dataclass
-class EntityThunk:
-    world: "World"
-    entity: Entity
-
-    def __getitem__[T](self, ty: type[T]) -> T:
-        return cast(T, self.world._components[ty][self.entity])
-
-    def __setitem__[T](self, ty: type[T], val: T) -> None:
-        if self.entity not in self.world._entities:
-            self.world._entities[self.entity] = set()
-        if ty not in self.world._entities[self.entity]:
-            self.world._entities[self.entity].add(ty)
-        if ty not in self.world._components:
-            self.world._components[ty] = {}
-        self.world._components[ty][self.entity] = val
-
-    def __delitem__[T](self, ty: type[T]) -> None:
-        if ty not in self.world._entities[self.entity]:
-            return
-        self.world._entities[self.entity].remove(ty)
-        del self.world._components[ty][self.entity]
-
-    def __contains__[T](self, ty: type[T]) -> bool:
-        return ty in self.world._entities[self.entity]
-
-
-class World:
-    def __init__(self) -> None:
-        self._entities: dict[Entity, set[type]] = {}
-        self._components: dict[type, dict[Entity, Any]] = {}
-        self._resources: dict[type, Any] = {}
-        self._schedules: dict[ScheduleLabel, Schedule[System]] = {}
-
-    def entity(self, entity: Entity) -> EntityThunk:
-        return EntityThunk(self, entity)
-
-    def query[*T](self, ty: type[tuple[*T]]) -> Generator[tuple[*T]]:
-        args = get_args(ty)
-        if len(args) == 0:
-            return
-        sets: list[dict[Entity, Any]] = sorted(
-            (
-                self._entities if arg is Entity else self._components[arg]
-                for arg in args
-                if arg is Entity or arg in self._components
-            ),
-            key=len,
-            reverse=True,
-        )
-        if len(sets) == 0:
-            return
-        for entity in sets.pop():
-            if any(entity not in e for e in sets):
-                continue
-            yield cast(
-                tuple[*T],
-                tuple(
-                    entity if e is Entity else self._components[e][entity]
-                    for e in args
-                ),
-            )
-
-    @overload
-    def __getitem__[T](self, arg: Resource[T]) -> T: ...
-    @overload
-    def __getitem__[*T](
-        self, arg: type[tuple[*T]]
-    ) -> Generator[tuple[*T]]: ...
-    @overload
-    def __getitem__(self, arg: Entity) -> EntityThunk: ...
-    def __getitem__(
-        self,
-        arg: Any,
-    ) -> Any:
-        if isinstance(arg, Entity):
-            return self.entity(arg)
-        if isinstance(arg, Resource):
-            return self._resources[arg.storage]
-        return self.query(arg)
-
-    @overload
-    def __setitem__[T](self, key: Resource[T], val: T) -> None: ...
-    @overload
-    def __setitem__[*T](self, key: Entity, val: tuple[*T]) -> None: ...
-    def __setitem__(self, key: Any, val: Any) -> None:
-        if isinstance(key, Entity) and isinstance(val, tuple):
-            if key in self:
-                del self[key]
-            self._entities[key] = set()
-            for component in val:
-                self[key][type(component)] = component
-        elif isinstance(key, Resource) and isinstance(val, key.storage):
-            self._resources[key.storage] = val
-        else:
-            raise TypeError()
-
-    @overload
-    def __delitem__[T](self, key: Resource[T]) -> None: ...
-    @overload
-    def __delitem__(self, key: Entity) -> None: ...
-    def __delitem__[T](self, key: Entity | Resource[T]) -> None:
-        if isinstance(key, Entity):
-            components = self._entities[key]
-            del self._entities[key]
-            for component in components:
-                del self._components[component][key]
-        else:
-            del self._resources[key.storage]
-
-    @overload
-    def __contains__[T](self, item: Resource[T]) -> bool: ...
-    @overload
-    def __contains__(self, item: Entity) -> bool: ...
-
-    def __contains__[T](self, item: Entity | Resource[T]) -> bool:
-        if isinstance(item, Resource):
-            return item.storage in self._resources
-        return item in self._entities
-
-    def _run_cond(self, _: System | SystemSet) -> bool:
-        return True
-
-    def tick(self, schedule: ScheduleLabel) -> None:
-        if schedule not in self._schedules:
-            return
-        for system in self._schedules[schedule].traverse(self._run_cond):
-            try:
-                system(self)
-            except SkipSystem:
-                pass
-
-    def res_s[T](self, ty: type[T]) -> T:
-        if Resource(ty) not in self:
-            raise SkipSystem()
-        return self[Resource(ty)]
-
-    def run(
-        self,
-        schedule: ScheduleLabel,
-        stop_cond: Callable[["World"], bool] = lambda _: False,
-    ) -> None:
-        while not stop_cond(self):
-            self.tick(schedule)
-
-    def with_systems(
-        self, schedule: ScheduleLabel, *systems: System
-    ) -> "World":
-        if schedule not in self._schedules:
-            self._schedules[schedule] = Schedule()
-        self._schedules[schedule].add_ordering(systems, [], [], [])
-        return self
-
-    def with_plugins(self, *plugins: Plugin) -> "World":
-        for plugin in plugins:
-            if isinstance(plugin, tuple):
-                self.with_plugins(plugin)
-                continue
-            plugin(self)
-        return self
-
-    def run_main(self) -> None:
-        self.run(
-            MainSchedule, lambda world: Resource(WorldShouldExit) in world
-        )
-
-
-MainSchedule = ScheduleLabel("main")
-StartupSchedule = ScheduleLabel("startup")
-UpdateSchedule = ScheduleLabel("update")
-
-
-class WorldIsInit:
-    pass
-
-
-class WorldShouldExit:
-    pass
+from pacman.ecs.hierarchy import hierachy_plugins
+from pacman.ecs.schedule import (
+    MainSchedule,
+    Schedule,
+    ScheduleLabel,
+    StartupSchedule,
+    SystemSet,
+    UpdateSchedule,
+    WorldIsInit,
+    WorldShouldExit,
+)
+from pacman.ecs.world import Entity, Resource, Systems, World
 
 
 def run_minimal_subschedules(world: World) -> None:
@@ -214,10 +27,14 @@ def run_minimal_subschedules(world: World) -> None:
     world.tick(UpdateSchedule)
 
 
-def minimal_plugins(world: World) -> None:
+def schedule_plugins(world: World) -> None:
     world.with_systems(MainSchedule, run_minimal_subschedules)
 
 
+def minimal_plugins(world: World) -> None:
+    world.with_plugins(schedule_plugins, hierachy_plugins)
+
+
 @dataclass
 class ScreenCoord2D:
     vec: Vector2
@@ -279,7 +96,8 @@ def render_sprite2d(world: World) -> None:
 
 def graphics_plugins(world: World) -> None:
     world.with_systems(StartupSchedule, init_window).with_systems(
-        UpdateSchedule, poll_events, close_on_close, render_sprite2d
+        UpdateSchedule,
+        Systems(poll_events, close_on_close, render_sprite2d).chain(),
     )
 
 
diff --git a/src/pacman/ecs/hierarchy.py b/src/pacman/ecs/hierarchy.py
new file mode 100644 (file)
index 0000000..af8ca09
--- /dev/null
@@ -0,0 +1,96 @@
+from collections.abc import Generator, Iterable
+
+from pacman.ecs.world import Entity, World
+
+
+class Children:
+    def __init__(self, children: Iterable[Entity]) -> None:
+        self._children = set(children)
+
+    @property
+    def children(self) -> Generator[Entity]:
+        for child in self._children:
+            yield child
+
+
+__no_hook = False
+
+
+def get_parents(world: World, entity: Entity) -> Generator[Entity]:
+    """Generate parents of entity including self"""
+    yield entity
+    while Parent in world[entity]:
+        entity: Entity = world[entity][Parent]._parent
+        yield entity
+
+
+def children_insert_hook(
+    world: World, entity: Entity, component: Children
+) -> None:
+    global __no_hook
+    if __no_hook:
+        return
+    invalid_children = set(get_parents(world, entity))
+    if any(child in invalid_children for child in component.children):
+        raise Exception("Cyclic child hierarchy")
+    for child in map(lambda e: world[e], component.children):
+        if Parent in child:
+            del child[Parent]
+        __no_hook = True
+        child[Parent] = Parent(entity)
+        __no_hook = False
+
+        pass
+
+
+def children_remove_hook(
+    world: World, _entity: Entity, component: Children
+) -> None:
+    global __no_hook
+    if __no_hook:
+        return
+    __no_hook = True
+    for child in component.children:
+        del world[child][Parent]
+    __no_hook = False
+
+
+class Parent:
+    def __init__(self, parent: Entity) -> None:
+        self._parent = parent
+
+    @property
+    def parent(self) -> Entity:
+        return self._parent
+
+
+def parent_insert_hook(
+    world: World, entity: Entity, component: Parent
+) -> None:
+    global __no_hook
+    if __no_hook:
+        return
+    parent = world[component.parent]
+    if Parent not in parent:
+        __no_hook = True
+        parent[Children] = Children((entity,))
+        __no_hook = False
+    else:
+        parent[Children]._children.add(entity)
+
+
+def parent_remove_hook(
+    world: World, entity: Entity, component: Parent
+) -> None:
+    global __no_hook
+    if __no_hook:
+        return
+    world[component.parent][Children]._children.remove(entity)
+
+
+def hierachy_plugins(world: World) -> None:
+    world.with_hook("insert", Children, children_insert_hook).with_hook(
+        "remove", Children, children_remove_hook
+    ).with_hook("insert", Parent, parent_insert_hook).with_hook(
+        "remove", Parent, parent_remove_hook
+    )
index 4033ee044f40f63b742078dfdfeda8f54595ed11..fb397de352f9c78064c6e6922042c7a5867931db 100644 (file)
@@ -128,14 +128,14 @@ class Schedule[T]:
             yield e
 
 
-if __name__ == "__main__":
-    graph = Schedule[str]()
+MainSchedule = ScheduleLabel("main")
+StartupSchedule = ScheduleLabel("startup")
+UpdateSchedule = ScheduleLabel("update")
 
-    group_a = SystemSet("Group A")
-    group_b = SystemSet("Group B")
 
-    graph.add_ordering(["a", "b", "c"], ["z"], ["w"], [group_a])
-    graph.add_ordering(["q"], [group_a], [], [group_b])
-    graph.add_ordering([group_b], [], ["w"], [])
-    for e in graph.traverse(lambda _: True):
-        print(e)
+class WorldIsInit:
+    pass
+
+
+class WorldShouldExit:
+    pass
diff --git a/src/pacman/ecs/world.py b/src/pacman/ecs/world.py
new file mode 100644 (file)
index 0000000..6ea3d43
--- /dev/null
@@ -0,0 +1,281 @@
+from collections.abc import Callable, Generator
+from dataclasses import dataclass
+from typing import Any, Literal, cast, get_args, overload
+
+from pacman.ecs.schedule import (
+    MainSchedule,
+    Schedule,
+    ScheduleLabel,
+    SystemSet,
+    WorldShouldExit,
+)
+
+
+class Entity:
+    pass
+
+
+@dataclass
+class Resource[T]:
+    storage: type[T]
+
+
+class SkipSystem(Exception):
+    pass
+
+
+type System = Callable[["World"], None]
+
+type Plugin = Callable[["World"], None] | tuple[Plugin, ...]
+
+type ComponentHook[T] = Callable[["World", "Entity", T], None]
+
+
+@dataclass
+class EntityThunk:
+    world: "World"
+    entity: Entity
+
+    def __getitem__[T](self, ty: type[T]) -> T:
+        return cast(T, self.world._components[ty][self.entity])
+
+    def __setitem__[T](self, ty: type[T], val: T) -> None:
+        if self.entity not in self.world._entities:
+            self.world._entities[self.entity] = set()
+        if ty not in self.world._entities[self.entity]:
+            self.world._entities[self.entity].add(ty)
+        if ty not in self.world._components:
+            self.world._components[ty] = {}
+        self.world._components[ty][self.entity] = val
+
+    def __delitem__[T](self, ty: type[T]) -> None:
+        if ty not in self.world._entities[self.entity]:
+            return
+        self.world._entities[self.entity].remove(ty)
+        del self.world._components[ty][self.entity]
+
+    def __contains__[T](self, ty: type[T]) -> bool:
+        return ty in self.world._entities[self.entity]
+
+
+class World:
+    def __init__(self) -> None:
+        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._insert_hooks: dict[type, ComponentHook[Any]] = {}
+        self._remove_hooks: dict[type, ComponentHook[Any]] = {}
+
+    def entity(self, entity: Entity) -> EntityThunk:
+        return EntityThunk(self, entity)
+
+    def query[*T, *U](
+        self, ty: type[tuple[*T]], without: type[tuple[*U]] | None = None
+    ) -> Generator[tuple[*T]]:
+        args = get_args(ty)
+        neg_args = get_args(without)
+        if len(args) == 0:
+            return
+        sets: list[dict[Entity, Any]] = sorted(
+            (
+                self._entities if arg is Entity else self._components[arg]
+                for arg in args
+                if arg is Entity or arg in self._components
+            ),
+            key=len,
+            reverse=True,
+        )
+        neg_sets: list[dict[Entity, Any]] = sorted(
+            (
+                self._entities if arg is Entity else self._components[arg]
+                for arg in neg_args
+                if arg is Entity or arg in self._components
+            ),
+            key=len,
+            reverse=True,
+        )
+        if len(sets) == 0:
+            return
+        for entity in sets.pop():
+            if any(entity not in e for e in sets) or any(
+                entity in e for e in neg_sets
+            ):
+                continue
+            yield cast(
+                tuple[*T],
+                tuple(
+                    entity if e is Entity else self._components[e][entity]
+                    for e in args
+                ),
+            )
+
+    @overload
+    def __getitem__[T](self, arg: Resource[T]) -> T: ...
+    @overload
+    def __getitem__[*T](
+        self, arg: type[tuple[*T]]
+    ) -> Generator[tuple[*T]]: ...
+    @overload
+    def __getitem__(self, arg: Entity) -> EntityThunk: ...
+    def __getitem__(
+        self,
+        arg: Any,
+    ) -> Any:
+        if isinstance(arg, Entity):
+            return self.entity(arg)
+        if isinstance(arg, Resource):
+            return self._resources[arg.storage]
+        return self.query(arg)
+
+    @overload
+    def __setitem__[T](self, key: Resource[T], val: T) -> None: ...
+    @overload
+    def __setitem__[*T](self, key: Entity, val: tuple[*T]) -> None: ...
+    def __setitem__(self, key: Any, val: Any) -> None:
+        if isinstance(key, Entity) and isinstance(val, tuple):
+            if key in self:
+                del self[key]
+            self._entities[key] = set()
+            for component in val:
+                self[key][type(component)] = component
+        elif isinstance(key, Resource) and isinstance(val, key.storage):
+            self._resources[key.storage] = val
+        else:
+            raise TypeError()
+
+    @overload
+    def __delitem__[T](self, key: Resource[T]) -> None: ...
+    @overload
+    def __delitem__(self, key: Entity) -> None: ...
+    def __delitem__[T](self, key: Entity | Resource[T]) -> None:
+        if isinstance(key, Entity):
+            components = self._entities[key]
+            del self._entities[key]
+            for component in components:
+                del self._components[component][key]
+        else:
+            del self._resources[key.storage]
+
+    @overload
+    def __contains__[T](self, item: Resource[T]) -> bool: ...
+    @overload
+    def __contains__(self, item: Entity) -> bool: ...
+
+    def __contains__[T](self, item: Entity | Resource[T]) -> bool:
+        if isinstance(item, Resource):
+            return item.storage in self._resources
+        return item in self._entities
+
+    def _run_cond(self, _: System | SystemSet) -> bool:
+        return True
+
+    def tick(self, schedule: ScheduleLabel) -> None:
+        if schedule not in self._schedules:
+            return
+        for system in self._schedules[schedule].traverse(self._run_cond):
+            try:
+                system(self)
+            except SkipSystem:
+                pass
+
+    def res_s[T](self, ty: type[T]) -> T:
+        if Resource(ty) not in self:
+            raise SkipSystem()
+        return self[Resource(ty)]
+
+    def run(
+        self,
+        schedule: ScheduleLabel,
+        stop_cond: Callable[["World"], bool] = lambda _: False,
+    ) -> None:
+        while not stop_cond(self):
+            self.tick(schedule)
+
+    def with_systems(
+        self, schedule: ScheduleLabel, *systems: "System | Systems"
+    ) -> "World":
+        if schedule not in self._schedules:
+            self._schedules[schedule] = Schedule()
+        Systems(*systems)._apply(self._schedules[schedule])
+        return self
+
+    def with_hook[T](
+        self,
+        cycle: Literal["insert", "remove"],
+        ty: type[T],
+        hook: ComponentHook[T],
+    ) -> "World":
+        match cycle:
+            case "insert":
+                d = self._insert_hooks
+            case "remove":
+                d = self._insert_hooks
+        if ty not in d:
+            d[ty] = hook
+        else:
+            old = d[ty]
+            d[ty] = lambda world, entity, component: (
+                old(world, entity, component),
+                hook(world, entity, component),
+            )[1]
+        return self
+
+    def with_plugins(self, *plugins: Plugin) -> "World":
+        for plugin in plugins:
+            if isinstance(plugin, tuple):
+                self.with_plugins(plugin)
+                continue
+            plugin(self)
+        return self
+
+    def run_main(self) -> None:
+        self.run(
+            MainSchedule, lambda world: Resource(WorldShouldExit) in world
+        )
+
+
+_order_groups: int = 0
+
+
+class Systems:
+    def __init__(self, *systems: "System | SystemSet | Systems") -> None:
+        self._systems = systems
+        self._chain = False
+        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: System | SystemSet) -> "Systems":
+        self._pre.extend(systems)
+        return self
+
+    def before(self, *systems: System | SystemSet) -> "Systems":
+        self._post.extend(systems)
+        return self
+
+    def in_set(self, *sets: SystemSet) -> "Systems":
+        self._in_set.extend(sets)
+        return self
+
+    def _apply(self, sched: Schedule[System]) -> SystemSet:
+        global _order_groups
+        _order_groups += 1
+        label = SystemSet(f"order-group-{_order_groups}")
+        systems = (
+            e._apply(sched) if isinstance(e, Systems) else e
+            for e in self._systems
+        )
+        sched.add_ordering((label,), self._pre, self._post, self._in_set)
+        if self._chain:
+            prev = ()
+            for system in systems:
+                sched.add_ordering((system,), prev, (), (label,))
+                prev = (system,)
+        else:
+            sched.add_ordering(systems, (), (), (label,))
+        return label