From a4981d1575695d2a0d33f081eaf7b8db5300d939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Fri, 16 Aug 2024 23:20:24 +0200 Subject: [PATCH 01/14] Co-authored-by: staubsauger --- g.py | 3 +++ game/states.py | 19 +++++++++++-------- game/world_tools.py | 2 ++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/g.py b/g.py index 66440ab..950ef81 100644 --- a/g.py +++ b/g.py @@ -14,6 +14,9 @@ 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""" + states: list[game.state.State] = [] """A stack of states with the last item being the active state.""" diff --git a/game/states.py b/game/states.py index 107d464..1df3274 100644 --- a/game/states.py +++ b/game/states.py @@ -16,7 +16,7 @@ from game.components import Gold, Graphic, Position from game.constants import DIRECTION_KEYS, ACTION_KEYS from game.state import Push, Reset, State, StateResult from game.tags import IsItem, IsPlayer, IsWall, IsDoor, IsActor - +from game.constants import WALL_CHAR @attrs.define() class InGame(State): @@ -56,19 +56,22 @@ class InGame(State): center = sum(centers, start=Position(0,0)) center = center.mod(len(centers)) - def draw(e): - pos = e.components[Position] - center + def draw(e_pos, e_graph): + pos = e_pos - center if (-console.width//2 <= pos.x < console.width//2\ and -console.height//2 <= pos.y < console.height//2): - graphic = e.components[Graphic] + graphic = e_graph console.rgb[["ch", "fg"]][pos.y + console.height//2, pos.x + console.width//2] = graphic.ch, graphic.fg - + for (y, row) in enumerate(g.world_map.walkable): + for (x, val) in enumerate(row): + if val: + draw(Position(x,y), Graphic(WALL_CHAR)) for entity in g.world.Q.all_of(components=[Position, Graphic]).none_of(tags=[IsActor]): - draw(entity) + draw(entity.components[Position], entity.components[Graphic]) for actor in g.world.Q.all_of(components=[Position, Graphic], tags=[IsActor]).none_of(tags=[IsPlayer]): - draw(actor) + draw(actor.components[Position], entity.components[Graphic]) for player in g.world.Q.all_of(tags=[IsPlayer]): - draw(player) + 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)) diff --git a/game/world_tools.py b/game/world_tools.py index 7ef2605..c32c6da 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -7,6 +7,7 @@ from random import Random import g from tcod.ecs import Registry, Entity +from tcod.map import Map from game.components import Gold, Graphic, Position from game.tags import IsActor, IsItem, IsPlayer, IsWall, IsDoor @@ -15,6 +16,7 @@ from game.tags import IsActor, IsItem, IsPlayer, IsWall, IsDoor def new_world() -> Registry: """Return a freshly generated world.""" world = Registry() + g.world_map = Map(100, 100) rng = world[None].components[Random] = Random() player = world[object()] From 9b85305481e12d00b4a46277c657791eca592a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 00:00:01 +0200 Subject: [PATCH 02/14] moved walls to static map object --- g.py | 2 ++ game/states.py | 24 ++++++++++++------------ game/world_tools.py | 28 ++++++++++++++++++---------- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/g.py b/g.py index 950ef81..8f18308 100644 --- a/g.py +++ b/g.py @@ -17,6 +17,8 @@ world: tcod.ecs.Registry world_map: tcod.map.Map """Wall Map of current World""" +world_center: tuple[int,int] = (50, 50) + states: list[game.state.State] = [] """A stack of states with the last item being the active state.""" diff --git a/game/states.py b/game/states.py index 1df3274..50dc67b 100644 --- a/game/states.py +++ b/game/states.py @@ -34,7 +34,7 @@ class InGame(State): raise SystemExit() case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: new_pos = player.components[Position] + DIRECTION_KEYS[sym] - if g.world.Q.all_of(tags=[new_pos, IsWall]) or g.world.Q.all_of(tags=[new_pos, IsDoor]): + if not g.world_map.walkable[g.world_center[1]+new_pos.y][g.world_center[0]+new_pos.x]: return None player.components[Position] = new_pos # Auto pickup gold @@ -51,21 +51,21 @@ class InGame(State): def on_draw(self, console: tcod.console.Console) -> None: """Draw the standard screen.""" - centers = g.world.Q.all_of(tags=[IsPlayer]) - centers = [a.components[Position] for a in centers] - center = sum(centers, start=Position(0,0)) - center = center.mod(len(centers)) - + centers = [a.components[Position] for a in g.world.Q.all_of(tags=[IsPlayer])] + camera_pos = sum(centers, start=Position(0,0)) + camera_pos = camera_pos.mod(len(centers)) + h = console.height//2 + w = console.width//2 def draw(e_pos, e_graph): - pos = e_pos - center - if (-console.width//2 <= pos.x < console.width//2\ - and -console.height//2 <= pos.y < console.height//2): + screen_pos = e_pos - camera_pos + if (-w <= screen_pos.x < w\ + and -h <= screen_pos.y < h): graphic = e_graph - console.rgb[["ch", "fg"]][pos.y + console.height//2, pos.x + console.width//2] = 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 (x, val) in enumerate(row): - if val: - draw(Position(x,y), Graphic(WALL_CHAR)) + if not val: + draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR)) for entity in g.world.Q.all_of(components=[Position, Graphic]).none_of(tags=[IsActor]): draw(entity.components[Position], entity.components[Graphic]) for actor in g.world.Q.all_of(components=[Position, Graphic], tags=[IsActor]).none_of(tags=[IsPlayer]): diff --git a/game/world_tools.py b/game/world_tools.py index c32c6da..6eb0df1 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -12,11 +12,23 @@ from tcod.map import Map from game.components import Gold, Graphic, Position from game.tags import IsActor, IsItem, IsPlayer, IsWall, IsDoor +def add_wall(x, y, remove=False): + g.world_map.walkable[50+y][50+x] = remove + +def add_door(x, y): + door = g.world[object()] + door.tags.add(IsDoor) + door.components[Position] = Position(x,y) + door.components[Graphic] = Graphic(ord("\\")) + add_wall(x,y) + def new_world() -> Registry: """Return a freshly generated world.""" world = Registry() + g.world = world g.world_map = Map(100, 100) + g.world_map.walkable[:] = True rng = world[None].components[Random] = Random() player = world[object()] @@ -34,19 +46,15 @@ def new_world() -> Registry: for i in range(20): if i == 5 or i == 9: - door = world[object()] - door.components[Position] = Position(10, i) - door.components[Graphic] = Graphic(ord("\\")) - door.tags |= {IsDoor} - continue - wall = world[object()] - wall.components[Position] = Position(10, i) - wall.components[Graphic] = Graphic(ord("#")) - wall.tags |= {IsWall} + add_door(10,i) + else: + add_wall(10, i) return world def unlock_door(door: Entity): door.components[Graphic] = Graphic(ord("_")) - door.tags.discard(IsDoor) + door.tags.clear() + pos = door.components[Position] + add_wall(pos.x,pos.y, remove=True) From 91485c0f6b656b34f4546b412a3e5d9eab23e61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 09:56:13 +0200 Subject: [PATCH 03/14] completed moving walls to map --- g.py | 3 ++- game/states.py | 4 ++-- game/tags.py | 3 --- game/world_tools.py | 19 ++++++++++--------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/g.py b/g.py index 8f18308..52bf2ab 100644 --- a/g.py +++ b/g.py @@ -7,6 +7,7 @@ import tcod.context import tcod.ecs import game.state +from game.components import Position context: tcod.context.Context """The window managed by tcod.""" @@ -17,7 +18,7 @@ world: tcod.ecs.Registry world_map: tcod.map.Map """Wall Map of current World""" -world_center: tuple[int,int] = (50, 50) +world_center: tuple[int,int] = Position(50, 50) states: list[game.state.State] = [] """A stack of states with the last item being the active state.""" diff --git a/game/states.py b/game/states.py index 50dc67b..181a13c 100644 --- a/game/states.py +++ b/game/states.py @@ -15,7 +15,7 @@ import game.world_tools from game.components import Gold, Graphic, Position from game.constants import DIRECTION_KEYS, ACTION_KEYS from game.state import Push, Reset, State, StateResult -from game.tags import IsItem, IsPlayer, IsWall, IsDoor, IsActor +from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR @attrs.define() @@ -34,7 +34,7 @@ class InGame(State): 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_map.walkable[g.world_center[1]+new_pos.y][g.world_center[0]+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 diff --git a/game/tags.py b/game/tags.py index 954fb52..bfbbe7c 100644 --- a/game/tags.py +++ b/game/tags.py @@ -13,8 +13,5 @@ IsActor: Final = "IsActor" IsItem: Final = "IsItem" """Entity is an item.""" -IsWall: Final = "IsWall" -"""Entity is a wall.""" - IsDoor: Final = "IsDoor" """Entiy is a door.""" diff --git a/game/world_tools.py b/game/world_tools.py index 6eb0df1..65b93c7 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -10,17 +10,18 @@ from tcod.ecs import Registry, Entity from tcod.map import Map from game.components import Gold, Graphic, Position -from game.tags import IsActor, IsItem, IsPlayer, IsWall, IsDoor +from game.tags import IsActor, IsItem, IsPlayer, IsDoor -def add_wall(x, y, remove=False): - g.world_map.walkable[50+y][50+x] = remove +def add_wall(pos, remove=False): + r_pos = g.world_center + pos + g.world_map.walkable[r_pos.y][r_pos.x] = remove -def add_door(x, y): +def add_door(pos): door = g.world[object()] door.tags.add(IsDoor) - door.components[Position] = Position(x,y) + door.components[Position] = pos door.components[Graphic] = Graphic(ord("\\")) - add_wall(x,y) + add_wall(pos) def new_world() -> Registry: @@ -46,9 +47,9 @@ def new_world() -> Registry: for i in range(20): if i == 5 or i == 9: - add_door(10,i) + add_door(Position(10,i)) else: - add_wall(10, i) + add_wall(Position(10, i)) return world @@ -57,4 +58,4 @@ def unlock_door(door: Entity): door.components[Graphic] = Graphic(ord("_")) door.tags.clear() pos = door.components[Position] - add_wall(pos.x,pos.y, remove=True) + add_wall(pos, remove=True) From 8f7a15e17cf6073cb29d5c0c22940a17bf6e3f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 09:58:00 +0200 Subject: [PATCH 04/14] "Readme" --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2339b4e..2e6c695 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# python-tcod-tutorial-2023 +# pyrogue + +A small roguelike written in Python using tcod. From 72ec02dbaef730da116e47c0f8e5182f5700f562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 10:05:44 +0200 Subject: [PATCH 05/14] extracted states to own files --- game/{states.py => states/InGame.py} | 46 ++-------------------------- game/states/MainMenu.py | 45 +++++++++++++++++++++++++++ main.py | 4 ++- 3 files changed, 51 insertions(+), 44 deletions(-) rename game/{states.py => states/InGame.py} (76%) create mode 100644 game/states/MainMenu.py diff --git a/game/states.py b/game/states/InGame.py similarity index 76% rename from game/states.py rename to game/states/InGame.py index 181a13c..106f45c 100644 --- a/game/states.py +++ b/game/states/InGame.py @@ -1,7 +1,4 @@ -"""A collection of game states.""" - from __future__ import annotations -from functools import reduce import attrs import tcod.console @@ -10,13 +7,14 @@ import tcod.event from tcod.event import KeySym 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.state import Push, Reset, State, StateResult +from game.state import Push, State, StateResult from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR +from game.states import MainMenu + @attrs.define() class InGame(State): @@ -74,41 +72,3 @@ class InGame(State): 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/states/MainMenu.py b/game/states/MainMenu.py new file mode 100644 index 0000000..6854593 --- /dev/null +++ b/game/states/MainMenu.py @@ -0,0 +1,45 @@ +"""The main menu state""" + +from __future__ import annotations + +import g +import game.menus +import game.world_tools +from game.state import Reset, StateResult +from game.states.InGame import InGame + +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/main.py b/main.py index 176aefd..6703a24 100755 --- a/main.py +++ b/main.py @@ -11,6 +11,8 @@ import g import game.state_tools import game.states +from game.states.MainMenu import MainMenu + def main() -> None: """Entry point function.""" tileset = tcod.tileset.load_tilesheet( @@ -18,7 +20,7 @@ def main() -> None: ) tcod.tileset.procedural_block_elements(tileset=tileset) - g.states = [game.states.MainMenu()] + g.states = [MainMenu()] g.console = tcod.console.Console(80, 50) with tcod.context.new(console=g.console, tileset=tileset) as g.context: game.state_tools.main_loop() From 2560a4dcd933b31a673e9297e7cbfa01e574996f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 10:40:57 +0200 Subject: [PATCH 06/14] module refactoring --- game/state.py | 55 ++++++++++++++++++ game/state_tools.py | 61 -------------------- game/states/{InGame.py => game_screens.py} | 6 +- game/states/{MainMenu.py => menu_screens.py} | 16 ++--- game/{ => states}/menus.py | 5 +- main.py | 7 +-- 6 files changed, 71 insertions(+), 79 deletions(-) delete mode 100644 game/state_tools.py rename game/states/{InGame.py => game_screens.py} (96%) rename game/states/{MainMenu.py => menu_screens.py} (65%) rename game/{ => states}/menus.py (96%) diff --git a/game/state.py b/game/state.py index 4b935c4..4497f0d 100644 --- a/game/state.py +++ b/game/state.py @@ -8,6 +8,8 @@ import attrs import tcod.console import tcod.event +import g + class State(Protocol): """An abstract game state.""" @@ -42,3 +44,56 @@ class Reset: 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/game/state_tools.py b/game/state_tools.py deleted file mode 100644 index 77d2e22..0000000 --- a/game/state_tools.py +++ /dev/null @@ -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 diff --git a/game/states/InGame.py b/game/states/game_screens.py similarity index 96% rename from game/states/InGame.py rename to game/states/game_screens.py index 106f45c..583e431 100644 --- a/game/states/InGame.py +++ b/game/states/game_screens.py @@ -13,11 +13,11 @@ from game.constants import DIRECTION_KEYS, ACTION_KEYS from game.state import Push, State, StateResult from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR -from game.states import MainMenu +from game.states import menu_screens @attrs.define() -class InGame(State): +class MainScreen(State): """Primary in-game state.""" def on_event(self, event: tcod.event.Event) -> StateResult: @@ -43,7 +43,7 @@ class InGame(State): gold.clear() return None case tcod.event.KeyDown(sym=KeySym.ESCAPE): - return Push(MainMenu()) + return Push(menu_screens()) case _: return None diff --git a/game/states/MainMenu.py b/game/states/menu_screens.py similarity index 65% rename from game/states/MainMenu.py rename to game/states/menu_screens.py index 6854593..320323f 100644 --- a/game/states/MainMenu.py +++ b/game/states/menu_screens.py @@ -3,23 +3,23 @@ from __future__ import annotations import g -import game.menus +import game.states.menus import game.world_tools from game.state import Reset, StateResult -from game.states.InGame import InGame +from game.states.game_screens import MainScreen -class MainMenu(game.menus.ListMenu): +class MainMenu(game.states.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), + game.states.menus.SelectItem("New game", self.new_game), + game.states.menus.SelectItem("Quit", self.quit), ] if hasattr(g, "world"): - items.insert(0, game.menus.SelectItem("Continue", self.continue_)) + items.insert(0, game.states.menus.SelectItem("Continue", self.continue_)) super().__init__( items=tuple(items), @@ -31,13 +31,13 @@ class MainMenu(game.menus.ListMenu): @staticmethod def continue_() -> StateResult: """Return to the game.""" - return Reset(InGame()) + return Reset(MainScreen()) @staticmethod def new_game() -> StateResult: """Begin a new game.""" g.world = game.world_tools.new_world() - return Reset(InGame()) + return Reset(MainScreen()) @staticmethod def quit() -> StateResult: diff --git a/game/menus.py b/game/states/menus.py similarity index 96% rename from game/menus.py rename to game/states/menus.py index 7c1bb37..c9e6219 100644 --- a/game/menus.py +++ b/game/states/menus.py @@ -10,9 +10,8 @@ import tcod.console import tcod.event from tcod.event import KeySym -import game.state_tools from game.constants import DIRECTION_KEYS -from game.state import Pop, State, StateResult +from game.state import Pop, State, StateResult, draw_previous_states class MenuItem(Protocol): @@ -96,6 +95,6 @@ class ListMenu(State): def on_draw(self, console: tcod.console.Console) -> None: """Render the menu.""" - game.state_tools.draw_previous_state(self, console) + draw_previous_states(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/main.py b/main.py index 6703a24..582f885 100755 --- a/main.py +++ b/main.py @@ -8,10 +8,9 @@ import tcod.context import tcod.tileset import g -import game.state_tools -import game.states -from game.states.MainMenu import MainMenu +import game.state +from game.states.menu_screens import MainMenu def main() -> None: """Entry point function.""" @@ -23,7 +22,7 @@ def main() -> None: g.states = [MainMenu()] g.console = tcod.console.Console(80, 50) with tcod.context.new(console=g.console, tileset=tileset) as g.context: - game.state_tools.main_loop() + game.state.main_loop() if __name__ == "__main__": 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 07/14] 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__": From 45e7a8927a73db6c12a719ccb9df3a6c53cfe1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 11:29:39 +0200 Subject: [PATCH 08/14] fixed pause menu and made `world_map` a component of `world` --- g.py | 3 -- game/screens/game_screens.py | 7 +++-- game/world_tools.py | 56 ++++++++++++++++++++++++------------ 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/g.py b/g.py index 2d12b10..e30e095 100644 --- a/g.py +++ b/g.py @@ -15,9 +15,6 @@ 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] = [] diff --git a/game/screens/game_screens.py b/game/screens/game_screens.py index 504ccec..e9bc948 100644 --- a/game/screens/game_screens.py +++ b/game/screens/game_screens.py @@ -7,6 +7,7 @@ import tcod.console import tcod.constants import tcod.event from tcod.event import KeySym +from tcod.map import Map import g from game.components import Gold, Graphic, Position @@ -33,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_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 player.components[Position] = new_pos # Auto pickup gold @@ -44,7 +45,7 @@ class MainScreen(Screen): gold.clear() return None case tcod.event.KeyDown(sym=KeySym.ESCAPE): - return Push(menu_screens()) + return Push(menu_screens.MainMenu()) case _: return None @@ -61,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_map.walkable): + for (y, row) in enumerate(g.world[None].components[Map].walkable): for (x, val) in enumerate(row): if not val: draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR)) diff --git a/game/world_tools.py b/game/world_tools.py index 65b93c7..fc7022e 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -14,13 +14,18 @@ from game.tags import IsActor, IsItem, IsPlayer, IsDoor def add_wall(pos, remove=False): 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): - door = g.world[object()] - door.tags.add(IsDoor) - door.components[Position] = pos - door.components[Graphic] = Graphic(ord("\\")) + g.world.new_entity( + components={ + Position: pos, + Graphic: Graphic(ord('\\')) + }, + tags=[IsDoor] + ) add_wall(pos) @@ -28,22 +33,36 @@ def new_world() -> Registry: """Return a freshly generated world.""" world = Registry() 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() - player = world[object()] - player.components[Position] = Position(5, 5) - player.components[Graphic] = Graphic(ord("@")) - player.components[Gold] = 0 - player.tags |= {IsPlayer, IsActor} + 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} for _ in range(10): - 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} + 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] + ) for i in range(20): if i == 5 or i == 9: @@ -57,5 +76,4 @@ def new_world() -> Registry: def unlock_door(door: Entity): door.components[Graphic] = Graphic(ord("_")) door.tags.clear() - pos = door.components[Position] - add_wall(pos, remove=True) + add_wall(door.components[Position], remove=True) From bd0db732b673c28e020a88be37a06fc624bf36bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 11:36:48 +0200 Subject: [PATCH 09/14] fixed missing state -> screen refactor --- game/screens/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/game/screens/__init__.py b/game/screens/__init__.py index ee16cc0..f8da103 100644 --- a/game/screens/__init__.py +++ b/game/screens/__init__.py @@ -1,4 +1,4 @@ -"""Package for game state stuff.""" +"""Package for game screens.""" from __future__ import annotations @@ -55,8 +55,8 @@ def main_draw() -> None: g.context.present(g.console) -def apply_state_result(result: ScreenResult) -> None: - """Apply a StateResult to `g.states`.""" +def apply_screen_result(result: ScreenResult) -> None: + """Apply a ScreenResult to `g.screens`.""" match result: case Push(screen=screen): g.screens.append(screen) @@ -64,8 +64,8 @@ def apply_state_result(result: ScreenResult) -> None: g.screens.pop() case Reset(screen=screen): while g.screens: - apply_state_result(Pop()) - apply_state_result(Push(screen)) + apply_screen_result(Pop()) + apply_screen_result(Push(screen)) case None: pass case _: @@ -73,23 +73,23 @@ def apply_state_result(result: ScreenResult) -> None: def main_loop() -> None: - """Run the active state forever.""" + """Run the active screen 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)) + apply_screen_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.""" + """Return the screen before `screen` 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.""" + """Draw previous screens, optionally dimming all but the active screen.""" prev_screen = get_previous_screen(screen) if prev_screen is None: return From 1b5d512a4b3677831a1027f99ddb51971a344014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 11:48:28 +0200 Subject: [PATCH 10/14] fixed global usage --- game/world_tools.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/game/world_tools.py b/game/world_tools.py index fc7022e..43a5a20 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -12,27 +12,26 @@ from tcod.map import Map from game.components import Gold, Graphic, Position from game.tags import IsActor, IsItem, IsPlayer, IsDoor -def add_wall(pos, remove=False): +def add_wall(world, pos, remove=False): r_pos = g.world_center + pos - map = g.world[None].components[Map] + map = 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): - g.world.new_entity( +def add_door(world, pos): + world.new_entity( components={ Position: pos, Graphic: Graphic(ord('\\')) }, tags=[IsDoor] ) - add_wall(pos) + add_wall(world, pos) 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 @@ -66,9 +65,9 @@ def new_world() -> Registry: for i in range(20): if i == 5 or i == 9: - add_door(Position(10,i)) + add_door(world, Position(10,i)) else: - add_wall(Position(10, i)) + add_wall(world, Position(10, i)) return world @@ -76,4 +75,4 @@ def new_world() -> Registry: def unlock_door(door: Entity): door.components[Graphic] = Graphic(ord("_")) door.tags.clear() - add_wall(door.components[Position], remove=True) + add_wall(g.world, door.components[Position], remove=True) From 0a0ff2d1ed854f4673c348923ced416d56fd31dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 12:09:19 +0200 Subject: [PATCH 11/14] added FOV --- game/screens/game_screens.py | 23 ++++++++++++++++++----- game/world_tools.py | 6 ++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/game/screens/game_screens.py b/game/screens/game_screens.py index e9bc948..067a833 100644 --- a/game/screens/game_screens.py +++ b/game/screens/game_screens.py @@ -16,7 +16,7 @@ from game.screens import Push, Screen, ScreenResult from game.tags import IsItem, IsPlayer, IsDoor, IsActor from game.constants import WALL_CHAR from game.screens import menu_screens - +from game.world_tools import world_pos_to_map_pos, map_pos_to_world_pos @attrs.define() class MainScreen(Screen): @@ -34,7 +34,8 @@ 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]: + map_pos = world_pos_to_map_pos(new_pos) + if not g.world[None].components[Map].walkable[map_pos.y][map_pos.x]: return None player.components[Position] = new_pos # Auto pickup gold @@ -56,20 +57,32 @@ class MainScreen(Screen): camera_pos = camera_pos.mod(len(centers)) h = console.height//2 w = console.width//2 + + map: Map = g.world[None].components[Map] + cam_map = world_pos_to_map_pos(camera_pos) + map.compute_fov(cam_map.x, cam_map.y, 100) + def draw(e_pos, e_graph): screen_pos = e_pos - camera_pos + mp = world_pos_to_map_pos(e_pos) if (-w <= screen_pos.x < w\ - and -h <= screen_pos.y < h): + and -h <= screen_pos.y < h)\ + and map.fov[mp.y][mp.x]: 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): + + # Draw walls + for (y, row) in enumerate(map.walkable): for (x, val) in enumerate(row): if not val: - draw(Position(x,y)-g.world_center, Graphic(WALL_CHAR)) + draw(map_pos_to_world_pos(Position(x,y)), Graphic(WALL_CHAR)) + # draw all entities that are not actors for entity in g.world.Q.all_of(components=[Position, Graphic]).none_of(tags=[IsActor]): draw(entity.components[Position], entity.components[Graphic]) + # draw all actors for actor in g.world.Q.all_of(components=[Position, Graphic], tags=[IsActor]).none_of(tags=[IsPlayer]): draw(actor.components[Position], entity.components[Graphic]) + # draw the player for player in g.world.Q.all_of(tags=[IsPlayer]): draw(player.components[Position], player.components[Graphic]) if text := g.world[None].components.get(("Text", str)): diff --git a/game/world_tools.py b/game/world_tools.py index 43a5a20..e0ede47 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -12,6 +12,12 @@ from tcod.map import Map from game.components import Gold, Graphic, Position from game.tags import IsActor, IsItem, IsPlayer, IsDoor +def world_pos_to_map_pos(pos): + return pos+g.world_center + +def map_pos_to_world_pos(pos): + return pos-g.world_center + def add_wall(world, pos, remove=False): r_pos = g.world_center + pos map = world[None].components[Map] From 1bd0ab372d50337775d7acb856bedfe06172a51d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 12:28:39 +0200 Subject: [PATCH 12/14] enhanced FOV --- g.py | 2 -- game/screens/game_screens.py | 17 +++++++++++------ game/world_tools.py | 14 ++++++++------ main.py | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/g.py b/g.py index e30e095..64d1041 100644 --- a/g.py +++ b/g.py @@ -15,8 +15,6 @@ context: tcod.context.Context world: tcod.ecs.Registry """The active ECS registry and current session.""" -world_center: tuple[int,int] = Position(50, 50) - screens: list[Screen] = [] """A stack of states with the last item being the active state.""" diff --git a/game/screens/game_screens.py b/game/screens/game_screens.py index 067a833..0075be1 100644 --- a/game/screens/game_screens.py +++ b/game/screens/game_screens.py @@ -64,18 +64,23 @@ class MainScreen(Screen): def draw(e_pos, e_graph): screen_pos = e_pos - camera_pos - mp = world_pos_to_map_pos(e_pos) + map_pos = world_pos_to_map_pos(e_pos) if (-w <= screen_pos.x < w\ - and -h <= screen_pos.y < h)\ - and map.fov[mp.y][mp.x]: - graphic = e_graph + and -h <= screen_pos.y < h): + if map.fov[map_pos.y][map_pos.x]: + graphic = e_graph + else: + graphic = Graphic(0x2591, (50, 50, 50)) console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg # Draw walls for (y, row) in enumerate(map.walkable): for (x, val) in enumerate(row): - if not val: - draw(map_pos_to_world_pos(Position(x,y)), Graphic(WALL_CHAR)) + pos = map_pos_to_world_pos(Position(x,y)) + if val: + draw(pos, Graphic(0)) # open air + else: + draw(pos, Graphic(WALL_CHAR)) # draw all entities that are not actors for entity in g.world.Q.all_of(components=[Position, Graphic]).none_of(tags=[IsActor]): draw(entity.components[Position], entity.components[Graphic]) diff --git a/game/world_tools.py b/game/world_tools.py index e0ede47..74a6783 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -1,25 +1,27 @@ """Functions for working with worlds.""" from __future__ import annotations - from random import Random - -import g +from typing import Final from tcod.ecs import Registry, Entity from tcod.map import Map +import g + from game.components import Gold, Graphic, Position from game.tags import IsActor, IsItem, IsPlayer, IsDoor +world_center: Final = Position(50, 50) + def world_pos_to_map_pos(pos): - return pos+g.world_center + return pos+world_center def map_pos_to_world_pos(pos): - return pos-g.world_center + return pos-world_center def add_wall(world, pos, remove=False): - r_pos = g.world_center + pos + r_pos = world_pos_to_map_pos(pos) map = world[None].components[Map] map.walkable[r_pos.y][r_pos.x] = remove map.transparent[r_pos.y][r_pos.x] = remove diff --git a/main.py b/main.py index 226a327..cfb479e 100755 --- a/main.py +++ b/main.py @@ -17,7 +17,7 @@ def main() -> None: tileset = tcod.tileset.load_tilesheet( "data/Alloy_curses_12x12.png", columns=16, rows=16, charmap=tcod.tileset.CHARMAP_CP437 ) - tcod.tileset.procedural_block_elements(tileset=tileset) + #tcod.tileset.procedural_block_elements(tileset=tileset) g.screens = [MainMenu()] g.console = tcod.console.Console(80, 50) From bbc958b36c958115510146a26a50ea2abe2e837e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 12:41:41 +0200 Subject: [PATCH 13/14] recalculate FOV only on movement or door opening --- game/screens/game_screens.py | 14 ++++++++++---- game/world_tools.py | 6 ++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/game/screens/game_screens.py b/game/screens/game_screens.py index 0075be1..a2d6ffd 100644 --- a/game/screens/game_screens.py +++ b/game/screens/game_screens.py @@ -25,19 +25,27 @@ class MainScreen(Screen): def on_event(self, event: tcod.event.Event) -> ScreenResult: """Handle events for the in-game state.""" (player,) = g.world.Q.all_of(tags=[IsPlayer]) + map: Map = g.world[None].components[Map] match event: case tcod.event.KeyDown(sym=sym) if sym in ACTION_KEYS: for door in g.world.Q.all_of(tags=[IsDoor]): - if (player.components[Position] - door.components[Position]).length() < 2: + player_pos = player.components[Position] + if (player_pos - door.components[Position]).length() < 2: ACTION_KEYS[sym](door) + cam_map = world_pos_to_map_pos(player_pos) + map.compute_fov(cam_map.x, cam_map.y, 100) + case tcod.event.Quit(): raise SystemExit() case tcod.event.KeyDown(sym=sym) if sym in DIRECTION_KEYS: new_pos = player.components[Position] + DIRECTION_KEYS[sym] map_pos = world_pos_to_map_pos(new_pos) - if not g.world[None].components[Map].walkable[map_pos.y][map_pos.x]: + if not map.walkable[map_pos.y][map_pos.x]: return None player.components[Position] = new_pos + cam_map = world_pos_to_map_pos(new_pos) + map.compute_fov(cam_map.x, cam_map.y, 100) + # Auto pickup gold for gold in g.world.Q.all_of(components=[Gold], tags=[player.components[Position], IsItem]): player.components[Gold] += gold.components[Gold] @@ -59,8 +67,6 @@ class MainScreen(Screen): w = console.width//2 map: Map = g.world[None].components[Map] - cam_map = world_pos_to_map_pos(camera_pos) - map.compute_fov(cam_map.x, cam_map.y, 100) def draw(e_pos, e_graph): screen_pos = e_pos - camera_pos diff --git a/game/world_tools.py b/game/world_tools.py index 74a6783..7e0e844 100644 --- a/game/world_tools.py +++ b/game/world_tools.py @@ -46,10 +46,10 @@ def new_world() -> Registry: map.transparent[:] = True rng = world[None].components[Random] = Random() - + player_pos = Position(5,5) world.new_entity( components={ - Position: Position(5,5), + Position: player_pos, Graphic: Graphic(ord('@')), Gold: 0 }, @@ -77,6 +77,8 @@ def new_world() -> Registry: else: add_wall(world, Position(10, i)) + cam_pos = world_pos_to_map_pos(player_pos) + map.compute_fov(cam_pos.x, cam_pos.y) return world From 4e5005523bd9012e01e1d19adc91604774c031cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20N=C3=B6llemeyer?= Date: Sat, 17 Aug 2024 13:15:44 +0200 Subject: [PATCH 14/14] small drawing change --- game/screens/game_screens.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/game/screens/game_screens.py b/game/screens/game_screens.py index a2d6ffd..238dc6f 100644 --- a/game/screens/game_screens.py +++ b/game/screens/game_screens.py @@ -67,7 +67,10 @@ class MainScreen(Screen): w = console.width//2 map: Map = g.world[None].components[Map] - + # TODO: eigentlich wäre andersrum rendern schöner + # also nicht alle objekte zu rendern und dabei rauszufinden ob sie auf dem screen sind, + # sondern über alle screen zellen laufen, über prüfen ob es im fov ist und dann gucken ob es dort ein objekt + # gibt und es entsprechend rendern def draw(e_pos, e_graph): screen_pos = e_pos - camera_pos map_pos = world_pos_to_map_pos(e_pos) @@ -77,16 +80,14 @@ class MainScreen(Screen): graphic = e_graph else: graphic = Graphic(0x2591, (50, 50, 50)) - console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg + if graphic.ch != 0: + console.rgb[["ch", "fg"]][screen_pos.y + h, screen_pos.x + w] = graphic.ch, graphic.fg # Draw walls for (y, row) in enumerate(map.walkable): for (x, val) in enumerate(row): pos = map_pos_to_world_pos(Position(x,y)) - if val: - draw(pos, Graphic(0)) # open air - else: - draw(pos, Graphic(WALL_CHAR)) + draw(pos, Graphic(0) if val else Graphic(WALL_CHAR)) # draw all entities that are not actors for entity in g.world.Q.all_of(components=[Position, Graphic]).none_of(tags=[IsActor]): draw(entity.components[Position], entity.components[Graphic])