going-rogue/roguebasin/actions.py

187 lines
5.6 KiB
Python

#!/usr/bin/env python3
# Eryn Wells <eryn@erynwells.me>
'''
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
'''
import logging
from typing import Optional, TYPE_CHECKING
from .geometry import Direction
from .object import Entity
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, entity: Optional[Entity] = None):
self.entity = entity
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 __repr__(self):
return f'{self.__class__.__name__}({self.entity!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, entity: Entity, direction: Direction):
super().__init__(entity)
self.direction = direction
def __repr__(self):
return f'{self.__class__.__name__}({self.entity!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.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)
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.entity,
new_position,
position_is_in_bounds,
position_is_walkable,
entity_occupying_position)
if not position_is_in_bounds or not position_is_walkable:
return ActionResult(self, success=False)
if entity_occupying_position:
return ActionResult(self, alternate=MeleeAction(self.entity, self.direction, entity_occupying_position))
return ActionResult(self, alternate=WalkAction(self.entity, self.direction))
class WalkAction(MoveAction):
'''Walk one step in the given direction.'''
def perform(self, engine: 'Engine') -> ActionResult:
new_position = self.entity.position + self.direction
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, entity: Entity, direction: Direction, target: Entity):
super().__init__(entity, direction)
self.target = target
def perform(self, engine: 'Engine') -> ActionResult:
LOG.info('Attack! %s', self.target)
return ActionResult(self, success=True)
class WaitAction(Action):
'''Wait a turn'''
def perform(self, engine: 'Engine') -> ActionResult:
LOG.info('%s is waiting a turn', self.entity)
return ActionResult(self, success=True)