+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):
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
--- /dev/null
+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),
+ )
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,
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
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))
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,
)
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(
pos = home_cell.grid_pos
world.spawn(
tuple[
- Coord2D,
- CoordAbs2D,
+ CoordBundle,
AnimationSet,
Pacman,
ShouldRender,
ChildOf,
],
(
- Coord2D(pos[0] * 64, pos[1] * 64, -10),
- CoordAbs2D(),
+ CoordBundle(pos[0] * 64, pos[1] * 64, -10),
AnimationSet(
[
[
maze, maze_ent = maze_t
world.spawn_many(
tuple[
- Coord2D,
- CoordAbs2D,
+ CoordBundle,
AnimationSet,
ShouldRender,
InMotion,
],
[
(
- Coord2D(pos[0] * 64, pos[1] * 64, -10),
- CoordAbs2D(),
+ CoordBundle(pos[0] * 64, pos[1] * 64, -10),
AnimationSet(
[
[
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
spawn_ghosts,
start_pause_timer,
):
- world.add_raw_system(sys)()
+ world.run_raw_system(sys)
def update_inputs(inputs: Inputs, events: WindowEvents) -> 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(