refactored state -> screen

This commit is contained in:
Lukas Nöllemeyer 2024-08-17 10:56:55 +02:00
parent 2560a4dcd9
commit bfabba3d82
7 changed files with 130 additions and 129 deletions

4
g.py
View file

@ -6,7 +6,7 @@ import tcod.console
import tcod.context import tcod.context
import tcod.ecs import tcod.ecs
import game.state from game.screens import Screen
from game.components import Position from game.components import Position
context: tcod.context.Context context: tcod.context.Context
@ -20,7 +20,7 @@ world_map: tcod.map.Map
world_center: tuple[int,int] = Position(50, 50) 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.""" """A stack of states with the last item being the active state."""
console: tcod.console.Console console: tcod.console.Console

99
game/screens/__init__.py Normal file
View file

@ -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

View file

@ -1,3 +1,5 @@
"""All states the are used in-game"""
from __future__ import annotations from __future__ import annotations
import attrs import attrs
@ -7,20 +9,19 @@ import tcod.event
from tcod.event import KeySym from tcod.event import KeySym
import g import g
import game.world_tools
from game.components import Gold, Graphic, Position from game.components import Gold, Graphic, Position
from game.constants import DIRECTION_KEYS, ACTION_KEYS 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.tags import IsItem, IsPlayer, IsDoor, IsActor
from game.constants import WALL_CHAR from game.constants import WALL_CHAR
from game.states import menu_screens from game.screens import menu_screens
@attrs.define() @attrs.define()
class MainScreen(State): class MainScreen(Screen):
"""Primary in-game state.""" """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.""" """Handle events for the in-game state."""
(player,) = g.world.Q.all_of(tags=[IsPlayer]) (player,) = g.world.Q.all_of(tags=[IsPlayer])
match event: match event:

View file

@ -3,23 +3,23 @@
from __future__ import annotations from __future__ import annotations
import g import g
import game.states.menus import game.screens.menus
import game.world_tools import game.world_tools
from game.state import Reset, StateResult from game.screens import Reset, ScreenResult
from game.states.game_screens import MainScreen from game.screens.game_screens import MainScreen
class MainMenu(game.states.menus.ListMenu): class MainMenu(game.screens.menus.ListMenu):
"""Main/escape menu.""" """Main/escape menu."""
__slots__ = () __slots__ = ()
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the main menu.""" """Initialize the main menu."""
items = [ items = [
game.states.menus.SelectItem("New game", self.new_game), game.screens.menus.SelectItem("New game", self.new_game),
game.states.menus.SelectItem("Quit", self.quit), game.screens.menus.SelectItem("Quit", self.quit),
] ]
if hasattr(g, "world"): 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__( super().__init__(
items=tuple(items), items=tuple(items),
@ -29,17 +29,17 @@ class MainMenu(game.states.menus.ListMenu):
) )
@staticmethod @staticmethod
def continue_() -> StateResult: def continue_() -> ScreenResult:
"""Return to the game.""" """Return to the game."""
return Reset(MainScreen()) return Reset(MainScreen())
@staticmethod @staticmethod
def new_game() -> StateResult: def new_game() -> ScreenResult:
"""Begin a new game.""" """Begin a new game."""
g.world = game.world_tools.new_world() g.world = game.world_tools.new_world()
return Reset(MainScreen()) return Reset(MainScreen())
@staticmethod @staticmethod
def quit() -> StateResult: def quit() -> ScreenResult:
"""Close the program.""" """Close the program."""
raise SystemExit() raise SystemExit()

View file

@ -11,7 +11,7 @@ import tcod.event
from tcod.event import KeySym from tcod.event import KeySym
from game.constants import DIRECTION_KEYS 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): class MenuItem(Protocol):
@ -19,7 +19,7 @@ class MenuItem(Protocol):
__slots__ = () __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.""" """Handle events passed to the menu item."""
def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: 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.""" """Clickable menu item."""
label: str 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.""" """Handle events selecting this item."""
match event: match event:
case tcod.event.KeyDown(sym=sym) if sym in {KeySym.RETURN, KeySym.RETURN2, KeySym.KP_ENTER}: 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() @attrs.define()
class ListMenu(State): class ListMenu(Screen):
"""Simple list menu state.""" """Simple list menu state."""
items: tuple[MenuItem, ...] items: tuple[MenuItem, ...]
@ -57,7 +57,7 @@ class ListMenu(State):
x: int = 0 x: int = 0
y: 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.""" """Handle events for menus."""
match event: match event:
case tcod.event.Quit(): case tcod.event.Quit():
@ -83,18 +83,18 @@ class ListMenu(State):
case _: case _:
return self.activate_selected(event) 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.""" """Call the selected menu items callback."""
if self.selected is not None: if self.selected is not None:
return self.items[self.selected].on_event(event) return self.items[self.selected].on_event(event)
return None return None
def on_cancel(self) -> StateResult: def on_cancel(self) -> ScreenResult:
"""Handle escape or right click being pressed on menus.""" """Handle escape or right click being pressed on menus."""
return Pop() return Pop()
def on_draw(self, console: tcod.console.Console) -> None: def on_draw(self, console: tcod.console.Console) -> None:
"""Render the menu.""" """Render the menu."""
draw_previous_states(self, console) draw_previous_screens(self, console)
for i, item in enumerate(self.items): for i, item in enumerate(self.items):
item.on_draw(console, x=self.x, y=self.y + i, highlight=i == self.selected) item.on_draw(console, x=self.x, y=self.y + i, highlight=i == self.selected)

View file

@ -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

View file

@ -9,8 +9,8 @@ import tcod.tileset
import g import g
import game.state import game.screens
from game.states.menu_screens import MainMenu from game.screens.menu_screens import MainMenu
def main() -> None: def main() -> None:
"""Entry point function.""" """Entry point function."""
@ -19,10 +19,10 @@ def main() -> None:
) )
tcod.tileset.procedural_block_elements(tileset=tileset) tcod.tileset.procedural_block_elements(tileset=tileset)
g.states = [MainMenu()] g.screens = [MainMenu()]
g.console = tcod.console.Console(80, 50) g.console = tcod.console.Console(80, 50)
with tcod.context.new(console=g.console, tileset=tileset) as g.context: with tcod.context.new(console=g.console, tileset=tileset) as g.context:
game.state.main_loop() game.screens.main_loop()
if __name__ == "__main__": if __name__ == "__main__":