diff --git a/g.py b/g.py index e30e095..52bf2ab 100644 --- a/g.py +++ b/g.py @@ -6,7 +6,7 @@ import tcod.console import tcod.context import tcod.ecs -from game.screens import Screen +import game.state from game.components import Position context: tcod.context.Context @@ -15,9 +15,12 @@ context: tcod.context.Context world: tcod.ecs.Registry """The active ECS registry and current session.""" +world_map: tcod.map.Map +"""Wall Map of current World""" + world_center: tuple[int,int] = Position(50, 50) -screens: list[Screen] = [] +states: list[game.state.State] = [] """A stack of states with the last item being the active state.""" console: tcod.console.Console diff --git a/game/screens/menus.py b/game/menus.py similarity index 85% rename from game/screens/menus.py rename to game/menus.py index 7927cc5..7c1bb37 100644 --- a/game/screens/menus.py +++ b/game/menus.py @@ -10,8 +10,9 @@ import tcod.console import tcod.event from tcod.event import KeySym +import game.state_tools from game.constants import DIRECTION_KEYS -from game.screens import Pop, Screen, ScreenResult, draw_previous_screens +from game.state import Pop, State, StateResult class MenuItem(Protocol): @@ -19,7 +20,7 @@ class MenuItem(Protocol): __slots__ = () - def on_event(self, event: tcod.event.Event) -> ScreenResult: + def on_event(self, event: tcod.event.Event) -> StateResult: """Handle events passed to the menu item.""" def on_draw(self, console: tcod.console.Console, x: int, y: int, highlight: bool) -> None: @@ -31,9 +32,9 @@ class SelectItem(MenuItem): """Clickable menu item.""" label: str - callback: Callable[[], ScreenResult] + callback: Callable[[], StateResult] - def on_event(self, event: tcod.event.Event) -> ScreenResult: + def on_event(self, event: tcod.event.Event) -> StateResult: """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 +50,7 @@ class SelectItem(MenuItem): @attrs.define() -class ListMenu(Screen): +class ListMenu(State): """Simple list menu state.""" items: tuple[MenuItem, ...] @@ -57,7 +58,7 @@ class ListMenu(Screen): x: int = 0 y: int = 0 - def on_event(self, event: tcod.event.Event) -> ScreenResult: + def on_event(self, event: tcod.event.Event) -> StateResult: """Handle events for menus.""" match event: case tcod.event.Quit(): @@ -83,18 +84,18 @@ class ListMenu(Screen): case _: return self.activate_selected(event) - def activate_selected(self, event: tcod.event.Event) -> ScreenResult: + def activate_selected(self, event: tcod.event.Event) -> StateResult: """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) -> ScreenResult: + def on_cancel(self) -> StateResult: """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_screens(self, console) + game.state_tools.draw_previous_state(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/screens/__init__.py b/game/screens/__init__.py deleted file mode 100644 index ee16cc0..0000000 --- a/game/screens/__init__.py +++ /dev/null @@ -1,99 +0,0 @@ -"""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/screens/menu_screens.py b/game/screens/menu_screens.py deleted file mode 100644 index bfa4346..0000000 --- a/game/screens/menu_screens.py +++ /dev/null @@ -1,45 +0,0 @@ -"""The main menu state""" - -from __future__ import annotations - -import g -import game.screens.menus -import game.world_tools -from game.screens import Reset, ScreenResult -from game.screens.game_screens import MainScreen - -class MainMenu(game.screens.menus.ListMenu): - """Main/escape menu.""" - __slots__ = () - - def __init__(self) -> None: - """Initialize the main menu.""" - items = [ - game.screens.menus.SelectItem("New game", self.new_game), - game.screens.menus.SelectItem("Quit", self.quit), - ] - if hasattr(g, "world"): - items.insert(0, game.screens.menus.SelectItem("Continue", self.continue_)) - - super().__init__( - items=tuple(items), - selected=0, - x=5, - y=5, - ) - - @staticmethod - def continue_() -> ScreenResult: - """Return to the game.""" - return Reset(MainScreen()) - - @staticmethod - def new_game() -> ScreenResult: - """Begin a new game.""" - g.world = game.world_tools.new_world() - return Reset(MainScreen()) - - @staticmethod - def quit() -> ScreenResult: - """Close the program.""" - raise SystemExit() diff --git a/game/state.py b/game/state.py new file mode 100644 index 0000000..4b935c4 --- /dev/null +++ b/game/state.py @@ -0,0 +1,44 @@ +"""Base classes for states.""" + +from __future__ import annotations + +from typing import Protocol, TypeAlias + +import attrs +import tcod.console +import tcod.event + + +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.""" diff --git a/game/state_tools.py b/game/state_tools.py new file mode 100644 index 0000000..77d2e22 --- /dev/null +++ b/game/state_tools.py @@ -0,0 +1,61 @@ +"""State handling functions.""" + +from __future__ import annotations + +import tcod.console + +import g +from game.state import Pop, Push, Reset, State, StateResult + + +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_state(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/game/screens/game_screens.py b/game/states.py similarity index 69% rename from game/screens/game_screens.py rename to game/states.py index e9bc948..181a13c 100644 --- a/game/screens/game_screens.py +++ b/game/states.py @@ -1,28 +1,28 @@ -"""All states the are used in-game""" +"""A collection of game states.""" from __future__ import annotations +from functools import reduce import attrs import tcod.console import tcod.constants import tcod.event from tcod.event import KeySym -from tcod.map import Map import g +import game.menus +import game.world_tools from game.components import Gold, Graphic, Position from game.constants import DIRECTION_KEYS, ACTION_KEYS -from game.screens import Push, Screen, ScreenResult +from game.state import Push, Reset, State, StateResult from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR -from game.screens import menu_screens - @attrs.define() -class MainScreen(Screen): +class InGame(State): """Primary in-game state.""" - def on_event(self, event: tcod.event.Event) -> ScreenResult: + def on_event(self, event: tcod.event.Event) -> StateResult: """Handle events for the in-game state.""" (player,) = g.world.Q.all_of(tags=[IsPlayer]) match event: @@ -34,7 +34,7 @@ class MainScreen(Screen): raise SystemExit() case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: new_pos = player.components[Position] + DIRECTION_KEYS[sym] - if not g.world[None].components[Map].walkable[g.world_center.y+new_pos.y][g.world_center.x+new_pos.x]: + if not g.world_map.walkable[g.world_center.y+new_pos.y][g.world_center.x+new_pos.x]: return None player.components[Position] = new_pos # Auto pickup gold @@ -45,7 +45,7 @@ class MainScreen(Screen): gold.clear() return None case tcod.event.KeyDown(sym=KeySym.ESCAPE): - return Push(menu_screens.MainMenu()) + return Push(MainMenu()) case _: return None @@ -62,7 +62,7 @@ class MainScreen(Screen): and -h <= screen_pos.y < h): graphic = e_graph console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg - for (y, row) in enumerate(g.world[None].components[Map].walkable): + for (y, row) in enumerate(g.world_map.walkable): for (x, val) in enumerate(row): if not val: draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR)) @@ -74,3 +74,41 @@ class MainScreen(Screen): draw(player.components[Position], player.components[Graphic]) if text := g.world[None].components.get(("Text", str)): console.print(x=0, y=console.height - 1, string=text, fg=(255, 255, 255), bg=(0, 0, 0)) + + +class MainMenu(game.menus.ListMenu): + """Main/escape menu.""" + + __slots__ = () + + def __init__(self) -> None: + """Initialize the main menu.""" + items = [ + game.menus.SelectItem("New game", self.new_game), + game.menus.SelectItem("Quit", self.quit), + ] + if hasattr(g, "world"): + items.insert(0, game.menus.SelectItem("Continue", self.continue_)) + + super().__init__( + items=tuple(items), + selected=0, + x=5, + y=5, + ) + + @staticmethod + def continue_() -> StateResult: + """Return to the game.""" + return Reset(InGame()) + + @staticmethod + def new_game() -> StateResult: + """Begin a new game.""" + g.world = game.world_tools.new_world() + return Reset(InGame()) + + @staticmethod + def quit() -> StateResult: + """Close the program.""" + raise SystemExit() diff --git a/game/world_tools.py b/game/world_tools.py index fc7022e..65b93c7 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -14,18 +14,13 @@ from game.tags import IsActor, IsItem, IsPlayer, IsDoor def add_wall(pos, remove=False): r_pos = g.world_center + pos - map = g.world[None].components[Map] - map.walkable[r_pos.y][r_pos.x] = remove - map.transparent[r_pos.y][r_pos.x] = remove + g.world_map.walkable[r_pos.y][r_pos.x] = remove def add_door(pos): - g.world.new_entity( - components={ - Position: pos, - Graphic: Graphic(ord('\\')) - }, - tags=[IsDoor] - ) + door = g.world[object()] + door.tags.add(IsDoor) + door.components[Position] = pos + door.components[Graphic] = Graphic(ord("\\")) add_wall(pos) @@ -33,36 +28,22 @@ def new_world() -> Registry: """Return a freshly generated world.""" world = Registry() g.world = world - - map = world[None].components[Map] = Map(100, 100) - map.walkable[:] = True - map.transparent[:] = True - + g.world_map = Map(100, 100) + g.world_map.walkable[:] = True rng = world[None].components[Random] = Random() - world.new_entity( - components={ - Position: Position(5,5), - Graphic: Graphic(ord('@')), - Gold: 0 - }, - tags=[IsActor, IsPlayer] - ) - # player = world[object()] # <- das hier ist das gleiche wie das world.new_entity darĂ¼ber - # player.components[Position] = Position(5, 5) - # player.components[Graphic] = Graphic(ord("@")) - # player.components[Gold] = 0 - # player.tags |= {IsPlayer, IsActor} + player = world[object()] + player.components[Position] = Position(5, 5) + player.components[Graphic] = Graphic(ord("@")) + player.components[Gold] = 0 + player.tags |= {IsPlayer, IsActor} for _ in range(10): - world.new_entity( - components={ - Position: Position(rng.randint(0, 20), rng.randint(0, 20)), - Graphic: Graphic(ord("$"), fg=(255, 255, 0)), - Gold: rng.randint(1, 10) - }, - tags=[IsItem] - ) + gold = world[object()] + gold.components[Position] = Position(rng.randint(0, 20), rng.randint(0, 20)) + gold.components[Graphic] = Graphic(ord("$"), fg=(255, 255, 0)) + gold.components[Gold] = rng.randint(1, 10) + gold.tags |= {IsItem} for i in range(20): if i == 5 or i == 9: @@ -76,4 +57,5 @@ def new_world() -> Registry: def unlock_door(door: Entity): door.components[Graphic] = Graphic(ord("_")) door.tags.clear() - add_wall(door.components[Position], remove=True) + pos = door.components[Position] + add_wall(pos, remove=True) diff --git a/main.py b/main.py index 226a327..176aefd 100755 --- a/main.py +++ b/main.py @@ -8,9 +8,8 @@ import tcod.context import tcod.tileset import g - -import game.screens -from game.screens.menu_screens import MainMenu +import game.state_tools +import game.states def main() -> None: """Entry point function.""" @@ -19,10 +18,10 @@ def main() -> None: ) tcod.tileset.procedural_block_elements(tileset=tileset) - g.screens = [MainMenu()] + g.states = [game.states.MainMenu()] g.console = tcod.console.Console(80, 50) with tcod.context.new(console=g.console, tileset=tileset) as g.context: - game.screens.main_loop() + game.state_tools.main_loop() if __name__ == "__main__":