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 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()

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.console import Console
from tcod.map import compute_fov
from input_handlers import EventHandler
if TYPE_CHECKING:
from entity import Entity
from game_map import GameMap
from input_handlers import EventHandler
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(

View File

@ -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

View File

@ -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:

View File

@ -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

16
main.py
View File

@ -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__":

View File

@ -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):

View File

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