Compare commits

...

4 commits

9 changed files with 207 additions and 191 deletions

7
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
@ -15,12 +15,9 @@ context: tcod.context.Context
world: tcod.ecs.Registry world: tcod.ecs.Registry
"""The active ECS registry and current session.""" """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) 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,28 +1,28 @@
"""A collection of game states.""" """All states the are used in-game"""
from __future__ import annotations from __future__ import annotations
from functools import reduce
import attrs import attrs
import tcod.console import tcod.console
import tcod.constants import tcod.constants
import tcod.event import tcod.event
from tcod.event import KeySym from tcod.event import KeySym
from tcod.map import Map
import g import g
import game.menus
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, Reset, 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.screens import menu_screens
@attrs.define() @attrs.define()
class InGame(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:
@ -34,7 +34,7 @@ class InGame(State):
raise SystemExit() raise SystemExit()
case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS:
new_pos = player.components[Position] + DIRECTION_KEYS[sym] new_pos = player.components[Position] + DIRECTION_KEYS[sym]
if not g.world_map.walkable[g.world_center.y+new_pos.y][g.world_center.x+new_pos.x]: if not g.world[None].components[Map].walkable[g.world_center.y+new_pos.y][g.world_center.x+new_pos.x]:
return None return None
player.components[Position] = new_pos player.components[Position] = new_pos
# Auto pickup gold # Auto pickup gold
@ -45,7 +45,7 @@ class InGame(State):
gold.clear() gold.clear()
return None return None
case tcod.event.KeyDown(sym=KeySym.ESCAPE): case tcod.event.KeyDown(sym=KeySym.ESCAPE):
return Push(MainMenu()) return Push(menu_screens.MainMenu())
case _: case _:
return None return None
@ -62,7 +62,7 @@ class InGame(State):
and -h <= screen_pos.y < h): and -h <= screen_pos.y < h):
graphic = e_graph graphic = e_graph
console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg
for (y, row) in enumerate(g.world_map.walkable): for (y, row) in enumerate(g.world[None].components[Map].walkable):
for (x, val) in enumerate(row): for (x, val) in enumerate(row):
if not val: if not val:
draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR)) draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR))
@ -74,41 +74,3 @@ class InGame(State):
draw(player.components[Position], player.components[Graphic]) draw(player.components[Position], player.components[Graphic])
if text := g.world[None].components.get(("Text", str)): 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)) 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()

View file

@ -0,0 +1,45 @@
"""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()

View file

@ -10,9 +10,8 @@ import tcod.console
import tcod.event import tcod.event
from tcod.event import KeySym from tcod.event import KeySym
import game.state_tools
from game.constants import DIRECTION_KEYS from game.constants import DIRECTION_KEYS
from game.state import Pop, State, StateResult from game.screens import Pop, Screen, ScreenResult, draw_previous_screens
class MenuItem(Protocol): class MenuItem(Protocol):
@ -20,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:
@ -32,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}:
@ -50,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, ...]
@ -58,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():
@ -84,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."""
game.state_tools.draw_previous_state(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,44 +0,0 @@
"""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."""

View file

@ -1,61 +0,0 @@
"""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

View file

@ -14,13 +14,18 @@ from game.tags import IsActor, IsItem, IsPlayer, IsDoor
def add_wall(pos, remove=False): def add_wall(pos, remove=False):
r_pos = g.world_center + pos r_pos = g.world_center + pos
g.world_map.walkable[r_pos.y][r_pos.x] = remove map = g.world[None].components[Map]
map.walkable[r_pos.y][r_pos.x] = remove
map.transparent[r_pos.y][r_pos.x] = remove
def add_door(pos): def add_door(pos):
door = g.world[object()] g.world.new_entity(
door.tags.add(IsDoor) components={
door.components[Position] = pos Position: pos,
door.components[Graphic] = Graphic(ord("\\")) Graphic: Graphic(ord('\\'))
},
tags=[IsDoor]
)
add_wall(pos) add_wall(pos)
@ -28,22 +33,36 @@ def new_world() -> Registry:
"""Return a freshly generated world.""" """Return a freshly generated world."""
world = Registry() world = Registry()
g.world = world g.world = world
g.world_map = Map(100, 100)
g.world_map.walkable[:] = True map = world[None].components[Map] = Map(100, 100)
map.walkable[:] = True
map.transparent[:] = True
rng = world[None].components[Random] = Random() rng = world[None].components[Random] = Random()
player = world[object()] world.new_entity(
player.components[Position] = Position(5, 5) components={
player.components[Graphic] = Graphic(ord("@")) Position: Position(5,5),
player.components[Gold] = 0 Graphic: Graphic(ord('@')),
player.tags |= {IsPlayer, IsActor} 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}
for _ in range(10): for _ in range(10):
gold = world[object()] world.new_entity(
gold.components[Position] = Position(rng.randint(0, 20), rng.randint(0, 20)) components={
gold.components[Graphic] = Graphic(ord("$"), fg=(255, 255, 0)) Position: Position(rng.randint(0, 20), rng.randint(0, 20)),
gold.components[Gold] = rng.randint(1, 10) Graphic: Graphic(ord("$"), fg=(255, 255, 0)),
gold.tags |= {IsItem} Gold: rng.randint(1, 10)
},
tags=[IsItem]
)
for i in range(20): for i in range(20):
if i == 5 or i == 9: if i == 5 or i == 9:
@ -57,5 +76,4 @@ def new_world() -> Registry:
def unlock_door(door: Entity): def unlock_door(door: Entity):
door.components[Graphic] = Graphic(ord("_")) door.components[Graphic] = Graphic(ord("_"))
door.tags.clear() door.tags.clear()
pos = door.components[Position] add_wall(door.components[Position], remove=True)
add_wall(pos, remove=True)

View file

@ -8,8 +8,9 @@ import tcod.context
import tcod.tileset import tcod.tileset
import g import g
import game.state_tools
import game.states import game.screens
from game.screens.menu_screens import MainMenu
def main() -> None: def main() -> None:
"""Entry point function.""" """Entry point function."""
@ -18,10 +19,10 @@ def main() -> None:
) )
tcod.tileset.procedural_block_elements(tileset=tileset) tcod.tileset.procedural_block_elements(tileset=tileset)
g.states = [game.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_tools.main_loop() game.screens.main_loop()
if __name__ == "__main__": if __name__ == "__main__":