diff --git a/actions.py b/actions.py index a54096c..037f339 100644 --- a/actions.py +++ b/actions.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Optional, Tuple, TYPE_CHECKING if TYPE_CHECKING: from engine import Engine @@ -8,12 +8,21 @@ if TYPE_CHECKING: class Action: - def perform(self, engine: Engine, entity: Entity) -> None: + def __init__(self, entity: Entity) -> None: + super().__init__() + self.entity = entity + + @property + def engine(self) -> Engine: + """Return the engine this action belongs to.""" + return self.entity.gamemap.engine + + def perform(self) -> None: """Perform this action with the objects needed to determine its scope. - `engine` is the scope this action is being performed in. + `self.engine` is the scope this action is being performed in. - `entity` is the object performing the action. + `self.entity` is the object performing the action. This method must be overwritten by Action subclasses. """ @@ -21,26 +30,34 @@ class Action: class EscapeAction(Action): - def perform(self, engine: Engine, entity: Entity) -> None: + def perform(self) -> None: raise SystemExit() class ActionWithDirection(Action): - def __init__(self, dx: int, dy: int): - super().__init__() + def __init__(self, entity, dx: int, dy: int): + super().__init__(entity) self.dx = dx self.dy = dy - def perform(self, engine: Engine, entity: Entity) -> None: + @property + def dest_xy(self) -> Tuple[int, int]: + """Returns this action's destination.""" + return self.entity.x + self.dx, self.entity.y + self.dy + + @property + def blocking_entity(self) -> Optional[Entity]: + """Return the blocking entity at this action's destination.""" + return self.engine.game_map.get_blocking_entity_at_location(*self.dest_xy) + + def perform(self) -> None: raise NotImplementedError() class MeleeAction(ActionWithDirection): - def perform(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.dx - dest_y = entity.y + self.dy - target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y) + def perform(self) -> None: + target = self.blocking_entity if not target: return # No entity to attack. @@ -48,26 +65,22 @@ class MeleeAction(ActionWithDirection): class MovementAction(ActionWithDirection): - def perform(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.dx - dest_y = entity.y + self.dy + def perform(self) -> None: + dest_x, dest_y = self.dest_xy - if not engine.game_map.in_bounds(dest_x, dest_y): + if not self.engine.game_map.in_bounds(dest_x, dest_y): return # Destination is out of bounds - if not engine.game_map.tiles["walkable"][dest_x, dest_y]: + if not self.engine.game_map.tiles["walkable"][dest_x, dest_y]: return # Destination is blocked by a tile. - if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y): + if self.engine.game_map.get_blocking_entity_at_location(dest_x, dest_y): return # Destination is blocked by an entity. - entity.move(self.dx, self.dy) + self.entity.move(self.dx, self.dy) class BumpAction(ActionWithDirection): - def perform(self, engine: Engine, entity: Entity) -> None: - dest_x = entity.x + self.dx - dest_y = entity.y + self.dy - - if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y): - return MeleeAction(self.dx, self.dy).perform(engine, entity) + def perform(self) -> None: + if self.blocking_entity: + return MeleeAction(self.entity, self.dx, self.dy).perform() else: - return MovementAction(self.dx, self.dy).perform(engine, entity) + return MovementAction(self.entity, self.dx, self.dy).perform() diff --git a/engine.py b/engine.py index aa25512..056a720 100644 --- a/engine.py +++ b/engine.py @@ -1,43 +1,32 @@ -from typing import Iterable, Any +from __future__ import annotations + +from typing import TYPE_CHECKING from tcod.context import Context from tcod.console import Console from tcod.map import compute_fov -from entity import Entity -from game_map import GameMap from input_handlers import EventHandler +if TYPE_CHECKING: + from entity import Entity + from game_map import GameMap + class Engine: + game_map: GameMap + def __init__( self, - event_handler: EventHandler, - game_map: GameMap, player: Entity ): - self.event_handler = event_handler - self.game_map = game_map + self.event_handler: EventHandler = EventHandler(self) self.player = player - self.update_fov() def handle_enemy_turns(self) -> None: for entity in self.game_map.entities - {self.player}: print(f'The {entity.name} wonders when it will get to take a real turn.') - def handle_events(self, events: Iterable[Any]) -> None: - for event in events: - action = self.event_handler.dispatch(event) - - if action is None: - continue - - action.perform(self, self.player) - self.handle_enemy_turns() - - # Update the FOV before the player's next action. - self.update_fov() - def update_fov(self) -> None: """Recompute the visible area based on the player's point of view.""" self.game_map.visible[:] = compute_fov( diff --git a/entity.py b/entity.py index b171549..615c66f 100644 --- a/entity.py +++ b/entity.py @@ -1,7 +1,7 @@ from __future__ import annotations import copy -from typing import Tuple, TypeVar, TYPE_CHECKING +from typing import Optional, Tuple, TypeVar, TYPE_CHECKING if TYPE_CHECKING: from game_map import GameMap @@ -14,8 +14,11 @@ class Entity: A generic object to represent players, enemies, items, etc. """ + gamemap: GameMap + def __init__( self, + gamemap: Optional[GameMap] = None, x: int = 0, y: int = 0, char: str = "?", @@ -29,15 +32,31 @@ class Entity: self.color = color self.name = name self.blocks_movement = blocks_movement + if gamemap: + # If gamemap isn't provided now, it will be later. + self.gamemap = gamemap + gamemap.entities.add(self) def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T: """Spawn a copy of this instance at the given location.""" clone = copy.deepcopy(self) clone.x = x clone.y = y + clone.gamemap = gamemap gamemap.entities.add(clone) return clone + def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None: + """Place this entity at a new location. Handles moving across GameMaps.""" + self.x = x + self.y = y + if gamemap: + if hasattr(self, "gamemap"): # Possibly uninitialized + self.gamemap.entities.remove(self) + + self.gamemap = gamemap + gamemap.entities.add(self) + def move(self, dx: int, dy: int): # Move the entity by a given amount self.x += dx diff --git a/game_map.py b/game_map.py index 1bef8c4..53df5d7 100644 --- a/game_map.py +++ b/game_map.py @@ -8,21 +8,45 @@ from tcod.console import Console import tile_types if TYPE_CHECKING: + from engine import Engine from entity import Entity class GameMap: - def __init__(self, width: int, height: int, entities: Iterable[Entity] = ()): + def __init__( + self, + engine: Engine, + width: int, + height: int, + entities: Iterable[Entity] = () + ): + self.engine = engine self.width, self.height = width, height self.entities = set(entities) self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") - self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see - self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before + self.visible = np.full( + (width, height), + fill_value=False, + order="F" + ) # Tiles the player can currently see + self.explored = np.full( + (width, height), + fill_value=False, + order="F" + ) # Tiles the player has seen before - def get_blocking_entity_at_location(self, location_x: int, location_y: int) -> Optional[Entity]: + def get_blocking_entity_at_location( + self, + location_x: int, + location_y: int + ) -> Optional[Entity]: for entity in self.entities: - if entity.blocks_movement and entity.x == location_x and entity.y == location_y: + if ( + entity.blocks_movement + and entity.x == location_x + and entity.y == location_y + ): return entity return None @@ -44,7 +68,7 @@ class GameMap: console.tiles_rgb[0: self.width, 0: self.height] = np.select( condlist=[self.visible, self.explored], choicelist=[self.tiles["light"], self.tiles["dark"]], - default=tile_types.SHROUD + default=tile_types.SHROUD, ) for entity in self.entities: diff --git a/input_handlers.py b/input_handlers.py index d33e672..17fedd7 100644 --- a/input_handlers.py +++ b/input_handlers.py @@ -1,11 +1,31 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING import tcod.event from actions import Action, EscapeAction, BumpAction +if TYPE_CHECKING: + from engine import Engine + class EventHandler(tcod.event.EventDispatch[Action]): + def __init__(self, engine: Engine): + self.engine = engine + + def handle_events(self) -> None: + for event in tcod.event.wait(): + action = self.dispatch(event) + + if action is None: + continue + + action.perform() + + self.engine.handle_enemy_turns() + self.engine.update_fov() + def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]: raise SystemExit() @@ -14,17 +34,19 @@ class EventHandler(tcod.event.EventDispatch[Action]): key = event.sym + player = self.engine.player + if key == tcod.event.K_UP: - action = BumpAction(dx=0, dy=-1) + action = BumpAction(player, dx=0, dy=-1) elif key == tcod.event.K_DOWN: - action = BumpAction(dx=0, dy=1) + action = BumpAction(player, dx=0, dy=1) elif key == tcod.event.K_LEFT: - action = BumpAction(dx=-1, dy=0) + action = BumpAction(player, dx=-1, dy=0) elif key == tcod.event.K_RIGHT: - action = BumpAction(dx=1, dy=0) + action = BumpAction(player, dx=1, dy=0) elif key == tcod.event.K_ESCAPE: - action = EscapeAction() + action = EscapeAction(player) # No valid key was pressed return action diff --git a/main.py b/main.py index 0b12831..74719ca 100755 --- a/main.py +++ b/main.py @@ -5,7 +5,6 @@ import tcod from engine import Engine import entity_factories -from input_handlers import EventHandler from procgen import generate_dungeon @@ -29,21 +28,20 @@ def main() -> None: tcod.tileset.CHARMAP_TCOD ) - event_handler = EventHandler() - player = copy.deepcopy(entity_factories.player) - game_map = generate_dungeon( + engine = Engine(player) + + engine.game_map = generate_dungeon( max_rooms, room_min_size, room_max_size, map_width, map_height, max_monsters_per_room, - player, + engine, ) - - engine = Engine(event_handler, game_map, player) + engine.update_fov() with tcod.context.new_terminal( screen_width, @@ -56,9 +54,7 @@ def main() -> None: while True: engine.render(root_console, context) - events = tcod.event.wait() - - engine.handle_events(events) + engine.event_handler.handle_events() if __name__ == "__main__": diff --git a/procgen.py b/procgen.py index d2343af..07dbc06 100644 --- a/procgen.py +++ b/procgen.py @@ -10,7 +10,7 @@ from game_map import GameMap import tile_types if TYPE_CHECKING: - from entity import Entity + from engine import Engine class RectangularRoom: @@ -86,10 +86,11 @@ def generate_dungeon( map_width: int, map_height: int, max_monsters_per_room: int, - player: Entity, + engine: Engine, ) -> GameMap: """Generate a new dungeon map.""" - dungeon = GameMap(map_width, map_height, entities=[player]) + player = engine.player + dungeon = GameMap(engine, map_width, map_height, entities=[player]) rooms: List[RectangularRoom] = [] @@ -113,7 +114,7 @@ def generate_dungeon( if len(rooms) == 0: # The first room, where the player starts. - player.x, player.y = new_room.center + player.place(*new_room.center, dungeon) else: # All rooms after the first. # Dig out a tunnel between this room and the previous one for x, y in tunnel_between(rooms[-1].center, new_room.center): diff --git a/requirements.txt b/requirements.txt index 04ce4bf..edf1c42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -tcod>=11.14 +tcod>=11.15 numpy>=1.18 \ No newline at end of file