#!/usr/bin/env python3 # Eryn Wells ''' This module defines all of the actions that can be performed by the game. These actions can come from the player (e.g. via keyboard input), or from non-player entities (e.g. AI deciboard input), or from non-player entities (e.g. AI decisions). Class Hierarchy --------------- Action : Base class of all actions MoveAction : Base class for all actions that are performed with a direction BumpAction WalkAction MeleeAction ExitAction WaitAction ''' import logging from typing import Optional, TYPE_CHECKING from . import items from .geometry import Direction from .object import Actor, Item if TYPE_CHECKING: from .engine import Engine LOG = logging.getLogger(__name__) class ActionResult: '''The result of an Action. `Action.perform()` returns an instance of this class to inform the caller of the result Attributes ---------- action : Action The Action that was performed success : bool, optional True if the action succeeded done : bool, optional True if the action is complete, and no follow-up action is needed alternate : Action, optional An alternate action to perform if this action failed ''' def __init__(self, action: 'Action', *, success: Optional[bool] = None, done: Optional[bool] = None, alternate: Optional['Action'] = None): self.action = action self.alternate = alternate if success is not None: self.success = success elif alternate: self.success = False else: self.success = True if done is not None: self.done = done elif self.success: self.done = True else: self.done = not alternate def __repr__(self): return f'{self.__class__.__name__}({self.action!r}, success={self.success}, done={self.done}, alternate={self.alternate!r})' class Action: '''An action that an Entity should perform.''' def __init__(self, actor: Optional[Actor]): self.actor = actor def perform(self, engine: 'Engine') -> ActionResult: '''Perform this action. Parameters ---------- engine : Engine The game engine Returns ------- ActionResult A result object reflecting how the action was handled, and what follow-up actions, if any, are needed to complete the action. ''' raise NotImplementedError() def failure(self) -> ActionResult: '''Create an ActionResult indicating failure with no follow-up''' return ActionResult(self, success=False) def success(self) -> ActionResult: '''Create an ActionResult indicating success with no follow-up''' return ActionResult(self, success=True) def __repr__(self): return f'{self.__class__.__name__}({self.actor!r})' class ExitAction(Action): '''Exit the game.''' def perform(self, engine: 'Engine') -> ActionResult: raise SystemExit() class RegenerateRoomsAction(Action): '''Regenerate the dungeon map''' 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, actor: Actor, direction: Direction): super().__init__(actor) self.direction = direction def __repr__(self): return f'{self.__class__.__name__}({self.actor!r}, {self.direction!r})' class BumpAction(MoveAction): '''Attempt to perform a movement action in a direction. This action tests if an action in the direction is possible and returns the action that can be completed. Attributes ---------- direction : Direction The direction to test ''' def perform(self, engine: 'Engine') -> ActionResult: new_position = self.actor.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) for ent in engine.entities: if new_position != ent.position: continue entity_occupying_position = ent break else: entity_occupying_position = None LOG.info('Bumping %s into %s (in_bounds:%s walkable:%s overlaps:%s)', self.actor, new_position, position_is_in_bounds, position_is_walkable, entity_occupying_position) if not position_is_in_bounds or not position_is_walkable: return self.failure() if entity_occupying_position and entity_occupying_position.blocks_movement: return ActionResult(self, alternate=MeleeAction(self.actor, self.direction, entity_occupying_position)) return ActionResult(self, alternate=WalkAction(self.actor, self.direction)) class WalkAction(MoveAction): '''Walk one step in the given direction.''' def perform(self, engine: 'Engine') -> ActionResult: new_position = self.actor.position + self.direction LOG.info('Moving %s to %s', self.actor, new_position) self.actor.position = new_position return self.success() class MeleeAction(MoveAction): '''Perform a melee attack on another Actor''' def __init__(self, actor: Actor, direction: Direction, target: Actor): super().__init__(actor, direction) self.target = target def perform(self, engine: 'Engine') -> ActionResult: if not self.target: return self.failure() if not self.actor.fighter or not self.target.fighter: return self.failure() damage = self.actor.fighter.attack_power - self.target.fighter.defense if damage > 0: LOG.info('%s attacks %s for %d damage!', self.actor, self.target, damage) self.target.fighter.hit_points -= damage else: LOG.info('%s attacks %s but does no damage!', self.actor, self.target) if self.target.fighter.is_dead: LOG.info('%s is dead!', self.target) return ActionResult(self, alternate=DieAction(self.target)) return self.success() class WaitAction(Action): '''Wait a turn''' def perform(self, engine: 'Engine') -> ActionResult: LOG.info('%s is waiting a turn', self.actor) return self.success() class DieAction(Action): '''Kill an Actor''' def perform(self, engine: 'Engine') -> ActionResult: LOG.info('%s dies', self.actor) engine.entities.remove(self.actor) if self.actor.yields_corpse_on_death: LOG.info('%s leaves a corpse behind', self.actor) corpse = Item(kind=items.Corpse, name=f'{self.actor.name} Corpse', position=self.actor.position) return ActionResult(self, alternate=DropItemAction(self.actor, corpse)) return self.success() class DropItemAction(Action): '''Drop an item''' def __init__(self, actor: 'Actor', item: 'Item'): super().__init__(actor) self.item = item def perform(self, engine: 'Engine') -> ActionResult: engine.entities.add(self.item) return self.success()