From e1231a58e64a6f511bb15328bc252714ad8f58b2 Mon Sep 17 00:00:00 2001 From: Kayne Ruse Date: Mon, 24 Mar 2025 20:42:51 +1100 Subject: [PATCH] Refactored some logic --- source/actions.py | 53 ++++++++++++++++++++++------------------- source/engine.py | 50 ++++++++++++++++---------------------- source/entity.py | 14 ++++++----- source/event_handler.py | 23 ++++++++++++++---- source/floor_map.py | 6 ++++- source/main.py | 13 +++++----- source/procgen.py | 20 +++++++++------- 7 files changed, 98 insertions(+), 81 deletions(-) diff --git a/source/actions.py b/source/actions.py index cf52852..6f10640 100644 --- a/source/actions.py +++ b/source/actions.py @@ -1,58 +1,63 @@ -from engine import Engine from entity import Entity class Action: - def apply(self, engine: Engine, entity: Entity) -> None: + def __init__(self, entity: Entity): + self.entity = entity + + def apply(self) -> None: raise NotImplementedError() class QuitAction(Action): - def apply(self, engine: Engine, entity: Entity) -> None: + def __init__(self): #override the base __init__ + pass + + def apply(self) -> None: raise SystemExit() class DirectionAction(Action): - def __init__(self, xdir: int, ydir: int): - super().__init__() + def __init__(self, entity: Entity, xdir: int, ydir: int): + super().__init__(entity) self.xdir = xdir self.ydir = ydir - def apply(self, engine: Engine, entity: Entity) -> None: + def apply(self) -> None: raise NotImplementedError() class MovementAction(DirectionAction): - def apply(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.xdir - dest_y = entity.y + self.ydir + def apply(self) -> None: + dest_x = self.entity.x + self.xdir + dest_y = self.entity.y + self.ydir #bounds and collision checks - if not engine.floor_map.in_bounds(dest_x, dest_y): + if not self.entity.floor_map.in_bounds(dest_x, dest_y): return - if not engine.floor_map.tiles["walkable"][dest_x, dest_y]: + if not self.entity.floor_map.tiles["walkable"][dest_x, dest_y]: return - if engine.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None: + if self.entity.floor_map.get_entity_at(dest_x, dest_y, unwalkable_only=True) is not None: return - entity.set_pos(dest_x, dest_y) + self.entity.set_pos(dest_x, dest_y) class MeleeAction(DirectionAction): - def apply(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.xdir - dest_y = entity.y + self.ydir + def apply(self) -> None: + dest_x = self.entity.x + self.xdir + dest_y = self.entity.y + self.ydir - target = engine.floor_map.get_entity_at(dest_x, dest_y) + target = self.entity.floor_map.get_entity_at(dest_x, dest_y) if not target: return - print(f"You kicked the {target.name}, which did nothing") + print(f"You kicked the {target.name}, which was funny") class MoveAction(DirectionAction): - def apply(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.xdir - dest_y = entity.y + self.ydir + def apply(self) -> None: + dest_x = self.entity.x + self.xdir + dest_y = self.entity.y + self.ydir - if engine.floor_map.get_entity_at(dest_x, dest_y): - return MeleeAction(self.xdir, self.ydir).apply(engine, entity) + if self.entity.floor_map.get_entity_at(dest_x, dest_y): + return MeleeAction(self.entity, self.xdir, self.ydir).apply() else: - return MovementAction(self.xdir, self.ydir).apply(engine, entity) + return MovementAction(self.entity, self.xdir, self.ydir).apply() diff --git a/source/engine.py b/source/engine.py index b1aaab6..e584dfe 100644 --- a/source/engine.py +++ b/source/engine.py @@ -1,42 +1,41 @@ -from typing import Any, Iterable - from tcod.context import Context from tcod.console import Console from tcod.map import compute_fov -from entity import Entity import entity_types from floor_map import FloorMap #TODO: replace with "DungeonMap" class Engine: def __init__(self, floor_map: FloorMap): - from event_handler import EventHandler #here to prevent circular imports + from event_handler import EventHandler self.event_handler = EventHandler(self) self.floor_map = floor_map + self.floor_map.engine = self #references everywhere! - #spawn the player object - self.player = entity_types.player.clone(self.floor_map.procgen_cache["spawn_x"], self.floor_map.procgen_cache["spawn_y"]) - self.floor_map.entities.add(self.player) + #grab the player object + self.player = self.floor_map.player #kick off the render self.update_fov() - def handle_events(self, events: Iterable[Any]) -> None: - for event in events: - action = self.event_handler.dispatch(event) - - if action is None: - continue - - action.apply(self, self.player) - self.handle_entities() - - self.update_fov() #update before the next action - def handle_entities(self) -> None: - for entity in self.floor_map.entities - {self.player}: - pass #run entity AI + self.update_fov() #knowing the FOV lets entities mess with it + #all entities in the level + for entity in self.floor_map.entities - {self.player}: + pass #TODO: run entity AI + + def handle_rendering(self, context: Context, console: Console) -> None: + #map and all entities within + self.floor_map.render(console) + + #TODO: UI + + #send to the screen + context.present(console) + console.clear() + + #utils def update_fov(self): self.floor_map.visible[:] = compute_fov( self.floor_map.tiles["transparent"], @@ -46,12 +45,3 @@ class Engine: #add the visible tiles to the explored list self.floor_map.explored |= self.floor_map.visible - - def render(self, context: Context, console: Console) -> None: - self.floor_map.render(console) - - console.print(self.player.x, self.player.y, self.player.char, fg=self.player.color) - - #send to the screen - context.present(console) - console.clear() diff --git a/source/entity.py b/source/entity.py index 65b8ad8..1d9ee2c 100644 --- a/source/entity.py +++ b/source/entity.py @@ -1,9 +1,7 @@ from __future__ import annotations import copy -from typing import Tuple, TypeVar - -T = TypeVar("T", bound="Entity") +from typing import Tuple class Entity: def __init__( @@ -13,19 +11,23 @@ class Entity: char: str = "?", color: Tuple[int, int, int] = (255, 255, 255), name: str = "", - walkable: bool = True - ): + walkable: bool = True, + floor_map = None, + ): self.x = x self.y = y self.char = char self.color = color self.name = name self.walkable = walkable + self.floor_map = floor_map - def clone(self: T, x: int, y: int) -> T: + def spawn(self: T, x: int, y: int, floor_map): clone = copy.deepcopy(self) clone.x = x clone.y = y + clone.floor_map = floor_map + clone.floor_map.entities.add(clone) return clone def set_pos(self, x: int, y: int) -> None: diff --git a/source/event_handler.py b/source/event_handler.py index e4a2590..aaca902 100644 --- a/source/event_handler.py +++ b/source/event_handler.py @@ -5,30 +5,43 @@ import tcod from actions import Action, QuitAction, MoveAction from engine import Engine +#event handler is one part of the engine class EventHandler(tcod.event.EventDispatch[Action]): def __init__(self, engine: Engine): super().__init__() self.engine = engine + def handle_events(self) -> None: + for event in tcod.event.wait(): + action = self.dispatch(event) + + if action is None: + continue + + action.apply() #entity references the engine + + #callbacks def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]: return QuitAction() def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]: key = event.sym #SDL stuff, neat. - #parse input + player = self.engine.player + + #parse player input match key: case tcod.event.KeySym.ESCAPE: return QuitAction() case tcod.event.KeySym.UP: - return MoveAction(xdir = 0, ydir = -1) + return MoveAction(player, xdir = 0, ydir = -1) case tcod.event.KeySym.DOWN: - return MoveAction(xdir = 0, ydir = 1) + return MoveAction(player, xdir = 0, ydir = 1) case tcod.event.KeySym.LEFT: - return MoveAction(xdir = -1, ydir = 0) + return MoveAction(player, xdir = -1, ydir = 0) case tcod.event.KeySym.RIGHT: - return MoveAction(xdir = 1, ydir = 0) + return MoveAction(player, xdir = 1, ydir = 0) case _: return None \ No newline at end of file diff --git a/source/floor_map.py b/source/floor_map.py index cf4abec..d148297 100644 --- a/source/floor_map.py +++ b/source/floor_map.py @@ -8,7 +8,7 @@ import tile_types from entity import Entity class FloorMap: - def __init__(self, width: int, height: int, entities: Iterable[Entity] = (), ): + def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()): #terrain stuff self.width, self.height = width, height self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") @@ -20,6 +20,10 @@ class FloorMap: self.entities = set(entities) self.procgen_cache = {} #reserved for the procgen algorithm, otherwise ignored + #set externally + self.engine = None + self.player = None + def in_bounds(self, x: int, y: int) -> bool: return 0 <= x < self.width and 0 <= y < self.height diff --git a/source/main.py b/source/main.py index ba97090..b4f7f74 100755 --- a/source/main.py +++ b/source/main.py @@ -5,13 +5,11 @@ from engine import Engine from procgen import generate_floor_map def main() -> None: - #assets - tileset = tcod.tileset.load_tilesheet("assets/dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD) - + #tcod stuff context = tcod.context.new( columns = 80, rows = 45, - tileset = tileset, + tileset = tcod.tileset.load_tilesheet("assets/dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD), title = "Stepwise Roguelike", vsync = True ) @@ -25,14 +23,15 @@ def main() -> None: ) engine = Engine( + #is created externally, because floor_map = generate_floor_map(80, 45, 10, 10) ) # game loop while True: - engine.render(context, console) - events = tcod.event.wait() - engine.handle_events(events) + engine.event_handler.handle_events() + engine.handle_entities() + engine.handle_rendering(context, console) # this seems odd to me if __name__ == "__main__": diff --git a/source/procgen.py b/source/procgen.py index 7a2136d..6e17ca3 100644 --- a/source/procgen.py +++ b/source/procgen.py @@ -51,7 +51,7 @@ class RectangularRoom: self.y2 >= other.y1 ) -def spawn_monsters(room: RectangularRoom, floor_map: FloorMap, room_monsters_max: int) -> None: +def spawn_monsters(floor_map: FloorMap, room: RectangularRoom, room_monsters_max: int) -> None: monster_count = random.randint(0, room_monsters_max) #There can only be one @@ -60,18 +60,19 @@ def spawn_monsters(room: RectangularRoom, floor_map: FloorMap, room_monsters_max for i in range(monster_count): #admittedly weird layout here, because player isn't in the entities list yet - x, y = floor_map.procgen_cache["spawn_x"], floor_map.procgen_cache["spawn_y"] - while x == floor_map.procgen_cache["spawn_x"] and y == floor_map.procgen_cache["spawn_y"]: + x, y = floor_map.player.x, floor_map.player.y + while x == floor_map.player.x and y == floor_map.player.y: x = random.randint(room.x1 + 1, room.x2 - 1) y = random.randint(room.y1 + 1, room.y2 - 1) #if there's no entity at that position if not any(entity.x == x and entity.y == y for entity in floor_map.entities): + #there's never more than one red gobbo, but there can be none at all if not floor_map.procgen_cache["gobbo_red"] and random.random() < 0.2: floor_map.procgen_cache["gobbo_red"] = True - floor_map.entities.add(entity_types.gobbo_red.clone(x, y)) + entity_types.gobbo_red.spawn(x, y, floor_map) else: - floor_map.entities.add(entity_types.gobbo.clone(x, y)) + entity_types.gobbo.spawn(x, y, floor_map) #generators def generate_floor_map( @@ -99,17 +100,20 @@ def generate_floor_map( new_room = RectangularRoom(x, y, room_width, room_height) if any(new_room.intersects(other_room) for other_room in rooms): - continue + continue #nope.jpg floor_map.tiles[new_room.inner] = tile_types.floor if len(rooms) == 0: - floor_map.procgen_cache["spawn_x"], floor_map.procgen_cache["spawn_y"] = new_room.center + x, y = new_room.center + floor_map.player = entity_types.player.spawn(x, y, floor_map) + + floor_map.entities.add(floor_map.player) #get it working first else: for x, y in make_corridor(rooms[-1].center, new_room.center): floor_map.tiles[x, y] = tile_types.floor - spawn_monsters(new_room, floor_map, room_monsters_max) + spawn_monsters(floor_map, new_room, room_monsters_max) rooms.append(new_room)