From 477b4bf337ec5947b650fc24b8cfea71a1906445 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sun, 30 Aug 2026 17:28:49 +0200 Subject: [PATCH] ecs wooo --- .gitignore | 4 +- src/pacman/ecs/__init__.py | 246 +++++++++++++++++++++++++++++++++++++ src/pacman/input.py | 4 +- 3 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 src/pacman/ecs/__init__.py diff --git a/.gitignore b/.gitignore index 734025d..6dde557 100644 --- a/.gitignore +++ b/.gitignore @@ -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 index 0000000..5a71480 --- /dev/null +++ b/src/pacman/ecs/__init__.py @@ -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)]) diff --git a/src/pacman/input.py b/src/pacman/input.py index a245c1a..c7ab3da 100644 --- a/src/pacman/input.py +++ b/src/pacman/input.py @@ -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): -- 2.53.0