From bfabba3d821372874cf4cb6a54ef90a9c17b885c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 10:56:55 +0200 Subject: [PATCH] refactored state -> screen --- g.py | 4 +- game/screens/__init__.py | 99 ++++++++++++++++++++++++ game/{states => screens}/game_screens.py | 11 +-- game/{states => screens}/menu_screens.py | 20 ++--- game/{states => screens}/menus.py | 18 ++--- game/state.py | 99 ------------------------ main.py | 8 +- 7 files changed, 130 insertions(+), 129 deletions(-) create mode 100644 game/screens/__init__.py rename game/{states => screens}/game_screens.py (93%) rename game/{states => screens}/menu_screens.py (59%) rename game/{states => screens}/menus.py (85%) delete mode 100644 game/state.py diff --git a/g.py b/g.py index 52bf2ab..2d12b10 100644 --- a/g.py +++ b/g.py @@ -6,7 +6,7 @@ import tcod.console import tcod.context import tcod.ecs -import game.state +from game.screens import Screen from game.components import Position context: tcod.context.Context @@ -20,7 +20,7 @@ world_map: tcod.map.Map world_center: tuple[int,int] = Position(50, 50) -states: list[game.state.State] = [] +screens: list[Screen] = [] """A stack of states with the last item being the active state.""" console: tcod.console.Console diff --git a/game/screens/__init__.py b/game/screens/__init__.py new file mode 100644 index 0000000..ee16cc0 --- /dev/null +++ b/game/screens/__init__.py @@ -0,0 +1,99 @@ +"""Package for game state stuff.""" + +from __future__ import annotations + +from typing import Protocol, TypeAlias + +import attrs +import tcod.console +import tcod.event + +import g + + +class Screen(Protocol): + """An abstract game screen.""" + + __slots__ = () + + def on_event(self, event: tcod.event.Event) -> ScreenResult: + """Called on events.""" + + def on_draw(self, console: tcod.console.Console) -> None: + """Called when the screen is being drawn.""" + + +@attrs.define() +class Push: + """Push a new screen on top of the stack.""" + + screen: Screen + + +@attrs.define() +class Pop: + """Remove the current screen from the stack.""" + + +@attrs.define() +class Reset: + """Replace the entire stack with a new screen.""" + + screen: Screen + + +ScreenResult: TypeAlias = "Push | Pop | Reset | None" +"""Union of screen results.""" + + +def main_draw() -> None: + """Render and present the active screen.""" + if not g.screens: + return + g.console.clear() + g.screens[-1].on_draw(g.console) + g.context.present(g.console) + + +def apply_state_result(result: ScreenResult) -> None: + """Apply a StateResult to `g.states`.""" + match result: + case Push(screen=screen): + g.screens.append(screen) + case Pop(): + g.screens.pop() + case Reset(screen=screen): + while g.screens: + apply_state_result(Pop()) + apply_state_result(Push(screen)) + case None: + pass + case _: + raise TypeError(result) + + +def main_loop() -> None: + """Run the active state forever.""" + while g.screens: + main_draw() + for event in tcod.event.wait(): + tile_event = g.context.convert_event(event) + if g.screens: + apply_state_result(g.screens[-1].on_event(tile_event)) + + +def get_previous_screen(screen: Screen) -> Screen | None: + """Return the state before `state` in the stack if it exists.""" + current_index = next(index for index, value in enumerate(g.screens) if value is screen) + return g.screens[current_index - 1] if current_index > 0 else None + + +def draw_previous_screens(screen: Screen, console: tcod.console.Console, dim: bool = True) -> None: + """Draw previous states, optionally dimming all but the active state.""" + prev_screen = get_previous_screen(screen) + if prev_screen is None: + return + prev_screen.on_draw(console) + if dim and screen is g.screens[-1]: + console.rgb["fg"] //= 4 + console.rgb["bg"] //= 4 diff --git a/game/states/game_screens.py b/game/screens/game_screens.py similarity index 93% rename from game/states/game_screens.py rename to game/screens/game_screens.py index 583e431..504ccec 100644 --- a/game/states/game_screens.py +++ b/game/screens/game_screens.py @@ -1,3 +1,5 @@ +"""All states the are used in-game""" + from __future__ import annotations import attrs @@ -7,20 +9,19 @@ import tcod.event from tcod.event import KeySym import g -import game.world_tools from game.components import Gold, Graphic, Position from game.constants import DIRECTION_KEYS, ACTION_KEYS -from game.state import Push, State, StateResult +from game.screens import Push, Screen, ScreenResult from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR -from game.states import menu_screens +from game.screens import menu_screens @attrs.define() -class MainScreen(State): +class MainScreen(Screen): """Primary in-game state.""" - def on_event(self, event: tcod.event.Event) -> StateResult: + def on_event(self, event: tcod.event.Event) -> ScreenResult: """Handle events for the in-game state.""" (player,) = g.world.Q.all_of(tags=[IsPlayer]) match event: diff --git a/game/states/menu_screens.py b/game/screens/menu_screens.py similarity index 59% rename from game/states/menu_screens.py rename to game/screens/menu_screens.py index 320323f..bfa4346 100644 --- a/game/states/menu_screens.py +++ b/game/screens/menu_screens.py @@ -3,23 +3,23 @@ from __future__ import annotations import g -import game.states.menus +import game.screens.menus import game.world_tools -from game.state import Reset, StateResult -from game.states.game_screens import MainScreen +from game.screens import Reset, ScreenResult +from game.screens.game_screens import MainScreen -class MainMenu(game.states.menus.ListMenu): +class MainMenu(game.screens.menus.ListMenu): """Main/escape menu.""" __slots__ = () def __init__(self) -> None: """Initialize the main menu.""" items = [ - game.states.menus.SelectItem("New game", self.new_game), - game.states.menus.SelectItem("Quit", self.quit), + game.screens.menus.SelectItem("New game", self.new_game), + game.screens.menus.SelectItem("Quit", self.quit), ] if hasattr(g, "world"): - items.insert(0, game.states.menus.SelectItem("Continue", self.continue_)) + items.insert(0, game.screens.menus.SelectItem("Continue", self.continue_)) super().__init__( items=tuple(items), @@ -29,17 +29,17 @@ class MainMenu(game.states.menus.ListMenu): ) @staticmethod - def continue_() -> StateResult: + def continue_() -> ScreenResult: """Return to the game.""" return Reset(MainScreen()) @staticmethod - def new_game() -> StateResult: + def new_game() -> ScreenResult: """Begin a new game.""" g.world = game.world_tools.new_world() return Reset(MainScreen()) @staticmethod - def quit() -> StateResult: + def quit() -> ScreenResult: """Close the program.""" raise SystemExit() diff --git a/game/states/menus.py b/game/screens/menus.py similarity index 85% rename from game/states/menus.py rename to game/screens/menus.py index c9e6219..7927cc5 100644 --- a/game/states/menus.py +++ b/game/screens/menus.py @@ -11,7 +11,7 @@ import tcod.event from tcod.event import KeySym from game.constants import DIRECTION_KEYS -from game.state import Pop, State, StateResult, draw_previous_states +from game.screens import Pop, Screen, ScreenResult, draw_previous_screens class MenuItem(Protocol): @@ -19,7 +19,7 @@ class MenuItem(Protocol): __slots__ = () - def on_event(self, event: tcod.event.Event) -> StateResult: + def on_event(self, event: tcod.event.Event) -> ScreenResult: """Handle events passed to the menu item.""" def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: @@ -31,9 +31,9 @@ class SelectItem(MenuItem): """Clickable menu item.""" label: str - callback: Callable[[], StateResult] + callback: Callable[[], ScreenResult] - def on_event(self, event: tcod.event.Event) -> StateResult: + def on_event(self, event: tcod.event.Event) -> ScreenResult: """Handle events selecting this item.""" match event: case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}: @@ -49,7 +49,7 @@ class SelectItem(MenuItem): @attrs.define() -class ListMenu(State): +class ListMenu(Screen): """Simple list menu state.""" items: tuple[MenuItem, ...] @@ -57,7 +57,7 @@ class ListMenu(State): x: int = 0 y: int = 0 - def on_event(self, event: tcod.event.Event) -> StateResult: + def on_event(self, event: tcod.event.Event) -> ScreenResult: """Handle events for menus.""" match event: case tcod.event.Quit(): @@ -83,18 +83,18 @@ class ListMenu(State): case _: return self.activate_selected(event) - def activate_selected(self, event: tcod.event.Event) -> StateResult: + def activate_selected(self, event: tcod.event.Event) -> ScreenResult: """Call the selected menu items callback.""" if self.selected is not None: return self.items[self.selected].on_event(event) return None - def on_cancel(self) -> StateResult: + def on_cancel(self) -> ScreenResult: """Handle escape or right click being pressed on menus.""" return Pop() def on_draw(self, console: tcod.console.Console) -> None: """Render the menu.""" - draw_previous_states(self, console) + draw_previous_screens(self, console) for i, item in enumerate(self.items): item.on_draw(console, x=self.x, y=self.y + i, highlight=i == self.selected) diff --git a/game/state.py b/game/state.py deleted file mode 100644 index 4497f0d..0000000 --- a/game/state.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Base classes for states.""" - -from __future__ import annotations - -from typing import Protocol, TypeAlias - -import attrs -import tcod.console -import tcod.event - -import g - - -class State(Protocol): - """An abstract game state.""" - - __slots__ = () - - def on_event(self, event: tcod.event.Event) -> StateResult: - """Called on events.""" - - def on_draw(self, console: tcod.console.Console) -> None: - """Called when the state is being drawn.""" - - -@attrs.define() -class Push: - """Push a new state on top of the stack.""" - - state: State - - -@attrs.define() -class Pop: - """Remove the current state from the stack.""" - - -@attrs.define() -class Reset: - """Replace the entire stack with a new state.""" - - state: State - - -StateResult: TypeAlias = "Push | Pop | Reset | None" -"""Union of state results.""" - - -def main_draw() -> None: - """Render and present the active state.""" - if not g.states: - return - g.console.clear() - g.states[-1].on_draw(g.console) - g.context.present(g.console) - - -def apply_state_result(result: StateResult) -> None: - """Apply a StateResult to `g.states`.""" - match result: - case Push(state=state): - g.states.append(state) - case Pop(): - g.states.pop() - case Reset(state=state): - while g.states: - apply_state_result(Pop()) - apply_state_result(Push(state)) - case None: - pass - case _: - raise TypeError(result) - - -def main_loop() -> None: - """Run the active state forever.""" - while g.states: - main_draw() - for event in tcod.event.wait(): - tile_event = g.context.convert_event(event) - if g.states: - apply_state_result(g.states[-1].on_event(tile_event)) - - -def get_previous_state(state: State) -> State | None: - """Return the state before `state` in the stack if it exists.""" - current_index = next(index for index, value in enumerate(g.states) if value is state) - return g.states[current_index - 1] if current_index > 0 else None - - -def draw_previous_states(state: State, console: tcod.console.Console, dim: bool = True) -> None: - """Draw previous states, optionally dimming all but the active state.""" - prev_state = get_previous_state(state) - if prev_state is None: - return - prev_state.on_draw(console) - if dim and state is g.states[-1]: - console.rgb["fg"] //= 4 - console.rgb["bg"] //= 4 diff --git a/main.py b/main.py index 582f885..226a327 100755 --- a/main.py +++ b/main.py @@ -9,8 +9,8 @@ import tcod.tileset import g -import game.state -from game.states.menu_screens import MainMenu +import game.screens +from game.screens.menu_screens import MainMenu def main() -> None: """Entry point function.""" @@ -19,10 +19,10 @@ def main() -> None: ) tcod.tileset.procedural_block_elements(tileset=tileset) - g.states = [MainMenu()] + g.screens = [MainMenu()] g.console = tcod.console.Console(80, 50) with tcod.context.new(console=g.console, tileset=tileset) as g.context: - game.state.main_loop() + game.screens.main_loop() if __name__ == "__main__":