From 036c6491f2f48b6e070e28f85108519b38291536 Mon Sep 17 00:00:00 2001 From: = <=> Date: Mon, 12 Oct 2026 14:20:49 +0200 Subject: [PATCH] Gui button system --- src/pacman/ecs/graphics.py | 48 +++++++++++++++---- src/pacman/ecs/gui.py | 96 +++++++++++++++++++++++++++++++++++-- src/pacman/game/__init__.py | 67 +++++++++++--------------- 3 files changed, 162 insertions(+), 49 deletions(-) diff --git a/src/pacman/ecs/graphics.py b/src/pacman/ecs/graphics.py index 887a486..00f684d 100644 --- a/src/pacman/ecs/graphics.py +++ b/src/pacman/ecs/graphics.py @@ -1,8 +1,10 @@ from collections.abc import Iterator from dataclasses import dataclass +from time import time from typing import cast, override import pygame +from pygame import K_RETURN, K_a, K_d, K_s, K_w from pacman.ecs.assets import Asset from pacman.ecs.coord import CoordAbs2D, PropagateTransform @@ -30,6 +32,9 @@ class Window(Resource): clock: pygame.time.Clock +dir_lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3} + + @dataclass class WindowEvents(Resource): events: list[pygame.event.Event] @@ -49,12 +54,22 @@ class WindowEvents(Resource): ) -def init_window(world: World) -> None: - pygame.init() - world.spawn_resource( - Window(pygame.display.set_mode((1600, 1200)), pygame.time.Clock()), - ) - world.spawn_resource(WindowEvents([])) +@dataclass +class Inputs(Resource): + inputs: dict[int, float] + + def get_dir(self) -> int | None: + res = sorted( + ((k, v) for k, v in self.inputs.items() if k in dir_lookup), + key=lambda e: e[1], + reverse=True, + ) + if len(res) == 0: + return None + return dir_lookup[res[0][0]] + + def activate(self) -> bool: + return K_RETURN in self.inputs @dataclass @@ -71,6 +86,15 @@ class Sprite2D(Component, Asset): return Sprite2D(self.surface) +def init_graphics(world: World) -> None: + pygame.init() + world.spawn_resource( + Window(pygame.display.set_mode((1600, 1200)), pygame.time.Clock()), + ) + world.spawn_resource(WindowEvents([])) + world.spawn_resource(Inputs({})) + + def poll_events(events: WindowEvents) -> None: events.events = pygame.event.get() @@ -134,6 +158,14 @@ def update_animations( sprite.surface = anim.curr_frame.surface +def update_inputs(inputs: Inputs, events: WindowEvents) -> None: + for key, state in events.keys_itr(): + if state: + inputs.inputs[key] = time() + else: + del inputs.inputs[key] + + GraphicsSystems = SystemSet("graphics-systems") InputsSystems = SystemSet("inputs-systems") @@ -141,7 +173,7 @@ 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( + world.with_systems(StartupSchedule, init_graphics).with_systems( UpdateSchedule, Systems( insert_animation_sprites, @@ -150,7 +182,7 @@ def graphics_plugins(world: World) -> None: ) .chain() .in_set(PostUpdate, GraphicsSystems), - Systems(poll_events, close_on_close) + Systems(poll_events, close_on_close, update_inputs) .chain() .in_set(PreUpdate, InputsSystems), ).with_plugins(gui_plugins) diff --git a/src/pacman/ecs/gui.py b/src/pacman/ecs/gui.py index ff6e93c..5bd5aeb 100644 --- a/src/pacman/ecs/gui.py +++ b/src/pacman/ecs/gui.py @@ -1,11 +1,21 @@ +from collections.abc import Callable from dataclasses import dataclass, field +from enum import Enum, auto 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.coord import CoordAbs2D, PropagateTransform +from pacman.ecs.graphics import ( + AnimationSet, + GraphicsSystems, + Inputs, + Sprite2D, + WindowEvents, + dir_lookup, +) from pacman.ecs.schedule import StartupSchedule, UpdateSchedule from pacman.ecs.update import PostUpdate from pacman.ecs.world import ( @@ -13,6 +23,7 @@ from pacman.ecs.world import ( Component, Entity, Query, + Resource, System, Systems, World, @@ -44,6 +55,78 @@ class ComputedText(Component): cb: System[str] +class ButtonState(Enum): + unselected = auto() + selected = auto() + pressed = auto() + + +@dataclass +class Button(Component): + cb: System[None] + state: ButtonState = ButtonState.unselected + + +@dataclass +class SelectedButton(Resource): + button: Entity + + +def update_selected_button( + buttons: Query[tuple[Entity, CoordAbs2D], Button], + selected: SelectedButton, + events: WindowEvents, +) -> None: + for key, active in events.keys_itr(): + if not active: + continue + if (direction := dir_lookup.get(key)) is None: + continue + extract: Callable[[CoordAbs2D], float] = ( + (lambda e: e._vec.x) + if direction % 2 == 1 + else (lambda e: e._vec.y) + ) + curr_select = buttons[selected.button][1] + candidates = sorted( + ( + (ent, coord) + for ent, coord in buttons + if ent is selected.button + or extract(curr_select) != extract(coord) + ), + key=lambda e: extract(e[1]), + reverse=direction % 3 != 0, + ) + i = candidates.index((selected.button, curr_select)) + selected.button = candidates[i - 1][0] + + +def update_button_state( + buttons: Query[tuple[Entity, Button]], + selected: SelectedButton, + inputs: Inputs, + world: World, +) -> None: + cbs = [] + for entity, button in buttons: + if entity is not selected.button: + button.state = ButtonState.unselected + continue + if button.state == ButtonState.selected and inputs.activate(): + button.state = ButtonState.pressed + cbs.append(button.cb) + elif not inputs.activate(): + button.state = ButtonState.selected + for cb in cbs: + world.run_raw_system(cb) + + +def update_button_texture(buttons: Query[tuple[Button, AnimationSet]]) -> None: + for button, anim in buttons: + anim.idx = button.state.value - 1 + + def update_computed_text( elems: Query[tuple[TextDisplay, ComputedText]], world: World, @@ -83,8 +166,15 @@ def setup_gui_resources() -> None: def gui_plugins(world: World) -> None: world.with_systems(StartupSchedule, setup_gui_resources).with_systems( UpdateSchedule, - Systems(update_computed_text, update_text_sprites) + Systems( + update_selected_button, + update_button_state, + update_button_texture, + update_computed_text, + update_text_sprites, + ) .chain() .in_set(PostUpdate) - .before(GraphicsSystems), + .before(GraphicsSystems) + .after(PropagateTransform), ) diff --git a/src/pacman/game/__init__.py b/src/pacman/game/__init__.py index 8ab73dc..662c46c 100644 --- a/src/pacman/game/__init__.py +++ b/src/pacman/game/__init__.py @@ -2,12 +2,11 @@ from collections.abc import Callable from dataclasses import dataclass from enum import Enum, auto from random import random, shuffle -from time import time from typing import override from mazegenerator import MazeGenerator # type: ignore[import-untyped] from pydantic import BaseModel -from pygame import K_a, K_d, K_s, K_w, Vector2 +from pygame import Vector2 from pygame.math import clamp from pacman.ecs import minimal_plugins @@ -21,16 +20,21 @@ from pacman.ecs.coord import ( from pacman.ecs.graphics import ( AnimationSet, GraphicsSystems, - InputsSystems, + Inputs, ShouldRender, Sprite2D, - WindowEvents, graphics_plugins, ) -from pacman.ecs.gui import ComputedText, Font, TextDisplay +from pacman.ecs.gui import ( + Button, + ComputedText, + Font, + SelectedButton, + TextDisplay, +) from pacman.ecs.hierarchy import ChildOf, Hierarchy from pacman.ecs.schedule import StartupSchedule, UpdateSchedule -from pacman.ecs.update import PostUpdate, PreUpdate, Update +from pacman.ecs.update import PostUpdate, Update from pacman.ecs.world import ( CommandQueue, Component, @@ -115,22 +119,6 @@ class GameState(Resource): lives: int = 3 -@dataclass -class Inputs(Resource): - inputs: dict[int, float] - - def get_dir(self) -> int | None: - lookup = {K_w: 0, K_d: 1, K_s: 2, K_a: 3} - res = sorted( - ((k, v) for k, v in self.inputs.items() if k in lookup), - key=lambda e: e[1], - reverse=True, - ) - if len(res) == 0: - return None - return lookup[res[0][0]] - - class PacGum(Component): pass @@ -240,9 +228,6 @@ def create_maze( return inner - - - def spawn_superpacgums( world: World, mazes: Query[Maze], assets: Assets[Sprite2D] ) -> None: @@ -724,14 +709,6 @@ def setup_new_maze(world: World) -> None: world.run_raw_system(sys) -def update_inputs(inputs: Inputs, events: WindowEvents) -> None: - for key, state in events.keys_itr(): - if state: - inputs.inputs[key] = time() - else: - del inputs.inputs[key] - - def level_is_finished( pacgums: Query[SuperPacGum | PacGum], _: Not[GameIsOver] ) -> bool: @@ -788,7 +765,6 @@ def initial_pause_done(timer: InitialPauseTimer, config: Config) -> bool: def setup_resources(world: World) -> None: - world.spawn_resource(Inputs({})) world.spawn_resource(GameState()) world.spawn_resource(Config()) @@ -802,13 +778,25 @@ def print_lives(state: GameState) -> str: def print_timer(state: GameState, config: Config) -> str: - time = config.level_timer - state.timer + time = config.level_timer - state.timer return f"{time // 60}" +def print_a() -> None: + print("a") + + +def print_b() -> None: + print("b") + + +def print_c() -> None: + print("c") + + def setup_text(world: World, fonts: Assets[Font]) -> None: - world.spawn_many( - tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay], + spawned = world.spawn_many( + tuple[CoordBundle, ShouldRender, ComputedText, TextDisplay, Button], [ ( CoordBundle(depth=-20), @@ -818,6 +806,7 @@ def setup_text(world: World, fonts: Assets[Font]) -> None: "", fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"), ), + Button(print_a), ), ( CoordBundle(y=100, depth=-20), @@ -827,6 +816,7 @@ def setup_text(world: World, fonts: Assets[Font]) -> None: "", fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"), ), + Button(print_b), ), ( CoordBundle(y=200, depth=-20), @@ -836,9 +826,11 @@ def setup_text(world: World, fonts: Assets[Font]) -> None: "", fonts.load("./asset/fonts/JetBrainsMonoNL-Regular.ttf:30"), ), + Button(print_c), ), ], ) + world.spawn_resource(SelectedButton(spawned[0])) def run_game() -> None: @@ -860,7 +852,6 @@ def run_game() -> None: .chain() .run_if(initial_pause_done) .in_set(Update), - Systems(update_inputs).in_set(PreUpdate).after(InputsSystems), Systems(update_pacman_anim, update_ghost_anim) .before(GraphicsSystems) .in_set(PostUpdate), -- 2.53.0