diff --git a/roguebasin/__main__.py b/roguebasin/__main__.py index eeb6912..8232c2a 100644 --- a/roguebasin/__main__.py +++ b/roguebasin/__main__.py @@ -64,9 +64,9 @@ def main(argv): tileset = tcod.tileset.load_tilesheet(font, 16, 16, tcod.tileset.CHARMAP_CP437) console = tcod.Console(CONSOLE_WIDTH, CONSOLE_HEIGHT, order='F') - event_handler = EventHandler() 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: while True: @@ -74,8 +74,7 @@ def main(argv): engine.print_to_console(console) context.present(console) - for event in tcod.event.wait(): - engine.handle_event(event) + event_handler.wait_for_events() def run_until_exit(): ''' diff --git a/roguebasin/actions.py b/roguebasin/actions.py index 3b03c8f..5e849fa 100644 --- a/roguebasin/actions.py +++ b/roguebasin/actions.py @@ -71,15 +71,16 @@ class ActionResult: class Action: '''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. Parameters ---------- engine : Engine The game engine - entity : Entity - The entity that this action is being performed on Returns ------- @@ -95,21 +96,21 @@ class Action: class ExitAction(Action): '''Exit the game.''' - def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: + def perform(self, engine: 'Engine') -> ActionResult: raise SystemExit() class RegenerateRoomsAction(Action): '''Regenerate the dungeon map''' - def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: + def perform(self, engine: 'Engine') -> ActionResult: return ActionResult(self, success=False) # pylint: disable=abstract-method class MoveAction(Action): '''An abstract Action that requires a direction to complete.''' - def __init__(self, direction: Direction): - super().__init__() + def __init__(self, entity: Entity, direction: Direction): + super().__init__(entity) self.direction = direction def __repr__(self): @@ -126,8 +127,8 @@ class BumpAction(MoveAction): The direction to test ''' - def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: - new_position = entity.position + self.direction + def perform(self, engine: 'Engine') -> ActionResult: + new_position = self.entity.position + self.direction position_is_in_bounds = engine.map.tile_is_in_bounds(new_position) position_is_walkable = engine.map.tile_is_walkable(new_position) @@ -140,7 +141,8 @@ class BumpAction(MoveAction): else: 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, position_is_in_bounds, position_is_walkable, @@ -150,29 +152,29 @@ class BumpAction(MoveAction): return ActionResult(self, success=False) 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): '''Walk one step in the given direction.''' - def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: - new_position = entity.position + self.direction + def perform(self, engine: 'Engine') -> ActionResult: + new_position = self.entity.position + self.direction - LOG.info('Moving %s to %s', entity, new_position) - entity.position = new_position + LOG.info('Moving %s to %s', self.entity, new_position) + self.entity.position = new_position return ActionResult(self, success=True) class MeleeAction(MoveAction): '''Perform a melee attack on another entity''' - def __init__(self, direction: Direction, target: Entity): - super().__init__(direction) + def __init__(self, entity: Entity, direction: Direction, target: Entity): + super().__init__(entity, direction) self.target = target - def perform(self, engine: 'Engine', entity: Entity) -> ActionResult: + def perform(self, engine: 'Engine') -> ActionResult: LOG.info('Attack! %s', self.target) return ActionResult(self, success=True) diff --git a/roguebasin/engine.py b/roguebasin/engine.py index a253cfb..9c9c714 100644 --- a/roguebasin/engine.py +++ b/roguebasin/engine.py @@ -34,8 +34,6 @@ class Engine: Defines the basic configuration for the game entities : MutableSet[Entity] 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 The hero, the Entity controlled by the player map : Map @@ -44,8 +42,7 @@ class Engine: A random number generator ''' - def __init__(self, event_handler: EventHandler, configuration: Configuration): - self.event_handler = event_handler + def __init__(self, configuration: Configuration): self.configuration = configuration self.rng = tcod.random.Random() @@ -79,51 +76,6 @@ class Engine: 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): '''Print the whole game to the given console.''' self.map.print_to_console(console) diff --git a/roguebasin/events.py b/roguebasin/events.py index d729567..55ac392 100644 --- a/roguebasin/events.py +++ b/roguebasin/events.py @@ -2,40 +2,112 @@ '''Defines event handling mechanisms.''' -from typing import Optional +import logging +import random +from typing import MutableSet, Optional, TYPE_CHECKING import tcod -from .actions import Action, ExitAction, RegenerateRoomsAction, BumpAction +from .actions import Action, ActionResult, ExitAction, RegenerateRoomsAction, BumpAction 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]): '''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]: return ExitAction() def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[Action]: action: Optional[Action] = None + hero = self.engine.hero + sym = event.sym match sym: case tcod.event.KeySym.b: - action = BumpAction(Direction.SouthWest) + action = BumpAction(hero, Direction.SouthWest) case tcod.event.KeySym.h: - action = BumpAction(Direction.West) + action = BumpAction(hero, Direction.West) case tcod.event.KeySym.j: - action = BumpAction(Direction.South) + action = BumpAction(hero, Direction.South) case tcod.event.KeySym.k: - action = BumpAction(Direction.North) + action = BumpAction(hero, Direction.North) case tcod.event.KeySym.l: - action = BumpAction(Direction.East) + action = BumpAction(hero, Direction.East) case tcod.event.KeySym.n: - action = BumpAction(Direction.SouthEast) + action = BumpAction(hero, Direction.SouthEast) case tcod.event.KeySym.u: - action = BumpAction(Direction.NorthEast) + action = BumpAction(hero, Direction.NorthEast) case tcod.event.KeySym.y: - action = BumpAction(Direction.NorthWest) + action = BumpAction(hero, Direction.NorthWest) case tcod.event.KeySym.SPACE: action = RegenerateRoomsAction()