1
0

Refactor EventHandler class to handle its own events, and cut down on deep argument passing

This commit is contained in:
Timothy Warren 2022-01-07 16:25:07 -05:00
parent 0331834090
commit 6556aa518c
8 changed files with 139 additions and 75 deletions

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import Optional, Tuple, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from engine import Engine from engine import Engine
@ -8,12 +8,21 @@ if TYPE_CHECKING:
class Action: 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. """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. This method must be overwritten by Action subclasses.
""" """
@ -21,26 +30,34 @@ class Action:
class EscapeAction(Action): class EscapeAction(Action):
def perform(self, engine: Engine, entity: Entity) -> None: def perform(self) -> None:
raise SystemExit() raise SystemExit()
class ActionWithDirection(Action): class ActionWithDirection(Action):
def __init__(self, dx: int, dy: int): def __init__(self, entity, dx: int, dy: int):
super().__init__() super().__init__(entity)
self.dx = dx self.dx = dx
self.dy = dy 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() raise NotImplementedError()
class MeleeAction(ActionWithDirection): class MeleeAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -> None: def perform(self) -> None:
dest_x = entity.x + self.dx target = self.blocking_entity
dest_y = entity.y + self.dy
target = engine.game_map.get_blocking_entity_at_location(dest_x, dest_y)
if not target: if not target:
return # No entity to attack. return # No entity to attack.
@ -48,26 +65,22 @@ class MeleeAction(ActionWithDirection):
class MovementAction(ActionWithDirection): class MovementAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -> None: def perform(self) -> None:
dest_x = entity.x + self.dx dest_x, dest_y = self.dest_xy
dest_y = entity.y + self.dy
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 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. 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. return # Destination is blocked by an entity.
entity.move(self.dx, self.dy) self.entity.move(self.dx, self.dy)
class BumpAction(ActionWithDirection): class BumpAction(ActionWithDirection):
def perform(self, engine: Engine, entity: Entity) -> None: def perform(self) -> None:
dest_x = entity.x + self.dx if self.blocking_entity:
dest_y = entity.y + self.dy return MeleeAction(self.entity, self.dx, self.dy).perform()
if engine.game_map.get_blocking_entity_at_location(dest_x, dest_y):
return MeleeAction(self.dx, self.dy).perform(engine, entity)
else: else:
return MovementAction(self.dx, self.dy).perform(engine, entity) return MovementAction(self.entity, self.dx, self.dy).perform()

View File

@ -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.context import Context
from tcod.console import Console from tcod.console import Console
from tcod.map import compute_fov from tcod.map import compute_fov
from entity import Entity
from game_map import GameMap
from input_handlers import EventHandler from input_handlers import EventHandler
if TYPE_CHECKING:
from entity import Entity
from game_map import GameMap
class Engine: class Engine:
game_map: GameMap
def __init__( def __init__(
self, self,
event_handler: EventHandler,
game_map: GameMap,
player: Entity player: Entity
): ):
self.event_handler = event_handler self.event_handler: EventHandler = EventHandler(self)
self.game_map = game_map
self.player = player self.player = player
self.update_fov()
def handle_enemy_turns(self) -> None: def handle_enemy_turns(self) -> None:
for entity in self.game_map.entities - {self.player}: for entity in self.game_map.entities - {self.player}:
print(f'The {entity.name} wonders when it will get to take a real turn.') 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: def update_fov(self) -> None:
"""Recompute the visible area based on the player's point of view.""" """Recompute the visible area based on the player's point of view."""
self.game_map.visible[:] = compute_fov( self.game_map.visible[:] = compute_fov(

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import copy import copy
from typing import Tuple, TypeVar, TYPE_CHECKING from typing import Optional, Tuple, TypeVar, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from game_map import GameMap from game_map import GameMap
@ -14,8 +14,11 @@ class Entity:
A generic object to represent players, enemies, items, etc. A generic object to represent players, enemies, items, etc.
""" """
gamemap: GameMap
def __init__( def __init__(
self, self,
gamemap: Optional[GameMap] = None,
x: int = 0, x: int = 0,
y: int = 0, y: int = 0,
char: str = "?", char: str = "?",
@ -29,15 +32,31 @@ class Entity:
self.color = color self.color = color
self.name = name self.name = name
self.blocks_movement = blocks_movement 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: def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T:
"""Spawn a copy of this instance at the given location.""" """Spawn a copy of this instance at the given location."""
clone = copy.deepcopy(self) clone = copy.deepcopy(self)
clone.x = x clone.x = x
clone.y = y clone.y = y
clone.gamemap = gamemap
gamemap.entities.add(clone) gamemap.entities.add(clone)
return 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): def move(self, dx: int, dy: int):
# Move the entity by a given amount # Move the entity by a given amount
self.x += dx self.x += dx

View File

@ -8,21 +8,45 @@ from tcod.console import Console
import tile_types import tile_types
if TYPE_CHECKING: if TYPE_CHECKING:
from engine import Engine
from entity import Entity from entity import Entity
class GameMap: 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.width, self.height = width, height
self.entities = set(entities) self.entities = set(entities)
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F") 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.visible = np.full(
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before (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: 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 entity
return None return None
@ -44,7 +68,7 @@ class GameMap:
console.tiles_rgb[0: self.width, 0: self.height] = np.select( console.tiles_rgb[0: self.width, 0: self.height] = np.select(
condlist=[self.visible, self.explored], condlist=[self.visible, self.explored],
choicelist=[self.tiles["light"], self.tiles["dark"]], choicelist=[self.tiles["light"], self.tiles["dark"]],
default=tile_types.SHROUD default=tile_types.SHROUD,
) )
for entity in self.entities: for entity in self.entities:

View File

@ -1,11 +1,31 @@
from typing import Optional from __future__ import annotations
from typing import Optional, TYPE_CHECKING
import tcod.event import tcod.event
from actions import Action, EscapeAction, BumpAction from actions import Action, EscapeAction, BumpAction
if TYPE_CHECKING:
from engine import Engine
class EventHandler(tcod.event.EventDispatch[Action]): 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]: def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
raise SystemExit() raise SystemExit()
@ -14,17 +34,19 @@ class EventHandler(tcod.event.EventDispatch[Action]):
key = event.sym key = event.sym
player = self.engine.player
if key == tcod.event.K_UP: 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: 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: 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: 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: elif key == tcod.event.K_ESCAPE:
action = EscapeAction() action = EscapeAction(player)
# No valid key was pressed # No valid key was pressed
return action return action

16
main.py
View File

@ -5,7 +5,6 @@ import tcod
from engine import Engine from engine import Engine
import entity_factories import entity_factories
from input_handlers import EventHandler
from procgen import generate_dungeon from procgen import generate_dungeon
@ -29,21 +28,20 @@ def main() -> None:
tcod.tileset.CHARMAP_TCOD tcod.tileset.CHARMAP_TCOD
) )
event_handler = EventHandler()
player = copy.deepcopy(entity_factories.player) player = copy.deepcopy(entity_factories.player)
game_map = generate_dungeon( engine = Engine(player)
engine.game_map = generate_dungeon(
max_rooms, max_rooms,
room_min_size, room_min_size,
room_max_size, room_max_size,
map_width, map_width,
map_height, map_height,
max_monsters_per_room, max_monsters_per_room,
player, engine,
) )
engine.update_fov()
engine = Engine(event_handler, game_map, player)
with tcod.context.new_terminal( with tcod.context.new_terminal(
screen_width, screen_width,
@ -56,9 +54,7 @@ def main() -> None:
while True: while True:
engine.render(root_console, context) engine.render(root_console, context)
events = tcod.event.wait() engine.event_handler.handle_events()
engine.handle_events(events)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -10,7 +10,7 @@ from game_map import GameMap
import tile_types import tile_types
if TYPE_CHECKING: if TYPE_CHECKING:
from entity import Entity from engine import Engine
class RectangularRoom: class RectangularRoom:
@ -86,10 +86,11 @@ def generate_dungeon(
map_width: int, map_width: int,
map_height: int, map_height: int,
max_monsters_per_room: int, max_monsters_per_room: int,
player: Entity, engine: Engine,
) -> GameMap: ) -> GameMap:
"""Generate a new dungeon map.""" """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] = [] rooms: List[RectangularRoom] = []
@ -113,7 +114,7 @@ def generate_dungeon(
if len(rooms) == 0: if len(rooms) == 0:
# The first room, where the player starts. # 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. else: # All rooms after the first.
# Dig out a tunnel between this room and the previous one # Dig out a tunnel between this room and the previous one
for x, y in tunnel_between(rooms[-1].center, new_room.center): for x, y in tunnel_between(rooms[-1].center, new_room.center):

View File

@ -1,2 +1,2 @@
tcod>=11.14 tcod>=11.15
numpy>=1.18 numpy>=1.18