Refactor event handling into EventHandler

Move all the event handling code from Engine to EventHandler. EventHandler has a
reference to Engine and can deal with entities from its methods.

Refactor Action to take an optional Entity in its initializer. Some actions
don't require an Entity, but many do/will.
This commit is contained in:
Eryn Wells 2022-05-07 12:25:46 -07:00
parent d75c9faea3
commit 8b3c0137a5
4 changed files with 107 additions and 82 deletions

View file

@ -64,9 +64,9 @@ def main(argv):
tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437) tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437)
console = tcod.Console(CONSOLE_WIDTH, CONSOLE_HEIGHT, order='F') console = tcod.Console(CONSOLE_WIDTH, CONSOLE_HEIGHT, order='F')
event_handler = EventHandler()
configuration = Configuration(map_size=Size(MAP_WIDTH, MAP_HEIGHT)) configuration = Configuration(map_size=Size(MAP_WIDTH, MAP_HEIGHT))
engine = Engine(event_handler, configuration) engine = Engine(configuration)
event_handler = EventHandler(engine)
with tcod.context.new(columns=console.width, rows=console.height, tileset=tileset) as context: with tcod.context.new(columns=console.width, rows=console.height, tileset=tileset) as context:
while True: while True:
@ -74,8 +74,7 @@ def main(argv):
engine.print_to_console(console) engine.print_to_console(console)
context.present(console) context.present(console)
for event in tcod.event.wait(): event_handler.wait_for_events()
engine.handle_event(event)
def run_until_exit(): def run_until_exit():
''' '''

View file

@ -71,15 +71,16 @@ class ActionResult:
class Action: class Action:
'''An action that an Entity should perform.''' '''An action that an Entity should perform.'''
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def __init__(self, entity: Optional[Entity] = None):
self.entity = entity
def perform(self, engine: 'Engine') -> ActionResult:
'''Perform this action. '''Perform this action.
Parameters Parameters
---------- ----------
engine : Engine engine : Engine
The game engine The game engine
entity : Entity
The entity that this action is being performed on
Returns Returns
------- -------
@ -95,21 +96,21 @@ class Action:
class ExitAction(Action): class ExitAction(Action):
'''Exit the game.''' '''Exit the game.'''
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
raise SystemExit() raise SystemExit()
class RegenerateRoomsAction(Action): class RegenerateRoomsAction(Action):
'''Regenerate the dungeon map''' '''Regenerate the dungeon map'''
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
return ActionResult(self, success=False) return ActionResult(self, success=False)
# pylint: disable=abstract-method # pylint: disable=abstract-method
class MoveAction(Action): class MoveAction(Action):
'''An abstract Action that requires a direction to complete.''' '''An abstract Action that requires a direction to complete.'''
def __init__(self, direction: Direction): def __init__(self, entity: Entity, direction: Direction):
super().__init__() super().__init__(entity)
self.direction = direction self.direction = direction
def __repr__(self): def __repr__(self):
@ -126,8 +127,8 @@ class BumpAction(MoveAction):
The direction to test The direction to test
''' '''
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
new_position = entity.position + self.direction new_position = self.entity.position + self.direction
position_is_in_bounds = engine.map.tile_is_in_bounds(new_position) position_is_in_bounds = engine.map.tile_is_in_bounds(new_position)
position_is_walkable = engine.map.tile_is_walkable(new_position) position_is_walkable = engine.map.tile_is_walkable(new_position)
@ -140,7 +141,8 @@ class BumpAction(MoveAction):
else: else:
entity_occupying_position = None entity_occupying_position = None
LOG.info('Bumping %s (in_bounds:%s walkable:%s overlaps:%s)', LOG.info('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)',
self.entity,
new_position, new_position,
position_is_in_bounds, position_is_in_bounds,
position_is_walkable, position_is_walkable,
@ -150,29 +152,29 @@ class BumpAction(MoveAction):
return ActionResult(self, success=False) return ActionResult(self, success=False)
if entity_occupying_position: if entity_occupying_position:
return ActionResult(self, alternate=MeleeAction(self.direction, entity_occupying_position)) return ActionResult(self, alternate=MeleeAction(self.entity, self.direction, entity_occupying_position))
return ActionResult(self, alternate=WalkAction(self.direction)) return ActionResult(self, alternate=WalkAction(self.entity, self.direction))
class WalkAction(MoveAction): class WalkAction(MoveAction):
'''Walk one step in the given direction.''' '''Walk one step in the given direction.'''
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
new_position = entity.position + self.direction new_position = self.entity.position + self.direction
LOG.info('Moving %s to %s', entity, new_position) LOG.info('Moving %s to %s', self.entity, new_position)
entity.position = new_position self.entity.position = new_position
return ActionResult(self, success=True) return ActionResult(self, success=True)
class MeleeAction(MoveAction): class MeleeAction(MoveAction):
'''Perform a melee attack on another entity''' '''Perform a melee attack on another entity'''
def __init__(self, direction: Direction, target: Entity): def __init__(self, entity: Entity, direction: Direction, target: Entity):
super().__init__(direction) super().__init__(entity, direction)
self.target = target self.target = target
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: def perform(self, engine: 'Engine') -> ActionResult:
LOG.info('Attack! %s', self.target) LOG.info('Attack! %s', self.target)
return ActionResult(self, success=True) return ActionResult(self, success=True)

View file

@ -34,8 +34,6 @@ class Engine:
Defines the basic configuration for the game Defines the basic configuration for the game
entities : MutableSet[Entity] entities : MutableSet[Entity]
A set of all the entities on the current map, including the Hero A set of all the entities on the current map, including the Hero
event_handler : EventHandler
An event handler object that can handle events from `tcod`
hero : Hero hero : Hero
The hero, the Entity controlled by the player The hero, the Entity controlled by the player
map : Map map : Map
@ -44,8 +42,7 @@ class Engine:
A random number generator A random number generator
''' '''
def __init__(self, event_handler: EventHandler, configuration: Configuration): def __init__(self, configuration: Configuration):
self.event_handler = event_handler
self.configuration = configuration self.configuration = configuration
self.rng = tcod.random.Random() self.rng = tcod.random.Random()
@ -79,51 +76,6 @@ class Engine:
self.update_field_of_view() self.update_field_of_view()
def handle_event(self, event: tcod.event.Event):
'''Handle the specified event. Transform that event into an Action via an EventHandler and perform it.'''
action = self.event_handler.dispatch(event)
if not action:
return
result = action.perform(self, self.hero)
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
while not result.done:
alternate = result.alternate
assert alternate is not None, f'Action {result.action} incomplete but no alternate action given'
result = alternate.perform(self, self.hero)
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
if result.success:
LOG.info('Action succeded!')
break
if result.done:
LOG.info('Action failed!')
break
if not result.success and result.done:
return
directions = list(Direction.all())
moved_entities: MutableSet[Entity] = {self.hero}
for ent in self.entities:
if ent == self.hero:
continue
while True:
new_position = ent.position + random.choice(directions)
overlaps_with_previously_moved_entity = any(new_position == moved_ent.position for moved_ent in moved_entities)
if not overlaps_with_previously_moved_entity and self.map.tile_is_walkable(new_position):
ent.position = new_position
moved_entities.add(ent)
break
self.update_field_of_view()
def print_to_console(self, console): def print_to_console(self, console):
'''Print the whole game to the given console.''' '''Print the whole game to the given console.'''
self.map.print_to_console(console) self.map.print_to_console(console)

View file

@ -2,40 +2,112 @@
'''Defines event handling mechanisms.''' '''Defines event handling mechanisms.'''
from typing import Optional import logging
import random
from typing import MutableSet, Optional, TYPE_CHECKING
import tcod import tcod
from .actions import Action, ExitAction, RegenerateRoomsAction, BumpAction from .actions import Action, ActionResult, ExitAction, RegenerateRoomsAction, BumpAction
from .geometry import Direction from .geometry import Direction
from .object import Entity
if TYPE_CHECKING:
from .engine import Engine
LOG = logging.getLogger('events')
class EventHandler(tcod.event.EventDispatch[Action]): class EventHandler(tcod.event.EventDispatch[Action]):
'''Handler of `tcod` events''' '''Handler of `tcod` events'''
def __init__(self, engine: 'Engine'):
super().__init__()
self.engine = engine
def wait_for_events(self):
'''Wait for events and handle them.'''
for event in tcod.event.wait():
self.handle_event(event)
def handle_event(self, event: tcod.event.Event) -> None:
'''Handle the given event. Transform that event into an Action via an EventHandler and perform it.'''
action = self.dispatch(event)
if not action:
return
result = self.perform_action_until_done(action)
# Action failed, so do nothing further.
if not result.success and result.done:
return
directions = list(Direction.all())
hero = self.engine.hero
moved_entities: MutableSet[Entity] = {self.engine.hero}
for ent in self.engine.entities:
if ent == hero:
continue
while True:
new_position = ent.position + random.choice(directions)
overlaps_with_previously_moved_entity = any(new_position == moved_ent.position for moved_ent in moved_entities)
tile_is_walkable = self.engine.map.tile_is_walkable(new_position)
if not overlaps_with_previously_moved_entity and tile_is_walkable:
ent.position = new_position
moved_entities.add(ent)
break
self.engine.update_field_of_view()
def perform_action_until_done(self, action: Action) -> ActionResult:
'''Perform the given action and any alternate follow-up actions until the action chain is done.'''
result = action.perform(self.engine)
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
while not result.done:
alternate = result.alternate
assert alternate is not None, f'Action {result.action} incomplete but no alternate action given'
result = alternate.perform(self.engine)
LOG.debug('Performed action success=%s done=%s alternate=%s', result.success, result.done, result.alternate)
if result.success:
LOG.info('Action succeded!')
break
else:
LOG.info('Action failed!')
return result
def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]: def ev_quit(self, event: tcod.event.Quit) -> Optional[Action]:
return ExitAction() return ExitAction()
def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]: def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]:
action: Optional[Action] = None action: Optional[Action] = None
hero = self.engine.hero
sym = event.sym sym = event.sym
match sym: match sym:
case tcod.event.KeySym.b: case tcod.event.KeySym.b:
action = BumpAction(Direction.SouthWest) action = BumpAction(hero, Direction.SouthWest)
case tcod.event.KeySym.h: case tcod.event.KeySym.h:
action = BumpAction(Direction.West) action = BumpAction(hero, Direction.West)
case tcod.event.KeySym.j: case tcod.event.KeySym.j:
action = BumpAction(Direction.South) action = BumpAction(hero, Direction.South)
case tcod.event.KeySym.k: case tcod.event.KeySym.k:
action = BumpAction(Direction.North) action = BumpAction(hero, Direction.North)
case tcod.event.KeySym.l: case tcod.event.KeySym.l:
action = BumpAction(Direction.East) action = BumpAction(hero, Direction.East)
case tcod.event.KeySym.n: case tcod.event.KeySym.n:
action = BumpAction(Direction.SouthEast) action = BumpAction(hero, Direction.SouthEast)
case tcod.event.KeySym.u: case tcod.event.KeySym.u:
action = BumpAction(Direction.NorthEast) action = BumpAction(hero, Direction.NorthEast)
case tcod.event.KeySym.y: case tcod.event.KeySym.y:
action = BumpAction(Direction.NorthWest) action = BumpAction(hero, Direction.NorthWest)
case tcod.event.KeySym.SPACE: case tcod.event.KeySym.SPACE:
action = RegenerateRoomsAction() action = RegenerateRoomsAction()