]> Untitled Git - axy/ft/pacman.git/commitdiff
ecs wooo
author= <=>
Sun, 30 Aug 2026 15:28:49 +0000 (17:28 +0200)
committer= <=>
Sun, 30 Aug 2026 15:28:49 +0000 (17:28 +0200)
.gitignore
src/pacman/ecs/__init__.py [new file with mode: 0644]
src/pacman/input.py

index 734025d6772a21c30a4cdb44421a7c3e8a567450..6dde557980f1cb6e1bf9413695c3256b2bf85a02 100644 (file)
@@ -5,4 +5,6 @@ cell.txt
 .venv
 mazegeneratordemerde.txt
 a.txt
-**/__pycache__
\ No newline at end of file
+**/__pycache__
+.mypy_cache
+.ruff_cache
diff --git a/src/pacman/ecs/__init__.py b/src/pacman/ecs/__init__.py
new file mode 100644 (file)
index 0000000..5a71480
--- /dev/null
@@ -0,0 +1,246 @@
+from collections.abc import Callable, Generator, Iterable
+from dataclasses import dataclass
+from typing import Any, cast, get_args, overload
+
+import pygame.sprite
+
+
+class Entity:
+    pass
+
+
+class Schedule:
+    pass
+
+
+@dataclass
+class Resource[T]:
+    storage: type[T]
+
+
+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[Schedule, set[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
+        for entity in self._entities:
+            try:
+                yield cast(
+                    tuple[*T],
+                    tuple(
+                        entity if e is Entity else self._components[e][entity]
+                        for e in args
+                    ),
+                )
+            except KeyError:
+                continue
+
+    @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,
+    ):
+        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, val):
+        if isinstance(key, Entity):
+            if key in self:
+                del self[key]
+            self._entities[key] = set()
+            for component in val:
+                self[key][type(component)] = component
+        else:
+            self._resources[key.storage] = val
+
+    @overload
+    def __delitem__[T](self, key: Resource[T]) -> None: ...
+
+    @overload
+    def __delitem__(self, key: Entity) -> None: ...
+
+    def __delitem__(self, key):
+        if isinstance(key, Entity):
+            components = self._entities[key]
+            del self._entities[entity]
+            for component in components:
+                del self._components[component][entity]
+        else:
+            del self._resources[key.storage]
+
+    @overload
+    def __contains__[T](self, item: Resource[T]) -> bool: ...
+    @overload
+    def __contains__(self, item: Entity) -> bool: ...
+
+    def __contains__(self, item):
+        if isinstance(item, Resource):
+            return item.storage in self._resources
+        return item in self._entities
+
+    def tick(self, schedule: Schedule) -> None:
+        if schedule not in self._schedules:
+            return
+        for system in self._schedules[schedule]:
+            system(self)
+
+    def run(
+        self,
+        schedule: Schedule,
+        stop_cond: Callable[["World"], bool] = lambda _: False,
+    ) -> None:
+        while not stop_cond(self):
+            self.tick(schedule)
+
+    def with_systems(self, schedule: Schedule, *systems: System) -> "World":
+        if schedule not in self._schedules:
+            self._schedules[schedule] = set()
+        self._schedules[schedule].update(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 = Schedule()
+StartupSchedule = Schedule()
+UpdateSchedule = Schedule()
+
+
+class WorldIsInit:
+    pass
+
+
+class WorldShouldExit:
+    pass
+
+
+def run_minimal_subschedules(world: World) -> None:
+    if Resource(WorldIsInit) not in world:
+        world.tick(StartupSchedule)
+        world[Resource(WorldIsInit)] = WorldIsInit()
+    world.tick(UpdateSchedule)
+
+
+def minimal_plugins(world: World) -> None:
+    world.with_systems(MainSchedule, run_minimal_subschedules)
+
+
+@dataclass
+class Vec2:
+    x: float
+    y: float
+
+
+@dataclass
+class Pos:
+    vec: Vec2
+
+
+@dataclass
+class Model2D:
+    sprite: pygame.Surface
+
+
+class ShouldRender:
+    pass
+
+
+world = World()
+a = Entity()
+b = Entity()
+c = Entity()
+world[a] = (3, "hello")
+world[b] = (2,)
+world[c] = ("hai",)
+world[b][tuple[str]] = ("wow",)
+del world[a][str]
+for entity, component in world[tuple[Entity, int]]:
+    print(entity)
+    print(component)
+for string, integer in world[tuple[str, int]]:
+    print(string)
+    print(integer)
+for string, integer in world[tuple[tuple[str], int]]:
+    print(string)
+    print(integer)
+
+world[Resource(int)] = 15155
+world[Resource(str)] = "hello ecs"
+print(world[Resource(int)])
+del world[Resource(int)]
+print(Resource(int) in world)
+print(world[Resource(str)])
+world[Resource(int)] = 15
+print(Resource(int) in world)
+print(world[Resource(int)])
index a245c1a1c1b42224f58a6c9380805cb7db3fb35d..c7ab3daba9316e08b588535c29374afab88ee753 100644 (file)
@@ -11,10 +11,10 @@ from pacman.render import EndingRenderer, MenuRenderer
 
 class Input(ABC):
     def get_event(self, event: pygame.event.Event) -> None:
-        pass
+        return None
 
     def get_key(self, keys: pygame.key.ScancodeWrapper) -> None:
-        pass
+        return None
 
 
 class MenuInput(Input):