From: = <=> Date: Tue, 15 Sep 2026 11:43:12 +0000 (+0200) Subject: Refactor and untested work on hierarchy X-Git-Url: https://git.uwuaxy.net/sitemap.xml?a=commitdiff_plain;h=1930be645e305b8c9b07132b0ae66d88affb6ff0;p=axy%2Fft%2Fpacman.git Refactor and untested work on hierarchy --- diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py index ad7e3bb..ebe82f3 100644 --- a/src/pacman/ecs/__init__.py +++ b/src/pacman/ecs/__init__.py @@ -1,210 +1,23 @@ 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 index 0000000..af8ca09 --- /dev/null +++ b/src/pacman/ecs/hierarchy.py @@ -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 + ) diff --git a/src/pacman/ecs/schedule.py b/src/pacman/ecs/schedule.py index 4033ee0..fb397de 100644 --- a/src/pacman/ecs/schedule.py +++ b/src/pacman/ecs/schedule.py @@ -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 index 0000000..6ea3d43 --- /dev/null +++ b/src/pacman/ecs/world.py @@ -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