]> Untitled Git - axy/ft/pacman.git/commitdiff
Minor refactor
author= <=>
Mon, 28 Sep 2026 11:48:19 +0000 (13:48 +0200)
committer= <=>
Mon, 28 Sep 2026 11:48:19 +0000 (13:48 +0200)
src/pacman/ecs/__init__.py
src/pacman/ecs/assets.py
src/pacman/ecs/coord.py [new file with mode: 0644]
src/pacman/ecs/graphics.py [new file with mode: 0644]
src/pacman/ecs/update.py [new file with mode: 0644]
src/pacman/ecs/world.py

index 6b3c0e2fefbe75a7c3b1c091a629292476ecde90..cce709ccef1a2588164f5a91dc0280f6f377de9d 100644 (file)
-from dataclasses import dataclass
 from pathlib import Path
-from typing import override
 
 import pygame
-from pygame import Vector3
 
-from pacman.ecs.assets import Asset, Assets
+from pacman.ecs.assets import Assets
+from pacman.ecs.coord import (
+    Coord2D,
+    CoordAbs2D,
+    coord_update_plugins,
+)
+from pacman.ecs.graphics import ShouldRender, Sprite2D, graphics_plugins
 from pacman.ecs.hierarchy import (
     ChildOf,
-    Hierarchy,
     hierachy_plugins,
 )
 from pacman.ecs.schedule import (
-    MainSchedule,
     StartupSchedule,
-    SystemSet,
     UpdateSchedule,
 )
+from pacman.ecs.update import schedule_plugins
 from pacman.ecs.world import (
     Component,
-    Not,
     Query,
-    Resource,
-    Systems,
     World,
-    WorldShouldExit,
 )
 
 
-class WorldIsInit(Resource):
-    pass
-
-
-def run_startup(world: World, _: Not[WorldIsInit]) -> None:
-    world.tick(StartupSchedule)
-    world.spawn(WorldIsInit, WorldIsInit())
-
-
-def run_update(world: World, _: WorldIsInit) -> None:
-    world.tick(UpdateSchedule)
-
-
-PreUpdate = SystemSet("preupdate")
-Update = SystemSet("update")
-PostUpdate = SystemSet("postupdate")
-
-
-def schedule_plugins(world: World) -> None:
-    world.with_systems(MainSchedule, run_startup, run_update).with_systems(
-        UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain()
-    )
-
-
-class CoordAbs2D(Component):
-    _vec: Vector3
-
-    def __init__(self) -> None:
-        self._vec = Vector3(0.0)
-
-
-class Coord2D(Vector3, Component):
-    pass
-
-
-def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None:
-    for abs_coord, coord in entities:
-        abs_coord._vec = coord.xyz
-
-
-def update_coord_hierarchy(hierarchy: Hierarchy[CoordAbs2D]) -> None:
-    for parent, child in hierarchy.top_down():
-        if parent is None:
-            continue
-        child._vec += parent._vec
-
-
-PropagateTransform = SystemSet("propagate-transform")
-
-
-def coord_update_plugins(world: World) -> None:
-    world.with_systems(
-        UpdateSchedule,
-        Systems(update_coord, update_coord_hierarchy)
-        .chain()
-        .in_set(PostUpdate, PropagateTransform),
-    )
-    world.with_systems(UpdateSchedule, update_coord)
-
-
 def minimal_plugins(world: World) -> None:
     world.with_plugins(
         schedule_plugins, hierachy_plugins, coord_update_plugins
     )
 
 
-class ShouldRender(Component):
-    pass
-
-
-@dataclass
-class Window(Resource):
-    surface: pygame.Surface
-    clock: pygame.time.Clock
-
-
-@dataclass
-class WindowEvents(Resource):
-    events: list[pygame.event.Event]
-
-
-def init_window(world: World) -> None:
-    pygame.init()
-    world.spawn(Window, Window(pygame.display.set_mode(), pygame.time.Clock()))
-    world.spawn(WindowEvents, WindowEvents([]))
-
-
-@dataclass
-class Sprite2D(Component, Asset):
-    surface: pygame.Surface
-
-    @override
-    @classmethod
-    def load(cls, ty: type, path: str) -> "Sprite2D":
-        del ty
-        return Sprite2D(pygame.image.load(path))
-
-
-def poll_events(events: WindowEvents) -> None:
-    events.events = pygame.event.get()
-
-
-def close_on_close(world: World, events: WindowEvents) -> None:
-    if pygame.QUIT in (event.type for event in events.events):
-        world.spawn(WorldShouldExit, WorldShouldExit())
-
-
-def render_sprite2d(
-    window: Window,
-    sprites_q: Query[tuple[CoordAbs2D, Sprite2D], ShouldRender],
-) -> None:
-    sprites_raw = sorted(
-        sprites_q,
-        key=lambda e: e[0]._vec.z,
-        reverse=True,
-    )
-    sprites = [
-        (sprite.surface, coord._vec.xy) 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,
-        Systems(render_sprite2d).in_set(PostUpdate).after(PropagateTransform),
-        Systems(poll_events, close_on_close).chain().in_set(PreUpdate),
-    )
-
-
 class WiggleSprite(Component):
     pass
 
@@ -222,7 +92,6 @@ def setup_world(world: World, assets: Assets[Sprite2D]) -> None:
             for i, asset_path in enumerate(asset_paths)
         ],
     )
-    pass
 
 
 if __name__ == "__main__":
index 0b8092564f63185f7042207f5a4bb0bf10edfc4b..ffb47d187f107c1e87d73308dda31d85f6667664 100644 (file)
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
 from collections.abc import Callable
 from os import PathLike
 from os.path import abspath
-from typing import Self, cast, get_args, override
+from typing import Self, get_args, override
 
 from pacman.ecs.world import Resource, World
 from pacman.utils.type_resolve import resolve_type_aliases
@@ -27,12 +27,8 @@ class Assets[T: Asset](Resource):
 
     @override
     @classmethod
-    def extract_from_world(
+    def extract_default(
         cls, ty: type, world: World
     ) -> Callable[[], Self | None]:
-        inner = cast(
-            Callable[[], Self | None], Resource.extract_from_world(ty, world)
-        )
-        return lambda: cast(
-            Self, (res if (res := inner()) is not None else Assets(ty))
-        )
+        del world
+        return lambda: cls(ty)
diff --git a/src/pacman/ecs/coord.py b/src/pacman/ecs/coord.py
new file mode 100644 (file)
index 0000000..2b8108e
--- /dev/null
@@ -0,0 +1,42 @@
+from pygame import Vector3
+
+from pacman.ecs.hierarchy import Hierarchy
+from pacman.ecs.schedule import SystemSet, UpdateSchedule
+from pacman.ecs.update import PostUpdate
+from pacman.ecs.world import Component, Query, Systems, World
+
+
+class CoordAbs2D(Component):
+    _vec: Vector3
+
+    def __init__(self) -> None:
+        self._vec = Vector3(0.0)
+
+
+class Coord2D(Vector3, Component):
+    pass
+
+
+def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None:
+    for abs_coord, coord in entities:
+        abs_coord._vec = coord.xyz
+
+
+def update_coord_hierarchy(hierarchy: Hierarchy[CoordAbs2D]) -> None:
+    for parent, child in hierarchy.top_down():
+        if parent is None:
+            continue
+        child._vec += parent._vec
+
+
+PropagateTransform = SystemSet("propagate-transform")
+
+
+def coord_update_plugins(world: World) -> None:
+    world.with_systems(
+        UpdateSchedule,
+        Systems(update_coord, update_coord_hierarchy)
+        .chain()
+        .in_set(PostUpdate, PropagateTransform),
+    )
+    world.with_systems(UpdateSchedule, update_coord)
diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py
new file mode 100644 (file)
index 0000000..15e911d
--- /dev/null
@@ -0,0 +1,84 @@
+from dataclasses import dataclass
+from typing import override
+
+import pygame
+
+from pacman.ecs.assets import Asset
+from pacman.ecs.coord import CoordAbs2D, PropagateTransform
+from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
+from pacman.ecs.update import PostUpdate, PreUpdate
+from pacman.ecs.world import (
+    Component,
+    Query,
+    Resource,
+    Systems,
+    World,
+    WorldShouldExit,
+)
+
+
+class ShouldRender(Component):
+    pass
+
+
+@dataclass
+class Window(Resource):
+    surface: pygame.Surface
+    clock: pygame.time.Clock
+
+
+@dataclass
+class WindowEvents(Resource):
+    events: list[pygame.event.Event]
+
+
+def init_window(world: World) -> None:
+    pygame.init()
+    world.spawn(Window, Window(pygame.display.set_mode(), pygame.time.Clock()))
+    world.spawn(WindowEvents, WindowEvents([]))
+
+
+@dataclass
+class Sprite2D(Component, Asset):
+    surface: pygame.Surface
+
+    @override
+    @classmethod
+    def load(cls, ty: type, path: str) -> "Sprite2D":
+        del ty
+        return Sprite2D(pygame.image.load(path))
+
+
+def poll_events(events: WindowEvents) -> None:
+    events.events = pygame.event.get()
+
+
+def close_on_close(world: World, events: WindowEvents) -> None:
+    if pygame.QUIT in (event.type for event in events.events):
+        world.spawn(WorldShouldExit, WorldShouldExit())
+
+
+def render_sprite2d(
+    window: Window,
+    sprites_q: Query[tuple[CoordAbs2D, Sprite2D], ShouldRender],
+) -> None:
+    sprites_raw = sorted(
+        sprites_q,
+        key=lambda e: e[0]._vec.z,
+        reverse=True,
+    )
+    sprites = [
+        (sprite.surface, coord._vec.xy) 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,
+        Systems(render_sprite2d).in_set(PostUpdate).after(PropagateTransform),
+        Systems(poll_events, close_on_close).chain().in_set(PreUpdate),
+    )
diff --git a/src/pacman/ecs/update.py b/src/pacman/ecs/update.py
new file mode 100644 (file)
index 0000000..503e7eb
--- /dev/null
@@ -0,0 +1,31 @@
+from pacman.ecs.schedule import (
+    MainSchedule,
+    StartupSchedule,
+    SystemSet,
+    UpdateSchedule,
+)
+from pacman.ecs.world import Not, Resource, Systems, World
+
+
+class WorldIsInit(Resource):
+    pass
+
+
+def run_startup(world: World, _: Not[WorldIsInit]) -> None:
+    world.tick(StartupSchedule)
+    world.spawn(WorldIsInit, WorldIsInit())
+
+
+def run_update(world: World, _: WorldIsInit) -> None:
+    world.tick(UpdateSchedule)
+
+
+PreUpdate = SystemSet("preupdate")
+Update = SystemSet("update")
+PostUpdate = SystemSet("postupdate")
+
+
+def schedule_plugins(world: World) -> None:
+    world.with_systems(MainSchedule, run_startup, run_update).with_systems(
+        UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain()
+    )
index f81f7680a45aa3e2642b237b1f0e5eefc4bdbea4..d663e308522255a8939b922a42c77a193a02ea00 100644 (file)
@@ -629,7 +629,7 @@ class Query[Items, Filters = tuple[()]](
         itr: Iterator[Items] = self.__iter__()
         res = next(itr, None)
         if next(itr, None) is not None:
-            return None
+            raise Exception("More than one entity matches single query")
         return res
 
 
@@ -643,7 +643,17 @@ class Resource(FromWorld, Component):
         query: Callable[[], Query[Self]] = cast(
             Callable[[], Query[Self]], _query.extract_from_world(_query, world)
         )
-        return lambda: query().single()
+        default: Callable[[], Self | None] = cls.extract_default(ty, world)
+        return lambda: (
+            res if (res := query().single()) is not None else default()
+        )
+
+    @classmethod
+    def extract_default(
+        cls, ty: type, world: "World"
+    ) -> Callable[[], Self | None]:
+        del ty, world
+        return lambda: None
 
 
 class CommandQueue(FromWorld):