]> Untitled Git - axy/ft/pacman.git/commitdiff
Docstrings on the core engine
authorAxy <gilliardmarthey.axel@gmail.com>
Mon, 19 Oct 2026 23:11:03 +0000 (01:11 +0200)
committer= <=>
Tue, 20 Oct 2026 14:35:31 +0000 (16:35 +0200)
17 files changed:
pyproject.toml
src/pacman/__init__.py
src/pacman/ecs/__init__.py
src/pacman/ecs/assets.py
src/pacman/ecs/collide.py [deleted file]
src/pacman/ecs/coord.py
src/pacman/ecs/graphics.py
src/pacman/ecs/gui.py
src/pacman/ecs/hierarchy.py
src/pacman/ecs/schedule.py
src/pacman/ecs/update.py
src/pacman/ecs/world.py
src/pacman/game/__init__.py
src/pacman/utils/__init__.py
src/pacman/utils/panic.py
src/pacman/utils/type_resolve.py
src/pacman/utils/variadics_please.py

index 1375c27be3966058f5fd044c642d2c23d6b506ae..d357ecb458530f1270a9f442aea6ccea5a1d2852 100644 (file)
@@ -28,8 +28,7 @@ build-backend = "uv_build"
 
 [tool.ruff]
 line-length = 79
-#lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "D", "ANN", "PYI"]
-lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "ANN", "PYI"]
+lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "ANN", "PYI", "D"]
 
 [tool.ruff.lint.pydocstyle]
 convention = "google"
index 6c5013baeb81bc71705398863ab894015dc92308..a9f9fddcda03d13ed79e4256c51b87f7a053a86f 100644 (file)
@@ -2,6 +2,8 @@ from pacman.game import run_game
 
 
 def main() -> None:
+    run_game()
+    return None
     try:
         run_game()
     except Exception as e:
index 56fc740dbe630481d0f4e98cc5b1916bbc0c224b..3d66d95dda8aefbb5a178a655a2a8e73bb01ffd2 100644 (file)
-from pathlib import Path
+"""The core ECS engine."""
 
-import pygame
-
-from pacman.ecs.assets import Assets
-from pacman.ecs.coord import (
-    Coord2D,
-    CoordAbs2D,
-    coord_update_plugins,
-)
-from pacman.ecs.graphics import (
-    ShouldRender,
-    Sprite2D,
-    WindowEvents,
-    graphics_plugins,
-)
-from pacman.ecs.hierarchy import (
-    ChildOf,
-    hierachy_plugins,
-)
-from pacman.ecs.schedule import (
-    StartupSchedule,
-    UpdateSchedule,
-)
+from pacman.ecs.coord import coord_update_plugins
+from pacman.ecs.hierarchy import hierachy_plugins
 from pacman.ecs.update import schedule_plugins
-from pacman.ecs.world import (
-    Component,
-    Query,
-    World,
-)
+from pacman.ecs.world import World
 
 
 def minimal_plugins(world: World) -> None:
+    """Setup the minimum plugins for general game functionality."""
     world.with_plugins(
         schedule_plugins, hierachy_plugins, coord_update_plugins
     )
-
-
-class WiggleSprite(Component):
-    pass
-
-
-def move_sprites(sprites: Query[Coord2D, WiggleSprite]) -> None:
-    import math
-
-    tick = pygame.time.get_ticks() / 100
-    for coord in sprites:
-        pos = max(tick - coord.z * 0.3, 0.0)
-        coord.y = 500.0 + math.sin(pos) * 30.0
-
-
-class WiggleRoot(Component):
-    pass
-
-
-def move_root(root: Query[Coord2D, WiggleRoot]) -> None:
-    import math
-
-    tick = pygame.time.get_ticks() / 100
-    for coord in root:
-        coord.x = 30.0 + math.cos(tick) * 30.0
-
-
-def print_clicks(events: WindowEvents) -> None:
-    for pos, click in events.clicks():
-        print(pos, click)
-
-
-def setup_world(world: World, assets: Assets[Sprite2D]) -> None:
-    asset_paths = list(Path("./asset").rglob("*.png"))
-    root = world.spawn(
-        tuple[Coord2D, CoordAbs2D, WiggleRoot],
-        (
-            Coord2D(100.0, 0.0, 0.0),
-            CoordAbs2D(),
-            WiggleRoot(),
-        ),
-    )
-    world.spawn_many(
-        tuple[
-            Coord2D,
-            CoordAbs2D,
-            Sprite2D,
-            ShouldRender,
-            WiggleSprite,
-            ChildOf,
-        ],
-        [
-            (
-                Coord2D(
-                    30.0 * (n := (i + j) % len(asset_paths)),
-                    500.0 + 10.0,
-                    1.0 * n + j,
-                ),
-                CoordAbs2D(),
-                assets.load(asset_path.as_posix()),
-                ShouldRender(),
-                WiggleSprite(),
-                ChildOf(root),
-            )
-            for j in range(30)
-            for i, asset_path in enumerate(asset_paths)
-        ],
-    )
-
-
-if __name__ == "__main__":
-    world = (
-        World()
-        .with_plugins(minimal_plugins, graphics_plugins)
-        .with_systems(StartupSchedule, setup_world)
-        .with_systems(UpdateSchedule, move_sprites, move_root, print_clicks)
-    )
-    world.run_main()
index ffb47d187f107c1e87d73308dda31d85f6667664..ddc3105b3f762a6911b44191f501e727e6a38ee8 100644 (file)
@@ -1,3 +1,5 @@
+"""Asset loading in ECS."""
+
 from abc import ABC, abstractmethod
 from collections.abc import Callable
 from os import PathLike
@@ -9,17 +11,31 @@ from pacman.utils.type_resolve import resolve_type_aliases
 
 
 class Asset(ABC):
+    """A data type which may be loaded from a specific identifier."""
+
     @classmethod
     @abstractmethod
-    def load(cls, ty: type, path: str) -> Self: ...
+    def load(cls, ty: type, path: str) -> Self:
+        """Load the type from the specified identifier.
+
+        Args:
+          ty:
+            The type, including generics, of cls.
+          path:
+            The identifier of the asset, usually a file path.
+        """
 
 
 class Assets[T: Asset](Resource):
+    """A collection of cached assets to be loaded."""
+
     def __init__(self, ty: type) -> None:
+        """Create a new empty asset collection."""
         self._lookup: dict[str, T] = {}
         self._asset_ty: type[T] = get_args(resolve_type_aliases(ty))[0]
 
     def load(self, s: str | PathLike[str]) -> T:
+        """Load the given asset or fetch it from cache."""
         s = abspath(s)
         if s not in self._lookup:
             self._lookup[s] = self._asset_ty.load(self._asset_ty, s)
diff --git a/src/pacman/ecs/collide.py b/src/pacman/ecs/collide.py
deleted file mode 100644 (file)
index e69de29..0000000
index dc8e6a3d00e163c97ddab49ac254c66fa084cbab..17e0bf10577f00802c84a878ca074bee495bf7bc 100644 (file)
@@ -1,3 +1,5 @@
+"""ECS coordinate logic."""
+
 from collections.abc import Iterator
 from typing import override
 
@@ -10,18 +12,24 @@ from pacman.ecs.world import Bundle, Component, Query, Systems, World
 
 
 class CoordAbs2D(Component):
+    """Coord absolute in the window space."""
+
     _vec: Vector3
 
     def __init__(self) -> None:
+        """Create a new CoordAbs2D."""
         self._vec = Vector3(0.0)
 
 
 class Coord2D(Vector3, Component):
-    pass
+    """Coord relative to the parent entity."""
 
 
 class CoordBundle(Bundle):
+    """A bundle of coords that allows for easily setting up coordinates."""
+
     def __init__(self, x: float = 0, y: float = 0, depth: float = -1) -> None:
+        """Create a new coord bundle."""
         self.coord = Coord2D(x, y, depth)
         self.coord_abs = CoordAbs2D()
 
@@ -37,11 +45,13 @@ class CoordBundle(Bundle):
 
 
 def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None:
+    """Update absolute coords to match relative coords."""
     for abs_coord, coord in entities:
         abs_coord._vec = coord.xyz
 
 
 def update_coord_hierarchy(hierarchy: Hierarchy[CoordAbs2D]) -> None:
+    """Update absolute coords to be offset by parent's absolute coords."""
     for parent, child in hierarchy.top_down():
         if parent is None:
             continue
@@ -52,6 +62,7 @@ PropagateTransform = SystemSet("propagate-transform")
 
 
 def coord_update_plugins(world: World) -> None:
+    """Coordinate related logic."""
     world.with_systems(
         UpdateSchedule,
         Systems(update_coord, update_coord_hierarchy)
index a8e5396726666cfe813cd2ea80bccfb522298f9a..41fa898071ffd8d5efef8430f743c24839737643 100644 (file)
@@ -1,3 +1,5 @@
+"""Simple 2D graphics and input in ECS."""
+
 from collections.abc import Iterator
 from dataclasses import dataclass
 from time import time
@@ -27,21 +29,25 @@ from pacman.ecs.world import (
 
 
 class ShouldRender(Component):
-    pass
+    """A marker component to avoid rendering a given entity."""
 
 
 class StopAnimation(Component):
-    pass
+    """A marker component to avoid animating a given entity."""
 
 
 @dataclass
 class Window(Resource):
+    """A window with clock to keep refresh rate."""
+
     surface: pygame.Surface
     clock: pygame.time.Clock
 
 
 @dataclass
 class WindowPositionned(Component):
+    """A component to position an entity relative to the window."""
+
     pos: Vector2
     offset: Vector2
 
@@ -51,9 +57,12 @@ dir_lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3}
 
 @dataclass
 class WindowEvents(Resource):
+    """A simple current tick list of window events."""
+
     events: list[pygame.event.Event]
 
     def clicks(self) -> Iterator[tuple[int, pygame.Vector2]]:
+        """Iterate over each click event on screen."""
         return (
             cast(tuple[int, pygame.Vector2], (e.pos, e.button))
             for e in self.events
@@ -61,6 +70,7 @@ class WindowEvents(Resource):
         )
 
     def keys_itr(self) -> Iterator[tuple[int, bool]]:
+        """Iterate over each key evenet and its pressed status."""
         return (
             (cast(int, (e.key)), e.type == pygame.KEYDOWN)
             for e in self.events
@@ -70,9 +80,12 @@ class WindowEvents(Resource):
 
 @dataclass
 class Inputs(Resource):
+    """An input tracking resource, including smart direction."""
+
     inputs: dict[int, float]
 
     def get_dir(self) -> int | None:
+        """Get which direction has been most recently pressed."""
         res = sorted(
             ((k, v) for k, v in self.inputs.items() if k in dir_lookup),
             key=lambda e: e[1],
@@ -83,11 +96,14 @@ class Inputs(Resource):
         return dir_lookup[res[0][0]]
 
     def activate(self) -> bool:
+        """Get whether an activate key is pressed."""
         return K_RETURN in self.inputs
 
 
 @dataclass
 class Sprite2D(Component, Asset):
+    """A 2D sprite for use in rendering."""
+
     surface: pygame.Surface
 
     @override
@@ -97,10 +113,13 @@ class Sprite2D(Component, Asset):
         return Sprite2D(pygame.image.load(path))
 
     def copy(self) -> "Sprite2D":
+        """Make a shallow copy of this sprite."""
         return Sprite2D(self.surface)
 
 
 def key_just_pressed(key: int) -> System[bool]:
+    """Return a system which returns whether the key was pressed this frame."""
+
     def inner(events: WindowEvents) -> bool:
         return any(key == k and pressed for k, pressed in events.keys_itr())
 
@@ -112,6 +131,8 @@ def window_positionned_insert_hook(
     window: Maybe[Window],
     world: World,
 ) -> RawComponentHook[WindowPositionned]:
+    """Hook when WindowPositionned components are inserted."""
+
     def inner(added: RawComponentArg[WindowPositionned]) -> None:
         dims = Vector2(0, 0)
         if (win := window.v) is not None:
@@ -138,12 +159,14 @@ def window_positionned_insert_hook(
 def update_window_positionned(
     elems: Query[tuple[WindowPositionned, Coord2D]], window: Window
 ) -> None:
+    """Update window-positionned coordinates based on window size."""
     dims = Vector2(window.surface.get_size())
     for pos, e in elems:
         e.xy = dims.elementwise() * pos.pos + pos.offset
 
 
 def init_graphics(world: World) -> None:
+    """Initialize the graphics data."""
     pygame.init()
     world.spawn_resource(
         Window(
@@ -156,10 +179,12 @@ def init_graphics(world: World) -> None:
 
 
 def poll_events(events: WindowEvents) -> None:
+    """Poll the window events from the backend."""
     events.events = pygame.event.get()
 
 
 def close_on_close(world: World, events: WindowEvents) -> None:
+    """Signal to exit the world based on window events."""
     if pygame.QUIT in (event.type for event in events.events):
         world.spawn(WorldShouldExit, WorldShouldExit())
 
@@ -168,6 +193,7 @@ def render_sprite2d(
     window: Window,
     sprites_q: Query[tuple[CoordAbs2D, Sprite2D], ShouldRender],
 ) -> None:
+    """Render depth-ordered 2d sprites."""
     sprites_raw = sorted(
         sprites_q,
         key=lambda e: e[0]._vec.z,
@@ -184,6 +210,8 @@ def render_sprite2d(
 
 @dataclass
 class AnimationSet(Component):
+    """An animation set, allowing for simple cyclic animations."""
+
     anims: list[list[Sprite2D]]
     ticks_per_frame: int
     idx: int = 0
@@ -192,9 +220,11 @@ class AnimationSet(Component):
 
     @property
     def curr_frame(self) -> Sprite2D:
+        """Return the current animation frame."""
         return self.anims[self.idx][self.frame]
 
     def tick(self) -> None:
+        """Update the animation by one frame."""
         self.curr_tick += 1
         self.frame += self.curr_tick // self.ticks_per_frame
         self.curr_tick %= self.ticks_per_frame
@@ -204,6 +234,7 @@ class AnimationSet(Component):
 def insert_animation_sprites(
     world: World, query: Query[tuple[Entity, AnimationSet], Not[Sprite2D]]
 ) -> None:
+    """Insert a sprite on entities which have an animation but no sprite."""
     inserts = []
     for ent, anim in query:
         inserts.append((ent, anim.curr_frame.copy()))
@@ -215,12 +246,14 @@ def update_animations(
         tuple[Sprite2D, AnimationSet], tuple[ShouldRender, Not[StopAnimation]]
     ],
 ) -> None:
+    """Update animations."""
     for sprite, anim in query:
         anim.tick()
         sprite.surface = anim.curr_frame.surface
 
 
 def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
+    """Update the inputs based on window events."""
     for key, state in events.keys_itr():
         if state:
             inputs.inputs[key] = time()
@@ -233,6 +266,7 @@ InputsSystems = SystemSet("inputs-systems")
 
 
 def graphics_plugins(world: World) -> None:
+    """Setup graphics related logic."""
     from pacman.ecs.gui import gui_plugins
 
     world.with_systems(StartupSchedule, init_graphics).with_systems(
index 72f0be56402cb578676164fcc8d21f6d46516d8e..cb93ed712f053b02726ce18eeb7d341f84f4241d 100644 (file)
@@ -1,3 +1,5 @@
+"""GUI in ECS, with buttons, text, and more."""
+
 from collections.abc import Callable
 from dataclasses import dataclass, field
 from enum import Enum, auto
@@ -34,6 +36,8 @@ from pacman.ecs.world import (
 
 @dataclass
 class Font(Asset, Component):
+    """A component for displaying text, newlines not supported."""
+
     font: pygame.font.Font
 
     @override
@@ -44,6 +48,7 @@ class Font(Asset, Component):
         return Font(pygame.font.Font(path, int(size)))
 
     def center_text_for(self, text: str, dims: Vector2) -> CoordBundle:
+        """Compute how to center the text in the given rectangle."""
         width, _ = self.font.size(text)
         height = self.font.get_ascent() + self.font.get_descent()
         return CoordBundle(
@@ -54,6 +59,8 @@ class Font(Asset, Component):
 
 @dataclass
 class TextDisplay(Component):
+    """A component for displaying text, newlines not supported."""
+
     text: str
     font: Font
     color: Color = field(default_factory=lambda: Color(255, 255, 255))
@@ -62,10 +69,14 @@ class TextDisplay(Component):
 
 @dataclass
 class ComputedText(Component):
+    """A component that continuously updates which text the button has."""
+
     cb: System[str]
 
 
 class ButtonState(Enum):
+    """The states a button can be in."""
+
     unselected = auto()
     selected = auto()
     pressed = auto()
@@ -73,12 +84,16 @@ class ButtonState(Enum):
 
 @dataclass
 class Button(Component):
+    """A button component for simple menus."""
+
     cb: System[None]
     state: ButtonState = ButtonState.unselected
 
 
 @dataclass
 class SelectedButton(Resource):
+    """A resource indicating a given button is currently selected."""
+
     button: Entity
 
 
@@ -87,6 +102,7 @@ def update_selected_button(
     selected: SelectedButton,
     events: WindowEvents,
 ) -> None:
+    """Update which button is selected based on inputs and button location."""
     for key, active in events.keys_itr():
         if not active:
             continue
@@ -118,6 +134,7 @@ def update_button_state(
     inputs: Inputs,
     world: World,
 ) -> None:
+    """Update the button states based on inputs and selected button."""
     cbs = []
     for entity, button in buttons:
         if entity is not selected.button:
@@ -129,10 +146,11 @@ def update_button_state(
         elif not inputs.activate():
             button.state = ButtonState.selected
     for cb in cbs:
-        world.run_raw_system(cb)
+        world.run_simple_system(cb)
 
 
 def update_button_texture(buttons: Query[tuple[Button, AnimationSet]]) -> None:
+    """Select an animation set for buttons."""
     for button, anim in buttons:
         anim.idx = button.state.value - 1
 
@@ -141,8 +159,9 @@ def update_computed_text(
     elems: Query[tuple[TextDisplay, ComputedText]],
     world: World,
 ) -> None:
+    """Update TextDisplay based on ComputedText components."""
     for display, compute in elems:
-        display.text = world.run_raw_system(
+        display.text = world.run_simple_system(
             compute.cb,
             cast(Callable[[], str], lambda display=display: display.text),
         )
@@ -151,6 +170,7 @@ def update_computed_text(
 def update_text_sprites(
     elems: Query[tuple[TextDisplay, Sprite2D | Entity]], queue: CommandQueue
 ) -> None:
+    """Update sprites for TextDisplay, inserting a Sprite2D if needed."""
     inserts = []
     for display, e in elems:
         if (
@@ -173,10 +193,12 @@ def update_text_sprites(
 
 
 def setup_gui_resources() -> None:
+    """Setup GUI related resources."""
     pygame.font.init()
 
 
 def gui_plugins(world: World) -> None:
+    """Setup GUI related logic."""
     world.with_systems(StartupSchedule, setup_gui_resources).with_systems(
         UpdateSchedule,
         Systems(
index 116f53ac6c5eb56c2f5cc8f5a465e6c60c098d61..389196ab392c0ff8cacc30b2c86b333e3d21d4fe 100644 (file)
@@ -1,3 +1,5 @@
+"""Hierarchy in ECS, with invariant maintenance."""
+
 from collections.abc import Callable, Generator, Iterable
 from typing import cast, get_args, override
 
@@ -16,26 +18,36 @@ from pacman.utils.type_resolve import resolve_type_aliases
 
 
 class Children(Component):
+    """A component storing the children of this entity."""
+
     def __init__(self, children: Iterable[Entity]) -> None:
+        """Create a parent-children relation."""
         self._children = set(children)
 
     @property
     def children(self) -> Generator[Entity]:
+        """The children of this entity."""
         yield from self._children
 
 
 class ChildOf(Component):
+    """A component storing the parent of this entity."""
+
     def __init__(self, parent: Entity) -> None:
+        """Create a child-parent relation."""
         self._parent = parent
 
     @property
     def parent(self) -> Entity:
+        """The parent of this entity."""
         return self._parent
 
 
-def parent_insert_hook(
+def children_insert_hook(
     queue: CommandQueue, children: Query[ChildOf]
 ) -> RawComponentHook[Children]:
+    """The hook called when a Children component is inserted."""
+
     def inner(itr: RawComponentArg[Children]) -> None:
         inserts = [
             (child, ChildOf(ent))
@@ -48,7 +60,9 @@ def parent_insert_hook(
     return inner
 
 
-def parent_remove_hook(queue: CommandQueue) -> RawComponentHook[Children]:
+def children_remove_hook(queue: CommandQueue) -> RawComponentHook[Children]:
+    """The hook called when a Children component is removed."""
+
     def inner(itr: RawComponentArg[Children]) -> None:
         removes = [e for _, children in itr for e in children.children]
         queue.queue(lambda world: world.remove_many(ChildOf, removes))
@@ -56,10 +70,12 @@ def parent_remove_hook(queue: CommandQueue) -> RawComponentHook[Children]:
     return inner
 
 
-def child_insert_hook(
+def childof_insert_hook(
     queue: CommandQueue,
     parents: Query[Children],
 ) -> RawComponentHook[ChildOf]:
+    """The hook called when a ChildOf component is inserted."""
+
     def inner(itr: RawComponentArg[ChildOf]) -> None:
         inserts = {}
         for ent, inserted in itr:
@@ -75,7 +91,11 @@ def child_insert_hook(
     return inner
 
 
-def child_remove_hook(parents: Query[Children]) -> RawComponentHook[ChildOf]:
+def childof_remove_hook(
+    parents: Query[Children],
+) -> RawComponentHook[ChildOf]:
+    """The hook called when a ChildOf component is removed."""
+
     def inner(itr: RawComponentArg[ChildOf]) -> None:
         for ent, removed in itr:
             parent = parents.get(removed.parent)
@@ -86,10 +106,11 @@ def child_remove_hook(parents: Query[Children]) -> RawComponentHook[ChildOf]:
 
 
 def hierachy_plugins(world: World) -> None:
-    world.with_hook("insert", Children, parent_insert_hook).with_hook(
-        "remove", Children, parent_remove_hook
-    ).with_hook("insert", ChildOf, child_insert_hook).with_hook(
-        "remove", ChildOf, child_remove_hook
+    """Setup hierarchy related logic."""
+    world.with_hook("insert", Children, children_insert_hook).with_hook(
+        "remove", Children, children_remove_hook
+    ).with_hook("insert", ChildOf, childof_insert_hook).with_hook(
+        "remove", ChildOf, childof_remove_hook
     )
 
 
@@ -100,11 +121,14 @@ type NodesQuery[Items, Filters] = Query[tuple[Items, Children | None], Filters]
 
 
 class Hierarchy[Items, Filters = tuple[()]](FromWorld):
+    """A hierarchy allowing for convenient manipulation."""
+
     def __init__(
         self,
         roots: RootsQuery[Items, Filters],
         nodes: NodesQuery[Items, Filters],
     ) -> None:
+        """Create a hierarchy from its queries."""
         self._roots = roots
         self._nodes = nodes
 
@@ -140,6 +164,7 @@ class Hierarchy[Items, Filters = tuple[()]](FromWorld):
         self._nodes.cleanup()
 
     def top_down(self) -> Generator[tuple[Items | None, Items]]:
+        """Traverse this hierarchy top down."""
         queue: list[tuple[Items | None, Entity]] = [
             (None, e) for e in self._roots
         ]
@@ -155,6 +180,7 @@ class Hierarchy[Items, Filters = tuple[()]](FromWorld):
             queue.extend((child, e) for e in children.children)
 
     def despawn_with_children(self, root: Entity) -> Callable[[World], None]:
+        """Despawn an entity and its children which are a part of the query."""
         queue = [root]
         entities = set()
         while len(queue) != 0:
index 8536ae9ab7f0e6ac387fb4d36e43faa73de92e3c..68ca8d483f1627ff3b3c18edcc8611e48b79c369 100644 (file)
@@ -1,3 +1,5 @@
+"""Schedule logic for systems, with run conditions and labels."""
+
 from collections import deque
 from collections.abc import Callable, Generator, Iterable
 from dataclasses import dataclass
@@ -7,15 +9,21 @@ from typing import override
 
 @dataclass
 class DAGNode[T]:
+    """A node in a DAG."""
+
     parents: set[T]
     children: set[T]
 
     def is_root(self) -> bool:
+        """Whether this node has no parents."""
         return len(self.parents) == 0
 
 
 class DAG[T]:
+    """A Directed Acyclic Graph, used for dependency ordering."""
+
     def __init__(self) -> None:
+        """Initialize an empty DAG."""
         self._nodes: dict[T, DAGNode[T]] = {}
 
     def _get_node(self, node: T) -> DAGNode[T]:
@@ -24,6 +32,7 @@ class DAG[T]:
         return self._nodes[node]
 
     def add_edge(self, parent: T, child: T) -> None:
+        """Add an edge between two nodes."""
         parents = self.node_parents(parent)
         children = self.node_children(child)
         shared = parents & children
@@ -37,6 +46,7 @@ class DAG[T]:
         self._get_node(child).parents.add(parent)
 
     def node_parents(self, node: T) -> set[T]:
+        """The set of parents of a node in the graph."""
         if node not in self._nodes:
             return set()
         res = set()
@@ -50,6 +60,7 @@ class DAG[T]:
         return res
 
     def node_children(self, node: T) -> set[T]:
+        """The set of children of a node in the graph."""
         if node not in self._nodes:
             return set()
         res = set()
@@ -63,6 +74,7 @@ class DAG[T]:
         return res
 
     def traverse_from(self, root: T) -> Generator[T]:
+        """Traverse the DAG from dependents to dependencies."""
         visited: set[T] = set()
         queue = deque([root])
         while len(queue) != 0:
@@ -80,7 +92,10 @@ class DAG[T]:
 
 
 class DAGLabel:
+    """A DAG label, used to structure the DAG."""
+
     def __init__(self, name: str) -> None:
+        """Create a named DAGLabel."""
         self.name = name
 
     @override
@@ -89,7 +104,10 @@ class DAGLabel:
 
 
 class ScheduleLabel:
+    """A schedule label, used to name schedules in the world."""
+
     def __init__(self, name: str) -> None:
+        """Create a named ScheduleLabel."""
         self.name = name
 
     @override
@@ -98,14 +116,20 @@ class ScheduleLabel:
 
 
 class SystemSet:
+    """A system set, used to label systems as being in a group."""
+
     def __init__(self, name: str) -> None:
+        """Create a named SystemSet."""
         self.name = name
         self._pre = DAGLabel(name + "-pre")
         self._post = DAGLabel(name + "-post")
 
 
 class Schedule[T]:
+    """A schedule represented by a DAG, with run predicates and system sets."""
+
     def __init__(self) -> None:
+        """Initialize an empty schedule."""
         self._dag: DAG[T | DAGLabel | None] = DAG()
         self._group_appartenance: dict[T | SystemSet, set[SystemSet]] = {}
         self._run_predicates: dict[T | SystemSet, Callable[[], bool]] = {}
@@ -118,6 +142,8 @@ class Schedule[T]:
         group: Iterable[SystemSet],
         cond: Callable[[], bool] | None,
     ) -> None:
+        """Add ordering items to this schedule."""
+
         def get_pre(e: T | SystemSet) -> T | DAGLabel:
             if isinstance(e, SystemSet):
                 return e._pre
@@ -147,6 +173,7 @@ class Schedule[T]:
                     self._run_predicates[item] = cond
 
     def traverse(self) -> Generator[T]:
+        """Traverse the schedule, dependencies first."""
         cond_cache: dict[T | SystemSet, bool] = {}
 
         def should_run(val: T | SystemSet) -> bool:
index 503e7ebc91ec0f9ad79d7572fd03fdf4f590c338..1468f6d22bd402f3b9f53cfb224f3ca355bcc273 100644 (file)
@@ -1,3 +1,5 @@
+"""World update and schedule logic."""
+
 from pacman.ecs.schedule import (
     MainSchedule,
     StartupSchedule,
@@ -8,15 +10,19 @@ from pacman.ecs.world import Not, Resource, Systems, World
 
 
 class WorldIsInit(Resource):
+    """A marker to signal the world has ran the startup schedule."""
+
     pass
 
 
 def run_startup(world: World, _: Not[WorldIsInit]) -> None:
+    """Run the startup schedule."""
     world.tick(StartupSchedule)
     world.spawn(WorldIsInit, WorldIsInit())
 
 
 def run_update(world: World, _: WorldIsInit) -> None:
+    """Run the udpate schedule."""
     world.tick(UpdateSchedule)
 
 
@@ -26,6 +32,7 @@ PostUpdate = SystemSet("postupdate")
 
 
 def schedule_plugins(world: World) -> None:
+    """Setup the schedule related functionality."""
     world.with_systems(MainSchedule, run_startup, run_update).with_systems(
         UpdateSchedule, Systems(PreUpdate, Update, PostUpdate).chain()
     )
index 11c8f116f7aeafd52d4faf3935476683cf71eb38..04ff7b7ee7a5f369ac44b82ef68845f62da9ef75 100644 (file)
@@ -1,3 +1,5 @@
+"""The core of the ECS engine, where everything is bridged together."""
+
 import inspect
 from abc import ABC, abstractmethod
 from collections.abc import Callable, Container, Iterable, Iterator
@@ -30,17 +32,26 @@ from pacman.utils.variadics_please import (
 
 
 class Bundle(ABC):
+    """A class which acts as a set of components."""
+
     @abstractmethod
-    def components_iter(self) -> Iterator["Component"]: ...
+    def components_iter(self) -> Iterator["Component"]:
+        """Iterate over the components of this instance."""
+
     @classmethod
     @abstractmethod
-    def components_type_iter(cls, ty: type) -> Iterator[type["Component"]]: ...
+    def components_type_iter(cls, ty: type) -> Iterator[type["Component"]]:
+        """Iterate over the types of compnents this type contains.
+
+        It is required to match components_iter.
+        """
 
 
 type Components = RecursiveTuple["Component | Bundle"]
 
 
 def components_iter(components: Components) -> Iterator["Component"]:
+    """Iterate over the components of nested components."""
     if isinstance(components, Component):
         yield components
     elif isinstance(components, tuple):
@@ -52,6 +63,7 @@ def components_iter(components: Components) -> Iterator["Component"]:
 def components_type_iter(
     components: type[Components],
 ) -> Iterator[type["Component"]]:
+    """Iterate over the component types of nested components."""
     if origin_or_cls(components) is tuple:
         yield from (
             e
@@ -70,7 +82,13 @@ ComponentID = NewType("ComponentID", int)
 
 
 class Archetype:
+    """An archetype, storing entities which all have the same components.
+
+    Archetypes are never deleted, and cache operation intermediates.
+    """
+
     def __init__(self, world: "World", types: Iterable[ComponentID]) -> None:
+        """Create a new archetype from the given component types."""
         self._world = world
         self._types = frozenset(types)
         self._positions: dict[Entity, int] = {}
@@ -91,6 +109,7 @@ class Archetype:
     def _compute_add[T: Components](
         self, ty: type[T]
     ) -> tuple["Archetype", list[ComponentID]]:
+        """Compute the destination archetype and list of component types."""
         ptr = id(ty)
         if res := self._add_cache.get(ptr):
             return (res[1], res[2])
@@ -112,6 +131,7 @@ class Archetype:
     def _compute_remove[T: Components](
         self, ty: type[T]
     ) -> tuple["Archetype", list[ComponentID]]:
+        """Compute the destination archetype and list of component types."""
         ptr = id(ty)
         if res := self._remove_cache.get(ptr):
             return (res[1], res[2])
@@ -133,6 +153,7 @@ class Archetype:
     def add_components[T: Components](
         self, ty: type[T], components: list[tuple["Entity", T]]
     ) -> Callable[[], None]:
+        """Add components to entities, moving them to other archetypes."""
         dst, comps_ty = self._compute_add(ty)
         comps = [components_iter(e) for _, e in components]
         positions = [self._positions[e] for e, _ in components]
@@ -175,6 +196,7 @@ class Archetype:
     def remove_components[T: Components](
         self, ty: type[T], entities: list["Entity"]
     ) -> Callable[[], None]:
+        """Remove components from entities, moving them to other archetypes."""
         dst, comps_ty = self._compute_remove(ty)
         positions = [self._positions[e] for e in entities]
 
@@ -199,6 +221,7 @@ class Archetype:
         return res
 
     def remove(self, entities: list["Entity"]) -> Callable[[], None]:
+        """Remove the given entities from self, despawning them."""
         entities = [e for e in entities if e in self._positions]
         positions = [self._positions[e] for e in entities]
         comps = [
@@ -222,6 +245,10 @@ class Archetype:
         self,
         cb: Callable[[ComponentID | None], Iterable[Any]],
     ) -> None:
+        """Extends self, calling cb to fetch tables of each required type.
+
+        None is used as a marker to request the entities.
+        """
         for entity in cb(None):
             self._positions[entity] = len(self._entities)
             self._entities.append(entity)
@@ -235,6 +262,10 @@ class Archetype:
         ), "Callaback's return lists weren't all of equal size"
 
     def _discard(self, itr: Iterable[int]) -> None:
+        """Discard the given entities in self.
+
+        This does not maintain invariants by itself.
+        """
         sort = sorted(itr, reverse=True)
 
         def remove_idx[T](
@@ -266,35 +297,52 @@ type ComponentHook[T] = System[RawComponentHook[T]]
 
 
 class FromWorld(ABC):
+    """A type which may be extracted from a world to be given to a system."""
+
     @classmethod
     @abstractmethod
     def extract_from_world(
         cls, ty: type, world: "World"
-    ) -> Callable[[], Self | None]: ...
+    ) -> Callable[[], Self | None]:
+        """Extract this type to pass it to a system.
+
+        A callable is used to allow for easy caching of intermediate
+        computations.
+        """
 
     def cleanup(self) -> None:
+        """Cleanup this system argument after system use."""
         return None
 
 
 class QueryFilter(ABC):
+    """A query filter, that is, a type which may be filtered for by a query."""
+
     @classmethod
     @abstractmethod
     def filter(
         cls, ty: type
-    ) -> Callable[[Archetype], bool | Callable[[int], bool]]: ...
+    ) -> Callable[[Archetype], bool | Callable[[int], bool]]:
+        """Filter for an archetype.
+
+        Computations about archetype appartenance and type extraction are done
+        at each step to improve performance.
+        """
 
 
 type QueryFilters = RecursiveTuple[QueryFilter]
 
 
 class _NoFetchCls:
-    pass
+    """A sentinel class indicating a query item failed to fetch."""
 
 
 _NoFetch = _NoFetchCls()
 
 
 class QueryItem(QueryFilter):
+    """A query item, that is, a type which may be fetched by a query."""
+
     @override
     @classmethod
     def filter(
@@ -309,6 +357,7 @@ class QueryItem(QueryFilter):
 
     @classmethod
     def fetch_prio(cls, ty: type) -> int:
+        """Returns the priority of this type in case of unions."""
         del ty
         return 0
 
@@ -316,13 +365,20 @@ class QueryItem(QueryFilter):
     @abstractmethod
     def filter_fetch(
         cls, ty: type
-    ) -> Callable[[Archetype], None | Callable[[int], Self | _NoFetchCls]]: ...
+    ) -> Callable[[Archetype], None | Callable[[int], Self | _NoFetchCls]]:
+        """Fetch this item for an archetype.
+
+        Computations about archetype appartenance and type extraction are done
+        at each step to improve performance.
+        """
 
 
 type QueryItems = RecursiveTuple[QueryItem | None]
 
 
 class Component(QueryItem):
+    """A component, attachable to an entity, fundamental to ECS."""
+
     @override
     @classmethod
     def filter(cls, ty: type) -> Callable[[Archetype], bool]:
@@ -348,6 +404,8 @@ class Component(QueryItem):
 
 
 class Entity(QueryItem):
+    """An entity, unique identifier in a world, also a query item."""
+
     @override
     @classmethod
     def filter(cls, ty: type) -> Callable[[Archetype], bool]:
@@ -364,6 +422,8 @@ class Entity(QueryItem):
 
 
 class Not[T: FromWorld | Any](FromWorld, QueryItem):
+    """A simple system argument and query item, matching if T does not."""
+
     @override
     @classmethod
     def filter_fetch(
@@ -411,7 +471,10 @@ class Not[T: FromWorld | Any](FromWorld, QueryItem):
 
 
 class Maybe[T: FromWorld](FromWorld):
+    """A simple system argument which always instantiates fallible args."""
+
     def __init__(self, v: T | None) -> None:
+        """Initialize a new maybe value."""
         self.v = v
 
     @override
@@ -431,12 +494,15 @@ class Maybe[T: FromWorld](FromWorld):
 class Query[Items, Filters = tuple[()]](
     FromWorld, Iterable[Items], Container[Entity]
 ):
+    """A query system arg, that allows operations on components."""
+
     def __init__(
         self,
         world: "World",
         fetchers: type[Items],
         filters: type[Filters] | None = None,
     ) -> None:
+        """Construct a query for the world with the given items and filters."""
         if filters is None:
             filters = cast(type[Filters], tuple[()])
         resolved_fetchers = resolve_type_aliases(fetchers)
@@ -463,6 +529,7 @@ class Query[Items, Filters = tuple[()]](
             [Archetype], Callable[[int], QueryItems | _NoFetchCls] | None
         ],
     ]:
+        """Process the query fetchers as much as possible at each step."""
         args = list(map(Query.preprocess_fetchers, get_args(fetcher)))
 
         def tuple_constructor(
@@ -518,6 +585,7 @@ class Query[Items, Filters = tuple[()]](
     def preprocess_filters(
         filter: type[QueryFilters],
     ) -> Callable[[Archetype], Callable[[int], bool] | bool]:
+        """Process the query filters as much as possible at each step."""
         args = list(map(Query.preprocess_filters, get_args(filter)))
 
         def tuple_constructor(
@@ -561,6 +629,7 @@ class Query[Items, Filters = tuple[()]](
         return cast(QueryFilter, filter).filter(filter)
 
     def add_archetype(self, archetype: Archetype) -> None:
+        """Observe a new archetype for consideration in iteration."""
         filt = self._filters_raw(archetype)
         if filt is False:
             return
@@ -577,6 +646,7 @@ class Query[Items, Filters = tuple[()]](
         self._fetchers.append(fetcher)
 
     def updated_from_world(self) -> Self:
+        """Update the query in place, observing new archetypes."""
         for arch in self._world._archetypes[self._seen_archetypes :]:
             self.add_archetype(arch)
         self._seen_archetypes = len(self._world._archetypes)
@@ -616,6 +686,7 @@ class Query[Items, Filters = tuple[()]](
         return self._fetchers[arch_idx](components_idx) is not _NoFetch
 
     def get(self, entity: Entity) -> Items | None:
+        """Get the items for the given entity, or None if absent."""
         arch: Archetype | None = self._world._entities.get(entity)
         if arch is None:
             return None
@@ -629,6 +700,11 @@ class Query[Items, Filters = tuple[()]](
         return cast(Items, res)
 
     def __getitem__(self, entity: Entity) -> Items:
+        """Get the items for the given entity.
+
+        Raises:
+            KeyError: if the entity is not in the query.
+        """
         arch: Archetype | None = self._world._entities.get(entity)
         if arch is None:
             raise KeyError("Entity not in world", entity)
@@ -642,19 +718,24 @@ class Query[Items, Filters = tuple[()]](
         return cast(Items, res)
 
     def single(self) -> Items | None:
+        """Fetches exactly one element from the query if it exists.
+
+        Raises:
+            Exception: if more than one entity matches the query.
+        """
         itr: Iterator[Items] = self.__iter__()
         res = next(itr, None)
         if next(itr, None) is not None:
             raise Exception("More than one entity matches single query")
         return res
 
-    def single_e(self) -> Items:
-        if (res := self.single()) is None:
-            raise Exception("single_e must contain exactly one matching item")
-        return res
-
 
 class Resource(FromWorld, Component):
+    """A resource system argument.
+
+    Resources exist in only one instance in the world.
+    """
+
     @override
     @classmethod
     def extract_from_world(
@@ -680,12 +761,23 @@ class Resource(FromWorld, Component):
     def extract_default(
         cls, ty: type, world: "World"
     ) -> Callable[[], Self | None]:
+        """Return a callable that optionally returns a default value.
+
+        Override this to allow for resources to be initialized if not present.
+        """
         del ty, world
         return lambda: None
 
 
 class CommandQueue(FromWorld):
+    """A command queue system argument for defered execution.
+
+    It stores functions to be called after system exit during cleanup.
+    Use it to avoid invalidating queries while still in use.
+    """
+
     def __init__(self, world: "World") -> None:
+        """Create an emtpy CommandQueue."""
         self._world = world
         self._queue: list[Callable[[World], Any]] = []
 
@@ -703,31 +795,45 @@ class CommandQueue(FromWorld):
             sys(self._world)
 
     def queue(self, sys: Callable[["World"], Any]) -> None:
+        """Enqueue the given sys to be executed after system exit."""
         self._queue.append(sys)
 
 
 def _system_failed_va[*P = *tuple[()]](*_: *P) -> Never:
+    """A default system fail callback for systems with arguments."""
     raise ValueError("Failed to fetch arguments for system")
 
 
 def _system_failed() -> Never:
+    """A default system fail callback."""
     raise ValueError("Failed to fetch arguments for system")
 
 
 @dataclass
 class ComponentHooks[T: Component]:
+    """Component lifecycle hooks."""
+
     _insert: RawComponentHook[T] = lambda _: None
     _remove: RawComponentHook[T] = lambda _: None
 
     def add_insert(self, cb: RawComponentHook[T]) -> None:
+        """Add an insert hook to these hooks."""
         self._insert = composed_by(self._insert, cb, lambda _1, _2: None)
 
     def add_remove(self, cb: RawComponentHook[T]) -> None:
+        """Add a remove hook to these hooks."""
         self._remove = composed_by(self._remove, cb, lambda _1, _2: None)
 
 
 class World(FromWorld):
+    """The world, core of the ECS.
+
+    It contains every entity, their components, stored in archetypes,
+    as well as the schedules.
+    """
+
     def __init__(self) -> None:
+        """Create an empty world."""
         self._entities: dict[Entity, Archetype] = {}
         self._archetypes_lookup: dict[frozenset[ComponentID], Archetype] = {}
         self._archetypes: list[Archetype] = []
@@ -743,6 +849,7 @@ class World(FromWorld):
         ] = {}
 
     def component_id(self, comp: type) -> ComponentID:
+        """Get a unique ComponentID for a given type."""
         if (res := self._components_lookup.get(comp)) is None:
             res = ComponentID(len(self._components_cache))
             self._components_cache.append((comp, ComponentHooks()))
@@ -750,9 +857,14 @@ class World(FromWorld):
         return res
 
     def spawn_empty(self) -> Entity:
+        """Spawn a single empty entity.
+
+        If called repeatedly, it is preferable to call spawn_many_empty.
+        """
         return self.spawn_many_empty(1)[0]
 
     def spawn_many_empty(self, n: int) -> list[Entity]:
+        """Spawn many empty entities efficiently."""
         res = [Entity() for _ in range(n)]
         self._base_archetype._extend(
             lambda ty: (
@@ -763,12 +875,10 @@ class World(FromWorld):
         )
         return res
 
-    def spawn[T: Components](self, ty: type[T], comp: T) -> Entity:
-        return self.spawn_many(ty, [comp])[0]
-
     def spawn_resource[T: Resource](
         self, comp: T, ty: type[T] | None = None
     ) -> Entity:
+        """Spawn a resource or overwrite it if it exists."""
         if ty is None:
             ty = type(comp)
 
@@ -776,16 +886,25 @@ class World(FromWorld):
         return self.spawn(ty, comp)
 
     def despawn_resource[T: Resource](self, ty: type[T]) -> None:
+        """Despawn a resource if it exists."""
         type Ty = ty  # type: ignore
 
         def remove(query: Query[Entity, Ty], world: World) -> None:
             world.remove_many(ty, list(query))
 
-        self.run_raw_system(remove)
+        self.run_simple_system(remove)
+
+    def spawn[T: Components](self, ty: type[T], comp: T) -> Entity:
+        """Spawn an entity, with initial components.
+
+        If called repeatedly, it is preferable to call spawn_many.
+        """
+        return self.spawn_many(ty, [comp])[0]
 
     def spawn_many[T: Components](
         self, ty: type[T], comps: list[T]
     ) -> list[Entity]:
+        """Spawn multiple entities efficiently, with initial components."""
         ents = self.spawn_many_empty(len(comps))
         self._base_archetype.add_components(
             cast(type[Components], ty),
@@ -794,9 +913,14 @@ class World(FromWorld):
         return ents
 
     def despawn(self, ent: Entity) -> None:
+        """Despawn a single entity.
+
+        If called repeatedly, it is preferable to call despawn_many.
+        """
         self.despawn_many([ent])
 
     def despawn_many(self, ents: Iterable[Entity]) -> None:
+        """Despawn many entities efficiently."""
         archs: dict[Archetype, list[Entity]] = {}
         for ent in ents:
             arch = self._entities[ent]
@@ -808,11 +932,16 @@ class World(FromWorld):
             cb()
 
     def insert[T: Components](self, ty: type[T], ent: Entity, comp: T) -> None:
+        """Insert components into a single entities.
+
+        If called repeatedly, it is preferable to call insert_many.
+        """
         self.insert_many(ty, [(ent, comp)])
 
     def insert_many[T: Components](
         self, ty: type[T], comps: Iterable[tuple[Entity, T]]
     ) -> None:
+        """Insert components into many entities efficiently."""
         archs: dict[Archetype, list[tuple[Entity, T]]] = {}
         for ent, comp in comps:
             arch = self._entities[ent]
@@ -825,13 +954,18 @@ class World(FromWorld):
             cb()
 
     def remove[T: Components](self, ty: type[T], ent: Entity) -> None:
+        """Remove components from a single entities.
+
+        If called repeatedly, it is preferable to call remove_many.
+        """
         self.remove_many(ty, [ent])
 
     def remove_many[T: Components](
-        self, ty: type[T], comps: Iterable[Entity]
+        self, ty: type[T], entities: Iterable[Entity]
     ) -> None:
+        """Remove components from many entities efficiently."""
         archs: dict[Archetype, list[Entity]] = {}
-        for ent in comps:
+        for ent in entities:
             if ent not in self._entities:
                 continue
             arch = self._entities[ent]
@@ -846,6 +980,7 @@ class World(FromWorld):
     def _sys_extractors[T](
         self, system: System[T]
     ) -> tuple[Callable[[], FromWorld | None], ...]:
+        """Create extractors for each arg of a system."""
         if system in self._extractors_cache:
             return self._extractors_cache[system]
         sig = inspect.signature(system, eval_str=True)
@@ -873,6 +1008,7 @@ class World(FromWorld):
         system: System[Callable[[*P], T]],
         default: Callable[[*P], T] = _system_failed_va,
     ) -> Callable[[*P], T]:
+        """Add a system with arguments and return the raw system."""
         extractors = self._sys_extractors(system)
 
         def inner(*cb_args: *P) -> T:
@@ -889,11 +1025,12 @@ class World(FromWorld):
 
         return inner
 
-    def add_raw_system[T](
+    def add_simple_system[T](
         self,
         system: System[T],
         default: Callable[[], T] = _system_failed,
     ) -> RawSystem[T]:
+        """Add a simple system and return the raw system."""
         extractors = self._sys_extractors(system)
 
         def inner() -> T:
@@ -916,14 +1053,16 @@ class World(FromWorld):
         *args: *P,
         default: Callable[[*P], T] = _system_failed_va,
     ) -> T:
+        """Run a system with arguments."""
         return self.add_system(system, default)(*args)
 
-    def run_raw_system[T](
+    def run_simple_system[T](
         self,
         system: System[T],
         default: Callable[[], T] = _system_failed,
     ) -> T:
-        return self.add_raw_system(system, default)()
+        """Run a simple system."""
+        return self.add_simple_system(system, default)()
 
     @classmethod
     @override
@@ -934,6 +1073,7 @@ class World(FromWorld):
         return lambda: world
 
     def tick(self, schedule: ScheduleLabel) -> None:
+        """Execute the given schedule once."""
         if schedule not in self._schedules:
             return
         for system in self._schedules[schedule].traverse():
@@ -944,12 +1084,16 @@ class World(FromWorld):
         schedule: ScheduleLabel,
         stop_cond: RawSystem[bool] = lambda: False,
     ) -> None:
+        """Repeatedly run the schedule until stop_cond returns true."""
         while not stop_cond():
             self.tick(schedule)
 
     def _register_schedule_system[T](self, system: System[T]) -> System[T]:
+        """Add a system to the scheduler cache, for faster later lookup."""
         if system not in self._systems:
-            self._systems[system] = self.add_raw_system(system, lambda: None)
+            self._systems[system] = self.add_simple_system(
+                system, lambda: None
+            )
         return system
 
     def with_systems(
@@ -957,6 +1101,7 @@ class World(FromWorld):
         schedule: ScheduleLabel,
         *systems: "System | Systems",
     ) -> Self:
+        """Add the systems to the given schedule."""
         if schedule not in self._schedules:
             self._schedules[schedule] = Schedule()
         Systems(*systems)._apply(self, self._schedules[schedule])
@@ -968,6 +1113,7 @@ class World(FromWorld):
         ty: type[T],
         hook: ComponentHook[T],
     ) -> Self:
+        """Add the component hook to the world for the given component."""
         comp_id = self.component_id(resolve_type_aliases(ty))
         hooks = cast(ComponentHooks[T], self._components_cache[comp_id][1])
         sys = self.add_system(hook, default=lambda _: None)
@@ -979,6 +1125,7 @@ class World(FromWorld):
         return self
 
     def with_plugins(self, *plugins: Plugin) -> Self:
+        """Add the plugins to the world."""
         for plugin in plugins:
             if isinstance(plugin, tuple):
                 self.with_plugins(plugin)
@@ -987,14 +1134,18 @@ class World(FromWorld):
         return self
 
     def run_main(self) -> None:
-        self.run(MainSchedule, self.add_raw_system(should_exit))
+        """Run the default main schedule on the world."""
+        self.run(MainSchedule, self.add_simple_system(should_exit))
 
 
 _order_groups: int = 0
 
 
 class Systems:
+    """A builder to create complex system ordering and set apparentance."""
+
     def __init__(self, *systems: "System | SystemSet | Systems") -> None:
+        """Create a builder with these systems."""
         self._systems = systems
         self._chain = False
         self._pre: list[System | SystemSet] = []
@@ -1003,26 +1154,32 @@ class Systems:
         self._conds: list[System[bool]] = []
 
     def chain(self) -> Self:
+        """Mark these systems to be executed in order."""
         self._chain = True
         return self
 
     def after(self, *systems: System | SystemSet) -> Self:
+        """Order these systems to execute after other schedule elements."""
         self._pre.extend(systems)
         return self
 
     def before(self, *systems: System | SystemSet) -> Self:
+        """Order these systems to execute before other schedule elements."""
         self._post.extend(systems)
         return self
 
     def in_set(self, *sets: SystemSet) -> Self:
+        """Add these systems to zero or more SystemSet."""
         self._in_set.extend(sets)
         return self
 
     def run_if(self, *conds: System[bool]) -> Self:
+        """Add zero or more run conditions to these systems."""
         self._conds.extend(conds)
         return self
 
     def _apply(self, world: World, sched: Schedule[System[Any]]) -> SystemSet:
+        """Applie this builder to the world and schedule."""
         global _order_groups
         _order_groups += 1
         label = SystemSet(f"order-group-{_order_groups}")
@@ -1038,7 +1195,11 @@ class Systems:
         )
         for cond in self._conds:
             sched.add_ordering(
-                (label,), (), (), (), world.add_raw_system(cond, lambda: False)
+                (label,),
+                (),
+                (),
+                (),
+                world.add_simple_system(cond, lambda: False),
             )
         sched.add_ordering((label,), self._pre, self._post, self._in_set, None)
         if self._chain:
@@ -1052,8 +1213,9 @@ class Systems:
 
 
 class WorldShouldExit(Resource):
-    pass
+    """A marker resource to signal the world should exit at this tick."""
 
 
 def should_exit(e: Maybe[WorldShouldExit]) -> bool:
+    """Returns whether this world has the exit marker resource."""
     return e.v is not None
index 6e7b078685f6638fe3104dff79f1fb8175cc7f0d..cdd2142c826d0bc99380a8131c52db8cf07f13e9 100644 (file)
@@ -59,9 +59,7 @@ _DIR_OFFSETS = [(0, -1), (1, 0), (0, 1), (-1, 0)]
 
 
 class GhostVariant(Enum):
-    """
-    Enumeration for Ghost variant and behavior.
-    """
+    """Enumeration for Ghost variant and behavior."""
 
     A = auto()
     B = auto()
@@ -76,9 +74,7 @@ class GhostVariant(Enum):
         slf: "Ghost",
         blinky_pos: tuple[int, int],
     ) -> tuple[int, int]:
-        """
-        Return ghost target coordinates.
-        """
+        """Return ghost target coordinates."""
         match self:
             case GhostVariant.A:
                 return player
@@ -99,9 +95,7 @@ class GhostVariant(Enum):
 
 
 class GhostState(Enum):
-    """
-    Enumeration for Ghost state and attitudes.
-    """
+    """Enumeration for Ghost state and attitudes."""
 
     chase = auto()
     wander = auto()
@@ -112,9 +106,7 @@ class GhostState(Enum):
 
 @dataclass
 class InitialPauseTimer(Resource):
-    """
-    Resource use for the initial pause before starting a level.
-    """
+    """Resource use for the initial pause before starting a level."""
 
     tick: int = 0
 
@@ -128,9 +120,7 @@ class InitialPauseTimer(Resource):
 
 
 class Config(Resource, BaseModel):
-    """
-    Resource storing the configuration of the game.
-    """
+    """Resource storing the configuration of the game."""
 
     ghost_chase_speed: float = Field(default=2.5, ge=0.1, le=100.0)
     ghost_flee_speed: float = Field(default=1.5, ge=0.1, le=100.0)
@@ -146,17 +136,13 @@ class Config(Resource, BaseModel):
 
 
 class Highscores(Resource, BaseModel):
-    """
-    Store the hichscores in a dictionnary.
-    """
+    """Store the hichscores in a dictionnary."""
 
     scores: dict[str, int]
 
 
 class GameIsOver(Resource):
-    """
-    Resource set when game is over.
-    """
+    """Resource set when game is over."""
 
     pass
 
@@ -168,9 +154,7 @@ class PlayerName(Resource):
 
 @dataclass
 class GameState(Resource):
-    """
-    Resource storing current score, level, timer and lives.
-    """
+    """Resource storing current score, level, timer and lives."""
 
     score: int = 0
     level: int = 0
@@ -179,58 +163,44 @@ class GameState(Resource):
 
 
 class CheatMode(Component):
-    """
-    Component for the cheat mod.
-    """
+    """Component for the cheat mod."""
 
     pass
 
 
 class MainMenu(Component):
-    """
-    Component for main menu items.
-    """
+    """Component for main menu items."""
 
     pass
 
 
 class GameMenu(Component):
-    """
-    Component for game menu items.
-    """
+    """Component for game menu items."""
 
     pass
 
 
 class GameGui(Component):
-    """
-    Component for game graphical user interface items.
-    """
+    """Component for game graphical user interface items."""
 
     pass
 
 
 class PacGum(Component):
-    """
-    Component for pacgums items.
-    """
+    """Component for pacgums items."""
 
     pass
 
 
 class SuperPacGum(Component):
-    """
-    Component for super pacgums items.
-    """
+    """Component for super pacgums items."""
 
     pass
 
 
 @dataclass
 class Pacman(Component):
-    """
-    Component for pacman items storing the home coordinates and entity.
-    """
+    """Component for pacman items storing the home coordinates and entity."""
 
     home: tuple[int, int]
     home_entity: Entity
@@ -238,9 +208,7 @@ class Pacman(Component):
 
 @dataclass
 class GhostStatus(Component):
-    """
-    Component for ghost state and cycle.
-    """
+    """Component for ghost state and cycle."""
 
     tick: int = 0
     cycle_wander: int = 60 * 10
@@ -249,8 +217,7 @@ class GhostStatus(Component):
 
 @dataclass
 class Ghost(Component):
-    """
-    Component for ghost items storing the ghost variant and the home
+    """Component for ghost items storing the ghost variant and the home
     coordinates and entity.
     """
 
@@ -261,26 +228,21 @@ class Ghost(Component):
 
 @dataclass
 class TargetTile(Component):
-    """
-    Component for the coordinates of a target used for ghost movement.
-    """
+    """Component for the coordinates of a target used for ghost movement."""
 
     pos: tuple[int, int]
 
 
 @dataclass
 class TilePos(Component):
-    """
-    Component tile position as entity.
-    """
+    """Component tile position as entity."""
 
     pos: Entity
 
 
 @dataclass
 class InMotion(Component):
-    """
-    Component for pacman or ghost movement storing the position, the provenance
+    """Component for pacman or ghost movement storing the position, the provenance
     and the direction.
     """
 
@@ -291,16 +253,13 @@ class InMotion(Component):
 
 @dataclass
 class MoveSpeed(Component):
-    """
-    Component for movement storing the movement speed by pixels per frames.
-    """
+    """Component for movement storing the movement speed by pixels per frames."""
 
     pixels_per_frame: float
 
 
 class Maze(Component):
-    """
-    Component for maze storing the x and y dimension and the cells in a
+    """Component for maze storing the x and y dimension and the cells in a
     dictionnary (coordonate entity).
     """
 
@@ -312,37 +271,28 @@ class Maze(Component):
 
 @dataclass
 class Cell(Component):
-    """
-    Component for the cells of the maze storing the value and the position.
-    """
+    """Component for the cells of the maze storing the value and the position."""
 
     n: int
     grid_pos: tuple[int, int]
 
     def neighbor(self, idx: int) -> tuple[int, int]:
-        """
-        Get neighbor position by direction (1: up, 2: right ...).
-        """
+        """Get neighbor position by direction (1: up, 2: right ...)."""
         dx, dy = _DIR_OFFSETS[idx]
         return (self.grid_pos[0] + dx, self.grid_pos[1] + dy)
 
     def walls(self) -> list[bool]:
-        """
-        Get list of walls.
-        """
+        """Get list of walls."""
         return [(1 & (self.n >> i)) == 1 for i in range(4)]
 
 
 def remove_comment(s: str) -> str:
-    """
-    Return s with all comment lines removed.
-    """
+    """Return s with all comment lines removed."""
     return "\n".join(line for line in s.split("\n") if not s.startswith("#"))
 
 
 def load_config(world: World) -> None:
-    """
-    Load the configuration json file given in first argument and spawn the
+    """Load the configuration json file given in first argument and spawn the
     Config resource.
     """
     if len(sys.argv) != 2:
@@ -356,9 +306,7 @@ def load_config(world: World) -> None:
 
 
 def load_highscores(world: World, config: Config) -> None:
-    """
-    Load the highscores json file and spawn the Highscores resource.
-    """
+    """Load the highscores json file and spawn the Highscores resource."""
     try:
         with open(config.highscores_path) as f:
             scores = Highscores.model_validate_json(f.read())
@@ -368,9 +316,7 @@ def load_highscores(world: World, config: Config) -> None:
 
 
 def save_highscores(config: Config, scores: Highscores) -> None:
-    """
-    Write the json highscores file from the Highscores resource.
-    """
+    """Write the json highscores file from the Highscores resource."""
     path = config.highscores_path
     with open(path, "w+") as f:
         f.write(scores.model_dump_json(indent=4))
@@ -379,9 +325,7 @@ def save_highscores(config: Config, scores: Highscores) -> None:
 def create_maze(
     world: World, assets: Assets[Sprite2D]
 ) -> Callable[[MazeGenerator], Entity]:
-    """
-    System creating and adding the maze to the world.
-    """
+    """System creating and adding the maze to the world."""
 
     def inner(maze: MazeGenerator) -> Entity:
         root_maze = Maze(len(maze.maze[0]), len(maze.maze))
@@ -424,9 +368,7 @@ def create_maze(
 def spawn_superpacgums(
     world: World, mazes: Query[Maze], assets: Assets[Sprite2D]
 ) -> None:
-    """
-    System creating and adding super pacgums to the maze.
-    """
+    """System creating and adding super pacgums to the maze."""
     pacgums = []
     for maze in mazes:
         for pos in [
@@ -457,9 +399,7 @@ def spawn_pacgums(
     superpacgums: Query[ChildOf, SuperPacGum],
     assets: Assets[Sprite2D],
 ) -> None:
-    """
-    System creating and adding pacgums to the maze.
-    """
+    """System creating and adding pacgums to the maze."""
     ignore = {e.parent for e in superpacgums}
     pacgums = []
     for ent, cell in cells:
@@ -485,9 +425,7 @@ def spawn_pacgums(
 
 
 def setup_maze(world: World) -> None:
-    """
-    System running the 'create_maze' system with a give MazeGenerator.
-    """
+    """System running the 'create_maze' system with a give MazeGenerator."""
     world.run_system(create_maze, MazeGenerator((14, 14)))
 
 
@@ -495,9 +433,7 @@ def move_in_motion(
     entities: Query[tuple[InMotion, Coord2D, MoveSpeed]],
     positions: Query[Coord2D],
 ) -> None:
-    """
-    System enabling movement operation for entity wit InMotion component.
-    """
+    """System enabling movement operation for entity wit InMotion component."""
     for motion, pos, speed in entities:
         if (dst := positions.get(motion.pos)) is None:
             continue
@@ -512,9 +448,7 @@ def update_tile_pos(
     entities: Query[tuple[InMotion, Coord2D, TilePos]],
     cells: Query[Coord2D],
 ) -> None:
-    """
-    System updating the tile position for entity wit InMotion component.
-    """
+    """System updating the tile position for entity wit InMotion component."""
     for motion, coord, pos in entities:
         src = cells[motion.src]
         dst = cells[motion.pos]
@@ -530,9 +464,7 @@ def spawn_pacman(
     cells: Query[Cell],
     config: Config,
 ) -> None:
-    """
-    System creating and adding pacman to the maze.
-    """
+    """System creating and adding pacman to the maze."""
     if (maze_t := maze_q.single()) is None:
         return
     maze, maze_ent = maze_t
@@ -584,9 +516,7 @@ def spawn_ghosts(
     maze_q: Query[tuple[Maze, Entity]],
     config: Config,
 ) -> None:
-    """
-    System creating and adding ghosts to the maze.
-    """
+    """System creating and adding ghosts to the maze."""
     if (maze_t := maze_q.single()) is None:
         return
     maze, maze_ent = maze_t
@@ -651,9 +581,7 @@ def move_pacman(
     cells: Query[tuple[Cell, Coord2D]],
     maze_q: Query[Maze],
 ) -> None:
-    """
-    System giving target movement to pacman with the input given by the user.
-    """
+    """System giving target movement to pacman with the input given by the user."""
     if (maze := maze_q.single()) is None:
         return
     for coord, motion, pos in pacman_q:
@@ -682,9 +610,7 @@ def collect_pacgum(
     score: GameState,
     config: Config,
 ) -> None:
-    """
-    System managing the collision and collection of pacgum by pacman.
-    """
+    """System managing the collision and collection of pacgum by pacman."""
     collided = [
         pacgum
         for coord, pacgum in pacgums
@@ -703,8 +629,7 @@ def collect_superpacgum(
     score: GameState,
     config: Config,
 ) -> None:
-    """
-    System managing the collision and collection of superpacgum by pacman and
+    """System managing the collision and collection of superpacgum by pacman and
     starting the ghost flee state if a superpacgum is collected.
     """
     collided = [
@@ -728,9 +653,7 @@ def collect_superpacgum(
 
 
 def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None:
-    """
-    System reseting pacman position when touched by ghost.
-    """
+    """System reseting pacman position when touched by ghost."""
     for pacman, pos, motion in pacmans:
         pos.xy = Vector2(*pacman.home).elementwise() * 64
         motion.pos = pacman.home_entity
@@ -738,9 +661,7 @@ def respawn_pacman(pacmans: Query[tuple[Pacman, Coord2D, InMotion]]) -> None:
 
 
 def respawn_ghost(ghosts: Query[tuple[Ghost, Coord2D, InMotion]]) -> None:
-    """
-    System reseting ghost fleeing position when touched by pacman.
-    """
+    """System reseting ghost fleeing position when touched by pacman."""
     for ghost, pos, motion in ghosts:
         pos.xy = Vector2(*ghost.home).elementwise() * 64
         motion.pos = ghost.home_entity
@@ -758,9 +679,7 @@ def collide_ghost(
     state: GameState,
     config: Config,
 ) -> None:
-    """
-    System managing the collision between by pacman and ghost.
-    """
+    """System managing the collision between by pacman and ghost."""
     for pacman_pos in pacmans:
         for ghost_pos, status, ghost, motion, pos in ghosts:
             if (
@@ -770,11 +689,11 @@ def collide_ghost(
                 continue
             match status.state:
                 case GhostState.wander | GhostState.chase:
-                    if world.run_raw_system(cheat_mode_enabled):
+                    if world.run_simple_system(cheat_mode_enabled):
                         continue
-                    world.run_raw_system(respawn_pacman)
-                    world.run_raw_system(respawn_ghost)
-                    world.run_raw_system(start_pause_timer)
+                    world.run_simple_system(respawn_pacman)
+                    world.run_simple_system(respawn_ghost)
+                    world.run_simple_system(start_pause_timer)
                     state.lives -= 1
                     break
 
@@ -794,9 +713,7 @@ def move_to_target(
     cells: Query[tuple[Cell, Coord2D]],
     maze_q: Query[Maze],
 ) -> None:
-    """
-    System giving target movement to ghost.
-    """
+    """System giving target movement to ghost."""
     if (maze := maze_q.single()) is None:
         return
     for target, motion, pos, status in entities:
@@ -836,9 +753,7 @@ def update_ghost_targets(
     pacman_q: Query[tuple[InMotion, TilePos], Pacman],
     cells: Query[Cell],
 ) -> None:
-    """
-    System updating target to ghost.
-    """
+    """System updating target to ghost."""
     player = pacman_q.single()
     if player is None:
         return
@@ -865,9 +780,7 @@ def update_ghost_targets(
 
 
 def update_chase_status(status_q: Query[GhostStatus], config: Config) -> None:
-    """
-    System updating ghost status and cycle.
-    """
+    """System updating ghost status and cycle."""
     for status in status_q:
         target_time: int
         next_state: GhostState
@@ -904,9 +817,7 @@ def update_chase_status(status_q: Query[GhostStatus], config: Config) -> None:
 def update_ghost_speed(
     ghost: Query[tuple[MoveSpeed, GhostStatus]], config: Config
 ) -> None:
-    """
-    System updating ghost speed according to his state.
-    """
+    """System updating ghost speed according to his state."""
     for speed, status in ghost:
         match status.state:
             case GhostState.chase | GhostState.wander:
@@ -918,9 +829,7 @@ def update_ghost_speed(
 def update_pacman_anim(
     pacman_q: Query[tuple[InMotion, AnimationSet], Pacman],
 ) -> None:
-    """
-    System updating pacman animation according to his direction.
-    """
+    """System updating pacman animation according to his direction."""
     for motion, anim in pacman_q:
         anim.idx = motion.direction
 
@@ -929,9 +838,7 @@ def update_ghost_anim(
     ghost_q: Query[tuple[InMotion, AnimationSet, GhostStatus], Ghost],
     config: Config,
 ) -> None:
-    """
-    System updating pacman animation according to his direction and state.
-    """
+    """System updating pacman animation according to his direction and state."""
     for motion, anim, status in ghost_q:
         match status.state:
             case GhostState.chase | GhostState.wander:
@@ -947,15 +854,12 @@ def update_ghost_anim(
 
 
 def start_pause_timer(timer: "InitialPauseTimer") -> None:
-    """
-    System starting the initial pause timer.
-    """
+    """System starting the initial pause timer."""
     timer.tick = 0
 
 
 def setup_new_maze(world: World) -> None:
-    """
-    System setting up the maze by running 'setup_maze', 'spawn_superpacgums',
+    """System setting up the maze by running 'setup_maze', 'spawn_superpacgums',
     'spawn_pacgums', 'spawn_pacman', 'spawn_ghosts' and
     'start_pause_timer' systems.
     """
@@ -967,13 +871,11 @@ def setup_new_maze(world: World) -> None:
         spawn_ghosts,
         start_pause_timer,
     ):
-        world.run_raw_system(system)
+        world.run_simple_system(system)
 
 
 def maze_exists(itr: Query[Maze]) -> bool:
-    """
-    System cheking maze existance.
-    """
+    """System cheking maze existance."""
     return next(itr.__iter__(), None) is not None
 
 
@@ -982,20 +884,16 @@ def game_is_over(game_over: Query[GameIsOver]) -> bool:
 
 
 def in_game(world: World) -> bool:
-    """
-    System cheking that the game is running.
-    """
-    return world.run_raw_system(maze_exists) and not world.run_raw_system(
-        in_game_menu
-    )
+    """System cheking that the game is running."""
+    return world.run_simple_system(
+        maze_exists
+    ) and not world.run_simple_system(in_game_menu)
 
 
 def level_is_finished(
     pacgums: Query[SuperPacGum | PacGum], _: Not[GameIsOver]
 ) -> bool:
-    """
-    System cheking the level completion.
-    """
+    """System cheking the level completion."""
     return next(iter(pacgums), None) is None
 
 
@@ -1005,14 +903,13 @@ def victory(
     queue: CommandQueue,
     state: GameState,
 ) -> None:
-    """
-    System called when level is completed, generating a new level or finishing
+    """System called when level is completed, generating a new level or finishing
     the game if all levels are completed.
     """
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
     if state.level == 2:
-        queue.queue(lambda world: world.run_raw_system(game_over))
+        queue.queue(lambda world: world.run_simple_system(game_over))
         return
     state.level += 1
     state.lives += 1
@@ -1023,9 +920,7 @@ def victory(
 def level_is_lost(
     config: Config, state: GameState, _: Not[GameIsOver]
 ) -> bool:
-    """
-    System cheking the level is lost.
-    """
+    """System cheking the level is lost."""
     return config.level_timer < state.timer or state.lives <= 0
 
 
@@ -1083,39 +978,31 @@ def lose(
     mazes: Query[Entity, Maze],
     queue: CommandQueue,
 ) -> None:
-    """
-    System called when the player lose the game.
-    """
+    """System called when the player lose the game."""
     world.despawn_resource(GameState)
     world.despawn_many(list(gui))
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
-    queue.queue(lambda world: world.add_raw_system(game_over)())
-    world.run_raw_system(spawn_game_over)
+    queue.queue(lambda world: world.add_simple_system(game_over)())
+    world.run_simple_system(spawn_game_over)
 
 
 def game_over(queue: CommandQueue) -> None:
     # FIXME
-    """
-    System called when game is finish.
-    """
+    """System called when game is finish."""
     queue.queue(lambda world: world.spawn_resource(GameIsOver()))
     queue.queue(lambda world: world.spawn_resource(PlayerName()))
 
 
 def update_game_timer(world: World, state: GameState) -> None:
-    """
-    System updating the levels timer.
-    """
-    if world.run_raw_system(cheat_mode_enabled):
+    """System updating the levels timer."""
+    if world.run_simple_system(cheat_mode_enabled):
         return
     state.timer += 1
 
 
 def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool:
-    """
-    System cheking if the inital timer is done.
-    """
+    """System cheking if the inital timer is done."""
     if timer.tick < config.initial_pos_timer:
         timer.tick += 1
         return False
@@ -1123,30 +1010,23 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool:
 
 
 def print_score(state: GameState) -> str:
-    """
-    Return a string with the player score.
-    """
+    """Return a string with the player score."""
     return f"Score: {state.score}"
 
 
 def print_lives(state: GameState) -> str:
-    """
-    Return a string with the player lives.
-    """
+    """Return a string with the player lives."""
     return f"Lives: {state.lives}"
 
 
 def print_timer(state: GameState, config: Config) -> str:
-    """
-    Return a string with the level timer.
-    """
+    """Return a string with the level timer."""
     time = config.level_timer - state.timer
     return f"Time left: {time // 60}"
 
 
 def spawn_hud(world: World, fonts: Assets[Font]) -> None:
-    """
-    System that print the Heads-Up Display in game with lives, levels and timer
+    """System that print the Heads-Up Display in game with lives, levels and timer
     informations.
     """
     world.spawn_many(
@@ -1186,31 +1066,25 @@ def start_game(
     hierarchy: Hierarchy[tuple[()]],
     menus: Query[Entity, MainMenu],
 ) -> None:
-    """
-    System closing main menu and starting the game and Heads-Up Display.
-    """
+    """System closing main menu and starting the game and Heads-Up Display."""
     for menu in menus:
         hierarchy.despawn_with_children(menu)(world)
     world.despawn_resource(SelectedButton)
 
     world.spawn_resource(GameState())
-    world.run_raw_system(setup_new_maze)
-    world.run_raw_system(spawn_hud)
+    world.run_simple_system(setup_new_maze)
+    world.run_simple_system(spawn_hud)
 
 
 def quit_game(world: World) -> None:
-    """
-    System closing the game and quiting the program.
-    """
+    """System closing the game and quiting the program."""
     world.spawn_resource(WorldShouldExit())
 
 
 def spawn_button(
     world: World, fonts: Assets[Font], sprites: Assets[Sprite2D]
 ) -> Callable[[WindowPositionned, str, System], Entity]:
-    """
-    System creating and adding a button to the world.
-    """
+    """System creating and adding a button to the world."""
 
     def inner(pos: WindowPositionned, s: str, cb: System) -> Entity:
         parent = world.spawn(
@@ -1278,7 +1152,7 @@ def cheat_mode_enabled(cheat: Query[CheatMode]) -> bool:
 
 
 def enable_cheat_mode(world: World) -> None:
-    world.run_raw_system(spawn_cheat_info)
+    world.run_simple_system(spawn_cheat_info)
     world.spawn(CheatMode, CheatMode())
 
 
@@ -1288,10 +1162,10 @@ def disable_cheat_mode(world: World, cheats: Query[Entity, CheatMode]) -> None:
 
 
 def toggle_cheat_mode(world: World) -> None:
-    if world.run_raw_system(cheat_mode_enabled):
-        world.run_raw_system(disable_cheat_mode)
+    if world.run_simple_system(cheat_mode_enabled):
+        world.run_simple_system(disable_cheat_mode)
     else:
-        world.run_raw_system(enable_cheat_mode)
+        world.run_simple_system(enable_cheat_mode)
 
 
 def close_current_game(
@@ -1301,41 +1175,33 @@ def close_current_game(
     gui: Query[Entity, GameGui],
     queue: CommandQueue,
 ) -> None:
-    """
-    System removing maze with childrens and game graphical user interface.
-    """
-    world.run_raw_system(close_game_menu)
+    """System removing maze with childrens and game graphical user interface."""
+    world.run_simple_system(close_game_menu)
     world.despawn_resource(GameState)
     world.despawn_many(list(gui))
     for maze in mazes:
         queue.queue(hierarchy.despawn_with_children(maze))
-    if world.run_raw_system(cheat_mode_enabled):
-        world.run_raw_system(disable_cheat_mode)
+    if world.run_simple_system(cheat_mode_enabled):
+        world.run_simple_system(disable_cheat_mode)
 
 
 def back_to_main_menu(world: World) -> None:
-    """
-    System closing the current game and opening the main menu.
-    """
-    world.run_raw_system(close_current_game)
-    world.run_raw_system(setup_main_menu)
+    """System closing the current game and opening the main menu."""
+    world.run_simple_system(close_current_game)
+    world.run_simple_system(setup_main_menu)
 
 
 def in_game_menu(menus: Query[GameMenu]) -> bool:
-    """
-    System cheking if the main menu is running.
-    """
+    """System cheking if the main menu is running."""
     return next(menus.__iter__(), None) is not None
 
 
 def toggle_game_menu(world: World) -> None:
-    """
-    System opening or closing the game menu.
-    """
-    if world.run_raw_system(in_game_menu):
-        world.run_raw_system(close_game_menu)
+    """System opening or closing the game menu."""
+    if world.run_simple_system(in_game_menu):
+        world.run_simple_system(close_game_menu)
     else:
-        world.run_raw_system(setup_game_menu)
+        world.run_simple_system(setup_game_menu)
 
 
 def close_game_menu(
@@ -1344,9 +1210,7 @@ def close_game_menu(
     menus: Query[Entity, GameMenu],
     animated: Query[Entity, Pacman | Ghost],
 ) -> None:
-    """
-    System closing the game menu.
-    """
+    """System closing the game menu."""
     world.remove_many(StopAnimation, list(animated))
     for menu in menus:
         hierarchy.despawn_with_children(menu)(world)
@@ -1356,9 +1220,7 @@ def close_game_menu(
 def setup_game_menu(
     world: World, animated: Query[Entity, Pacman | Ghost]
 ) -> None:
-    """
-    System opening the game menu.
-    """
+    """System opening the game menu."""
     world.insert_many(StopAnimation, [(e, StopAnimation()) for e in animated])
     selected = world.run_system(
         spawn_button,
@@ -1394,9 +1256,7 @@ def setup_game_menu(
 
 
 def set_up_instructions(world: World, fonts: Assets[Font]) -> list[Entity]:
-    """
-    System adding instructions to main menu.
-    """
+    """System adding instructions to main menu."""
     text = """Use 'WASD'to move pacman in the maze.
 Collect all pacgums to go to the next level.
 Complete all levels to win the game.
@@ -1454,9 +1314,7 @@ When you eat a super pacgum, you can eat ghost.
 def set_up_highsores(
     world: World, scores: Highscores, fonts: Assets[Font]
 ) -> list[Entity]:
-    """
-    System adding highscores to main menu.
-    """
+    """System adding highscores to main menu."""
     highscores = [(name, value) for name, value in scores.scores.items()]
     highscores.sort(key=lambda x: x[1], reverse=True)
     entities = [
@@ -1505,9 +1363,7 @@ def set_up_highsores(
 
 
 def setup_main_menu(world: World) -> None:
-    """
-    System creating the main menu by adding button, instruction and highscores.
-    """
+    """System creating the main menu by adding button, instruction and highscores."""
     selected = world.run_system(
         spawn_button,
         WindowPositionned(Vector2(0.5), Vector2(-150, -100)),
@@ -1526,8 +1382,8 @@ def setup_main_menu(world: World) -> None:
         )
     )
 
-    entities.extend(world.run_raw_system(set_up_highsores))
-    entities.extend(world.run_raw_system(set_up_instructions))
+    entities.extend(world.run_simple_system(set_up_highsores))
+    entities.extend(world.run_simple_system(set_up_instructions))
 
     world.spawn(
         tuple[MainMenu, Children, CoordBundle],
@@ -1553,9 +1409,7 @@ def get_player_name(events: WindowEvents, name: PlayerName) -> None:
 
 
 def run_game() -> None:
-    """
-    Start with all necessary plugins and system to run the game.
-    """
+    """Start with all necessary plugins and system to run the game."""
     World().with_plugins(graphics_plugins, minimal_plugins).with_systems(
         StartupSchedule,
         Systems(load_config, load_highscores, setup_main_menu)
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5313349f5b4cc18b669748934aeb56b1984240ec 100644 (file)
@@ -0,0 +1 @@
+"""General utilities used mostly in the game engine."""
index 19df3709925666d8ced1ec6ef37615ce04fcc53c..b4c4a621b0c860a3f60b2a6ea32a7431feca3dd1 100644 (file)
@@ -1,5 +1,8 @@
+"""Utilities to handle exceptions in lambas."""
+
 from typing import Never
 
 
 def panic(e: Exception) -> Never:
+    """Raises the exception, in a way that is usable in lambdas."""
     raise e
index 09295418e208eeb7ee56803394db7525b4de68b5..a7d53fabe70a52645ef72361b24d4cb80afb7df2 100644 (file)
@@ -1,5 +1,7 @@
 # ruff: noqa: ANN401
 
+"""Utilities to resolve type aliases and generics."""
+
 import types
 import typing
 from collections.abc import Iterable
@@ -11,7 +13,7 @@ from typing import (
 
 
 def map_type_generics(ty: Any, mapping: dict[Any, type]) -> Any:
-    """Map generics for this type, maybe creating a new type."""
+    """Resolve generics for this type, using the mapping."""
     if ty in mapping:
         return mapping[ty]
     if (orig := get_origin(ty)) is None:
@@ -25,7 +27,7 @@ def map_type_generics(ty: Any, mapping: dict[Any, type]) -> Any:
 
 
 def resolve_type_aliases[T](ty: Any) -> Any:
-    """Resolve any type aliases for a given type, recursively."""
+    """Resolve type aliases for a given type, recursively."""
     while hasattr(ty, "__value__"):
         if not (
             hasattr(ty, "__type_params__")
@@ -41,6 +43,7 @@ def resolve_type_aliases[T](ty: Any) -> Any:
 
 
 def origin_or_cls(ty: Any) -> Any:
+    """Return the base class of ty, without any generics if they exist."""
     res = get_origin(ty)
     if res is None:
         res = ty
index a96bf493ab31c9fc178492e7151220ae14bbbf22..ba48df21c27ae18203cdeefa951c4847bd88c0e0 100644 (file)
-from collections.abc import Callable, Generator
-from typing import cast
-
-type RecursiveTuple[T] = T | tuple[RecursiveTuple[T], ...]
-
-
-def recursive_tuple_map[T, U](
-    tup: RecursiveTuple[T], map: Callable[[T], U]
-) -> RecursiveTuple[U]:
-    if not isinstance(tup, tuple):
-        return map(tup)
-    return tuple(recursive_tuple_map(e, map) for e in tup)
+"""Simple utilities to work with variadic generic tuples efficiently."""
 
+from collections.abc import Callable
 
-def recursive_tuple_create[T, U](
-    tup: T, map: Callable[[T], U | tuple[T, ...]]
-) -> RecursiveTuple[U]:
-    if not isinstance((res := map(tup)), tuple):
-        return res
-    return tuple(
-        cast(RecursiveTuple[U], recursive_tuple_create(tup, map))
-        for tup in res
-    )
-
-
-def recursive_tuple_iter[T](tup: RecursiveTuple[T]) -> Generator[T]:
-    if not isinstance(tup, tuple):
-        yield tup
-    else:
-        for sub in cast(tuple[RecursiveTuple[T], ...], tup):
-            yield from recursive_tuple_iter(sub)
+type RecursiveTuple[T] = T | tuple[RecursiveTuple[T], ...]
 
 
 def fast_tuple_constructor[T, U](
     cbs: tuple[Callable[[U], T], ...],
 ) -> Callable[[U], tuple[T, ...]]:
-    match cbs:
-        case (e1,):
-            return lambda arg: (e1(arg),)
-        case (e1, e2):
-            return lambda arg: (e1(arg), e2(arg))
-        case (e1, e2, e3):
-            return lambda arg: (e1(arg), e2(arg), e3(arg))
-        case (e1, e2, e3, e4):
-            return lambda arg: (e1(arg), e2(arg), e3(arg), e4(arg))
-        case (e1, e2, e3, e4, e5):
-            return lambda arg: (e1(arg), e2(arg), e3(arg), e4(arg), e5(arg))
-        case (e1, e2, e3, e4, e5, e6):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9, e10):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-                e11(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-                e11(arg),
-                e12(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-                e11(arg),
-                e12(arg),
-                e13(arg),
-            )
-        case (e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-                e11(arg),
-                e12(arg),
-                e13(arg),
-                e14(arg),
-            )
-        case (
-            e1,
-            e2,
-            e3,
-            e4,
-            e5,
-            e6,
-            e7,
-            e8,
-            e9,
-            e10,
-            e11,
-            e12,
-            e13,
-            e14,
-            e15,
-        ):
-            return lambda arg: (
-                e1(arg),
-                e2(arg),
-                e3(arg),
-                e4(arg),
-                e5(arg),
-                e6(arg),
-                e7(arg),
-                e8(arg),
-                e9(arg),
-                e10(arg),
-                e11(arg),
-                e12(arg),
-                e13(arg),
-                e14(arg),
-                e15(arg),
-            )
-        case e:
-            return lambda arg: tuple(cb(arg) for cb in e)
+    """Map a tuple of callables to a callable of tuples."""
+    return lambda arg: tuple(cb(arg) for cb in cbs)
 
 
 def composed_by[T, U](
@@ -205,6 +17,7 @@ def composed_by[T, U](
     b: Callable[[T], U],
     op: Callable[[U, U], U],
 ) -> Callable[[T], U]:
+    """Compose two functions together, calling both on the same argument."""
     if a is None:
         return b
     return lambda i: op(a(i), b(i))