]> Untitled Git - axy/ft/pacman.git/commitdiff
Wiggling yippee
author= <=>
Mon, 31 Aug 2026 15:20:05 +0000 (17:20 +0200)
committer= <=>
Mon, 31 Aug 2026 15:20:05 +0000 (17:20 +0200)
pyproject.toml
src/pacman/ecs/__init__.py

index f34688c5a30734d44e0b0204aeb4e369205de04b..c1a0c92a3ce2d035f3a2648e2358b221464cadf6 100644 (file)
@@ -28,7 +28,7 @@ build-backend = "uv_build"
 
 [tool.ruff]
 line-length = 79
-lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "D"]
+lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "D", "ANN", "PYI"]
 
 [tool.ruff.lint.pydocstyle]
 convention = "google"
@@ -36,10 +36,6 @@ convention = "google"
 [tool.uv.sources]
 mazegenerator = { path = "mazegenerator-2.0.2-py3-none-any.whl" }
 
-#[[tool.mypy.overrides]]
-#module = ["fire"]
-#follow_untyped_imports = true
-
 [tool.ty.rules]
 all = "error"
 possibly-missing-import = "ignore"
index 44eb0adbec0517f19f1912bcff6992a5fac90b2a..b57e604eff1273b404ae17927db418063e059d15 100644 (file)
@@ -1,5 +1,6 @@
 from collections.abc import Callable, Generator, Iterable
 from dataclasses import dataclass
+from pathlib import Path
 from typing import Any, cast, get_args, overload
 
 import pygame.sprite
@@ -69,33 +70,40 @@ class World:
         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:
+        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,
-    ):
+        arg: Any,
+    ) -> Any:
         if isinstance(arg, Entity):
             return self.entity(arg)
         if isinstance(arg, Resource):
@@ -104,32 +112,30 @@ class World:
 
     @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):
+    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
-        else:
+        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__(self, key):
+    def __delitem__[T](self, key: Entity | Resource[T]) -> None:
         if isinstance(key, Entity):
             components = self._entities[key]
-            del self._entities[entity]
+            del self._entities[key]
             for component in components:
-                del self._components[component][entity]
+                del self._components[component][key]
         else:
             del self._resources[key.storage]
 
@@ -138,7 +144,7 @@ class World:
     @overload
     def __contains__(self, item: Entity) -> bool: ...
 
-    def __contains__(self, item):
+    def __contains__[T](self, item: Entity | Resource[T]) -> bool:
         if isinstance(item, Resource):
             return item.storage in self._resources
         return item in self._entities
@@ -212,6 +218,7 @@ def minimal_plugins(world: World) -> None:
 @dataclass
 class ScreenCoord2D:
     vec: Vector2
+    depth: float
 
 
 class ShouldRender:
@@ -221,86 +228,77 @@ class ShouldRender:
 @dataclass
 class Window:
     surface: pygame.Surface
-
-
-def init_window(world: World) -> None:
-    world[Resource(Window)] = Window(pygame.display.set_mode())
+    clock: pygame.time.Clock
 
 
 @dataclass
-class Sprite2D:
-    surface: pygame.Surface
+class WindowEvents:
+    events: list[pygame.event.Event]
 
 
-def render_sprite2d(world: World) -> None:
-    window = world.res_s(Window)
-    sprites = [
-        (sprite.surface, coord.vec)
-        for coord, sprite, _ in world.query(
-            tuple[ScreenCoord2D, Sprite2D, ShouldRender]
-        )
-    ]
-    window.surface.blits(sprites)
+def init_window(world: World) -> None:
+    pygame.init()
+    world[Resource(Window)] = Window(
+        pygame.display.set_mode(), pygame.time.Clock()
+    )
+    world[Resource(WindowEvents)] = WindowEvents([])
 
 
 @dataclass
-class ShapeColor:
-    color: pygame.Color
+class Sprite2D:
+    surface: pygame.Surface
 
 
-@dataclass
-class Circle:
-    radius: float
+def poll_events(world: World) -> None:
+    world.res_s(WindowEvents).events = pygame.event.get()
 
 
-def render_circle2d(world: World) -> None:
-    window = world.res_s(Window)
-    for circle, coord, color, _ in world.query(
-        tuple[Circle, ScreenCoord2D, ShapeColor, ShouldRender]
+def close_on_close(world: World) -> None:
+    if pygame.QUIT in (
+        event.type for event in world.res_s(WindowEvents).events
     ):
-        pygame.draw.circle(
-            window.surface, color.color, coord.vec, circle.radius
-        )
-
-
-@dataclass
-class Rectangle:
-    dims: Vector2
+        world[Resource(WorldShouldExit)] = WorldShouldExit()
 
 
-def render_rectangle2d(world: World) -> None:
+def render_sprite2d(world: World) -> None:
     window = world.res_s(Window)
-    for rect, coord, color, _ in world.query(
-        tuple[Rectangle, ScreenCoord2D, ShapeColor, ShouldRender]
-    ):
-        pygame.draw.rect(window.surface, color.color, (coord.vec, rect.dims))
-
-
-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)])
+    sprites_raw = sorted(
+        world[tuple[ScreenCoord2D, Sprite2D, ShouldRender]],
+        key=lambda e: e[0].depth,
+        reverse=True,
+    )
+    sprites = [(sprite.surface, coord.vec) for coord, sprite, _ in sprites_raw]
+    window.surface.fill((0, 0, 0))
+    window.surface.blits(sprites)
+    window.clock.tick(60)
+    pygame.display.flip()
+
+
+def graphics_plugins(world: World) -> None:
+    world.with_systems(StartupSchedule, init_window).with_systems(
+        UpdateSchedule, poll_events, close_on_close, render_sprite2d
+    )
+
+
+def move_sprites(world: World) -> None:
+    import math
+
+    time = pygame.time.get_ticks() / 100
+    for (coord,) in world[tuple[ScreenCoord2D]]:
+        pos = max(time - coord.depth * 0.3, 0.0)
+        coord.vec.y = 500.0 + math.sin(pos) * 30.0
+
+
+world = (
+    World()
+    .with_plugins(minimal_plugins, graphics_plugins)
+    .with_systems(UpdateSchedule, move_sprites)
+)
+assets = list(Path("./asset").rglob("*.png"))
+for i, asset in enumerate(assets):
+    world[Entity()] = (
+        ScreenCoord2D(Vector2(30.0 * i, 500.0), 1.0 * i),
+        Sprite2D(pygame.image.load(asset)),
+        ShouldRender(),
+    )
+world.run_main()