]> Untitled Git - axy/ft/pacman.git/commitdiff
Simple text gui stuff
author= <=>
Mon, 12 Oct 2026 10:07:03 +0000 (12:07 +0200)
committer= <=>
Mon, 12 Oct 2026 10:07:03 +0000 (12:07 +0200)
asset/fonts/JetBrainsMonoNL-Regular.ttf [new file with mode: 0644]
src/pacman/ecs/coord.py
src/pacman/ecs/graphics.py
src/pacman/ecs/gui.py [new file with mode: 0644]
src/pacman/ecs/world.py
src/pacman/game/__init__.py

diff --git a/asset/fonts/JetBrainsMonoNL-Regular.ttf b/asset/fonts/JetBrainsMonoNL-Regular.ttf
new file mode 100644 (file)
index 0000000..70d2ec9
Binary files /dev/null and b/asset/fonts/JetBrainsMonoNL-Regular.ttf differ
index cc3547b45fff32944f3c1f3403a198e659eb3e8b..dc8e6a3d00e163c97ddab49ac254c66fa084cbab 100644 (file)
@@ -1,9 +1,12 @@
+from collections.abc import Iterator
+from typing import override
+
 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
+from pacman.ecs.world import Bundle, Component, Query, Systems, World
 
 
 class CoordAbs2D(Component):
@@ -17,6 +20,22 @@ class Coord2D(Vector3, Component):
     pass
 
 
+class CoordBundle(Bundle):
+    def __init__(self, x: float = 0, y: float = 0, depth: float = -1) -> None:
+        self.coord = Coord2D(x, y, depth)
+        self.coord_abs = CoordAbs2D()
+
+    @override
+    def components_iter(self) -> Iterator[Component]:
+        return iter((self.coord, self.coord_abs))
+
+    @classmethod
+    @override
+    def components_type_iter(cls, ty: type) -> Iterator[type[Component]]:
+        del ty
+        return iter((Coord2D, CoordAbs2D))
+
+
 def update_coord(entities: Query[tuple[CoordAbs2D, Coord2D]]) -> None:
     for abs_coord, coord in entities:
         abs_coord._vec = coord.xyz
index de2416b981a985994bcb4d4da69ada226600fb87..887a486495ea941e96edda5b3de555ce32dd5d0f 100644 (file)
@@ -139,6 +139,8 @@ InputsSystems = SystemSet("inputs-systems")
 
 
 def graphics_plugins(world: World) -> None:
+    from pacman.ecs.gui import gui_plugins
+
     world.with_systems(StartupSchedule, init_window).with_systems(
         UpdateSchedule,
         Systems(
@@ -151,4 +153,4 @@ def graphics_plugins(world: World) -> None:
         Systems(poll_events, close_on_close)
         .chain()
         .in_set(PreUpdate, InputsSystems),
-    )
+    ).with_plugins(gui_plugins)
diff --git a/src/pacman/ecs/gui.py b/src/pacman/ecs/gui.py
new file mode 100644 (file)
index 0000000..ff6e93c
--- /dev/null
@@ -0,0 +1,90 @@
+from dataclasses import dataclass, field
+from typing import override
+
+import pygame
+from pygame import Color
+
+from pacman.ecs.assets import Asset
+from pacman.ecs.graphics import GraphicsSystems, Sprite2D
+from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
+from pacman.ecs.update import PostUpdate
+from pacman.ecs.world import (
+    CommandQueue,
+    Component,
+    Entity,
+    Query,
+    System,
+    Systems,
+    World,
+)
+
+
+@dataclass
+class Font(Asset, Component):
+    font: pygame.font.Font
+
+    @override
+    @classmethod
+    def load(cls, ty: type, path: str) -> "Font":
+        del ty
+        path, size = path.split(":")
+        return Font(pygame.font.Font(path, int(size)))
+
+
+@dataclass
+class TextDisplay(Component):
+    text: str
+    font: Font
+    color: Color = field(default_factory=lambda: Color(255, 255, 255))
+    _previously_rendered: tuple[str, Font, Color] | None = None
+
+
+@dataclass
+class ComputedText(Component):
+    cb: System[str]
+
+
+def update_computed_text(
+    elems: Query[tuple[TextDisplay, ComputedText]],
+    world: World,
+) -> None:
+    for display, compute in elems:
+        display.text = world.run_raw_system(compute.cb)
+
+
+def update_text_sprites(
+    elems: Query[tuple[TextDisplay, Sprite2D | Entity]], queue: CommandQueue
+) -> None:
+    inserts = []
+    for display, e in elems:
+        if (
+            display.text,
+            display.font,
+            display.color,
+        ) == display._previously_rendered:
+            continue
+        surface = display.font.font.render(display.text, True, display.color)
+        if isinstance(e, Sprite2D):
+            e.surface = surface
+        else:
+            inserts.append((e, Sprite2D(surface)))
+        display._previously_rendered = (
+            display.text,
+            display.font,
+            display.color,
+        )
+    queue.queue(lambda world: world.insert_many(Sprite2D, inserts))
+
+
+def setup_gui_resources() -> None:
+    pygame.font.init()
+
+
+def gui_plugins(world: World) -> None:
+    world.with_systems(StartupSchedule, setup_gui_resources).with_systems(
+        UpdateSchedule,
+        Systems(update_computed_text, update_text_sprites)
+        .chain()
+        .in_set(PostUpdate)
+        .before(GraphicsSystems),
+    )
index 9d0135f79a0c8778e089dffb73312fc4c3d219ff..2dc675799402c6893844e22608bbfce7865dca9a 100644 (file)
@@ -772,7 +772,7 @@ class World(FromWorld):
         def remove(query: Query[Entity, Ty], world: World) -> None:
             world.remove_many(ty, list(query))
 
-        self.add_raw_system(remove)()
+        self.run_raw_system(remove)
         return self.spawn(ty, comp)
 
     def spawn_many[T: Components](
@@ -902,6 +902,21 @@ class World(FromWorld):
 
         return inner
 
+    def run_system[T, *P](
+        self,
+        system: System[Callable[[*P], T]],
+        *args: *P,
+        default: Callable[[*P], T] = _system_failed_va,
+    ) -> T:
+        return self.add_system(system, default)(*args)
+
+    def run_raw_system[T](
+        self,
+        system: System[T],
+        default: Callable[[], T] = _system_failed,
+    ) -> T:
+        return self.add_raw_system(system, default)()
+
     @classmethod
     @override
     def extract_from_world(
index aaf4e3273a53e9f977397d3d2b646eab6728eeca..59805a27c4b8123e8a2d063a186d6df0f6a1920c 100644 (file)
@@ -12,7 +12,12 @@ from pygame.math import clamp
 
 from pacman.ecs import minimal_plugins
 from pacman.ecs.assets import Assets
-from pacman.ecs.coord import Coord2D, CoordAbs2D, PropagateTransform
+from pacman.ecs.coord import (
+    Coord2D,
+    CoordAbs2D,
+    CoordBundle,
+    PropagateTransform,
+)
 from pacman.ecs.graphics import (
     AnimationSet,
     GraphicsSystems,
@@ -22,6 +27,7 @@ from pacman.ecs.graphics import (
     WindowEvents,
     graphics_plugins,
 )
+from pacman.ecs.gui import ComputedText, Font, TextDisplay
 from pacman.ecs.hierarchy import ChildOf, Hierarchy
 from pacman.ecs.schedule import StartupSchedule, UpdateSchedule
 from pacman.ecs.update import PostUpdate, PreUpdate, Update
@@ -209,24 +215,23 @@ def create_maze(
     def inner(maze: MazeGenerator) -> Entity:
         root_maze = Maze(len(maze.maze[0]), len(maze.maze))
         root = world.spawn(
-            tuple[Maze, Coord2D, CoordAbs2D],
-            (root_maze, Coord2D(), CoordAbs2D()),
+            tuple[Maze, CoordBundle],
+            (root_maze, CoordBundle()),
         )
-        cell_dims = Coord2D(64, 64, 0)
+        cell_dims = Vector2(64, 64)
         cells = {
             (x, y): (
                 ChildOf(root),
-                Coord2D(Coord2D(x, y, 0).elementwise() * cell_dims),
+                CoordBundle(*(Vector2(x, y).elementwise() * cell_dims).xy),
                 Cell(cell, (x, y)),
                 ShouldRender(),
-                CoordAbs2D(),
                 assets.load(f"./asset/walls/{cell}.png"),
             )
             for y, line in enumerate(maze.maze)
             for x, cell in enumerate(line)
         }
         spawned_cells = world.spawn_many(
-            tuple[ChildOf, Coord2D, Cell, ShouldRender, CoordAbs2D, Sprite2D],
+            tuple[ChildOf, CoordBundle, Cell, ShouldRender, Sprite2D],
             list(cells.values()),
         )
         root_maze._cells = dict(zip(cells.keys(), spawned_cells, strict=True))
@@ -250,17 +255,14 @@ def spawn_superpacgums(
             pacgums.append(
                 (
                     ChildOf(cell),
-                    Coord2D(0, 0, -1),
-                    CoordAbs2D(),
+                    CoordBundle(),
                     assets.load("./asset/misc/superpacgum.png"),
                     ShouldRender(),
                     SuperPacGum(),
                 )
             )
     world.spawn_many(
-        tuple[
-            ChildOf, Coord2D, CoordAbs2D, Sprite2D, ShouldRender, SuperPacGum
-        ],
+        tuple[ChildOf, CoordBundle, Sprite2D, ShouldRender, SuperPacGum],
         pacgums,
     )
 
@@ -283,21 +285,20 @@ def spawn_pacgums(
         pacgums.append(
             (
                 ChildOf(ent),
-                Coord2D(0, 0, -1),
-                CoordAbs2D(),
+                CoordBundle(),
                 assets.load("./asset/misc/pacgum.png"),
                 ShouldRender(),
                 PacGum(),
             )
         )
     world.spawn_many(
-        tuple[ChildOf, Coord2D, CoordAbs2D, Sprite2D, ShouldRender, PacGum],
+        tuple[ChildOf, CoordBundle, Sprite2D, ShouldRender, PacGum],
         pacgums,
     )
 
 
 def setup_maze(world: World) -> None:
-    world.add_system(create_maze)(MazeGenerator((14, 14)))
+    world.run_system(create_maze, MazeGenerator((14, 14)))
 
 
 def move_in_motion(
@@ -384,8 +385,7 @@ def spawn_pacman(
     pos = home_cell.grid_pos
     world.spawn(
         tuple[
-            Coord2D,
-            CoordAbs2D,
+            CoordBundle,
             AnimationSet,
             Pacman,
             ShouldRender,
@@ -395,8 +395,7 @@ def spawn_pacman(
             ChildOf,
         ],
         (
-            Coord2D(pos[0] * 64, pos[1] * 64, -10),
-            CoordAbs2D(),
+            CoordBundle(pos[0] * 64, pos[1] * 64, -10),
             AnimationSet(
                 [
                     [
@@ -430,8 +429,7 @@ def spawn_ghosts(
     maze, maze_ent = maze_t
     world.spawn_many(
         tuple[
-            Coord2D,
-            CoordAbs2D,
+            CoordBundle,
             AnimationSet,
             ShouldRender,
             InMotion,
@@ -444,8 +442,7 @@ def spawn_ghosts(
         ],
         [
             (
-                Coord2D(pos[0] * 64, pos[1] * 64, -10),
-                CoordAbs2D(),
+                CoordBundle(pos[0] * 64, pos[1] * 64, -10),
                 AnimationSet(
                     [
                         [
@@ -591,9 +588,9 @@ def collide_ghost(
                 continue
             match status.state:
                 case GhostState.wander | GhostState.chase:
-                    world.add_raw_system(respawn_pacman)()
-                    world.add_raw_system(respawn_ghost)()
-                    world.add_raw_system(start_pause_timer)()
+                    world.run_raw_system(respawn_pacman)
+                    world.run_raw_system(respawn_ghost)
+                    world.run_raw_system(start_pause_timer)
                     state.lives -= 1
                     break
 
@@ -721,7 +718,7 @@ def setup_new_maze(world: World) -> None:
         spawn_ghosts,
         start_pause_timer,
     ):
-        world.add_raw_system(sys)()
+        world.run_raw_system(sys)
 
 
 def update_inputs(inputs: Inputs, events: WindowEvents) -> None:
@@ -793,10 +790,44 @@ def setup_resources(world: World) -> None:
     world.spawn_resource(Config())
 
 
+def print_score(state: GameState) -> str:
+    return f"Score: {state.score}"
+
+
+def print_lives(state: GameState) -> str:
+    return f"Lives: {state.lives}"
+
+
+def setup_text(world: World, fonts: Assets[Font]) -> None:
+    world.spawn_many(
+        tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay],
+        [
+            (
+                CoordBundle(depth=-20),
+                ShouldRender(),
+                ComputedText(print_score),
+                TextDisplay(
+                    "",
+                    fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
+                ),
+            ),
+            (
+                CoordBundle(y=100, depth=-20),
+                ShouldRender(),
+                ComputedText(print_lives),
+                TextDisplay(
+                    "",
+                    fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"),
+                ),
+            ),
+        ],
+    )
+
+
 def run_game() -> None:
     World().with_plugins(graphics_plugins, minimal_plugins).with_systems(
         StartupSchedule,
-        Systems(setup_resources, setup_new_maze).chain(),
+        Systems(setup_resources, setup_new_maze, setup_text).chain(),
     ).with_systems(
         UpdateSchedule,
         Systems(