Attack!!!
Refactor MovePlayerAction into a few different Action subclasses. Move direction to a parent MoveAction, and create three new subclasses of MoveAction: - BumpAction: perform the test that an action can be performed in the given direction - WalkAction, take a step in the given direction - MeleeAction, attack another Entity in the given direction Add an ActionResult class that communicates the result of performing an Action. - ActionResult.succeeded indicates whether the action succeeded. - ActionResult.done indicates if the action is fully complete or requires followup. - ActionResult.alternate specifies the follow-up action to perform. Convert all the key handling actions to BumpActions. In the Engine's event handler method, loop until an action is completed, performing specified alternate actions until the result object indicates the action is done.
This commit is contained in:
parent
57bbb2c3fc
commit
4002b64640
4 changed files with 153 additions and 36 deletions
|
@ -1,36 +1,75 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# Eryn Wells <eryn@erynwells.me>
|
# 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
|
||||||
|
WalkAction
|
||||||
|
MeleeAction
|
||||||
|
ExitAction
|
||||||
|
'''
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from .geometry import Direction
|
from .geometry import Direction
|
||||||
|
from .object import Entity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .engine import Engine
|
from .engine import Engine
|
||||||
from .object import Entity
|
|
||||||
|
|
||||||
LOG = logging.getLogger('events')
|
LOG = logging.getLogger('events')
|
||||||
|
|
||||||
class ActionResult:
|
class ActionResult:
|
||||||
'''An object that represents the result of an Action.
|
'''The result of an Action.
|
||||||
|
|
||||||
|
`Action.perform()` returns an instance of this class to inform the caller of the result
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
----------
|
----------
|
||||||
success : bool
|
action : Action
|
||||||
|
The Action that was performed
|
||||||
|
success : bool, optional
|
||||||
True if the action succeeded
|
True if the action succeeded
|
||||||
done : bool
|
done : bool, optional
|
||||||
True if the action is complete, and no follow-up action is needed.
|
True if the action is complete, and no follow-up action is needed
|
||||||
alternate : Action, optional
|
alternate : Action, optional
|
||||||
An alternate action to perform if this action failed
|
An alternate action to perform if this action failed
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, success: bool, done: bool = True, alternate: Optional['Action'] = None):
|
def __init__(self, action: 'Action', *,
|
||||||
self.success = success
|
success: Optional[bool] = None,
|
||||||
self.done = done
|
done: Optional[bool] = None,
|
||||||
|
alternate: Optional['Action'] = None):
|
||||||
|
self.action = action
|
||||||
self.alternate = alternate
|
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}, success={self.success}, done={self.done}, alternate={self.alternate})'
|
||||||
|
|
||||||
class Action:
|
class Action:
|
||||||
def perform(self, engine: 'Engine', entity: 'Entity') -> ActionResult:
|
'''An action that an Entity should perform.'''
|
||||||
|
|
||||||
|
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult:
|
||||||
'''Perform this action.
|
'''Perform this action.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
@ -52,28 +91,86 @@ class Action:
|
||||||
return f'{self.__class__.__name__}()'
|
return f'{self.__class__.__name__}()'
|
||||||
|
|
||||||
class ExitAction(Action):
|
class ExitAction(Action):
|
||||||
def perform(self, engine: 'Engine', entity: 'Entity') -> ActionResult:
|
'''Exit the game.'''
|
||||||
|
|
||||||
|
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult:
|
||||||
raise SystemExit()
|
raise SystemExit()
|
||||||
|
|
||||||
class RegenerateRoomsAction(Action):
|
class RegenerateRoomsAction(Action):
|
||||||
def perform(self, engine: 'Engine', entity: 'Entity') -> ActionResult:
|
'''Regenerate the dungeon map'''
|
||||||
return ActionResult(True)
|
|
||||||
|
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult:
|
||||||
|
return ActionResult(self, success=False)
|
||||||
|
|
||||||
|
# pylint: disable=abstract-method
|
||||||
|
class MoveAction(Action):
|
||||||
|
'''An abstract Action that requires a direction to complete.'''
|
||||||
|
|
||||||
class MovePlayerAction(Action):
|
|
||||||
def __init__(self, direction: Direction):
|
def __init__(self, direction: Direction):
|
||||||
|
super().__init__()
|
||||||
self.direction = direction
|
self.direction = direction
|
||||||
|
|
||||||
def perform(self, engine: 'Engine', entity: 'Entity') -> None:
|
def __repr__(self):
|
||||||
new_player_position = entity.position + self.direction
|
return f'{self.__class__.__name__}({self.direction})'
|
||||||
|
|
||||||
position_is_in_bounds = engine.map.tile_is_in_bounds(new_player_position)
|
class BumpAction(MoveAction):
|
||||||
position_is_walkable = engine.map.tile_is_walkable(new_player_position)
|
'''Attempt to perform a movement action in a direction.
|
||||||
overlaps_another_entity = any(new_player_position == ent.position for ent in engine.entities if ent is not entity)
|
|
||||||
|
|
||||||
if position_is_in_bounds and position_is_walkable and not overlaps_another_entity:
|
This action tests if an action in the direction is possible and returns the action that can be completed.
|
||||||
LOG.info('Moving hero to %s (in_bounds:%s walkable:%s overlaps:%s)',
|
|
||||||
new_player_position,
|
Attributes
|
||||||
|
----------
|
||||||
|
direction : Direction
|
||||||
|
The direction to test
|
||||||
|
'''
|
||||||
|
|
||||||
|
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult:
|
||||||
|
new_position = 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 (in_bounds:%s walkable:%s overlaps:%s)',
|
||||||
|
new_position,
|
||||||
position_is_in_bounds,
|
position_is_in_bounds,
|
||||||
position_is_walkable,
|
position_is_walkable,
|
||||||
overlaps_another_entity)
|
entity_occupying_position)
|
||||||
entity.position = new_player_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.direction, entity_occupying_position))
|
||||||
|
|
||||||
|
return ActionResult(self, alternate=WalkAction(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
|
||||||
|
|
||||||
|
LOG.info('Moving %s to %s', entity, new_position)
|
||||||
|
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)
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def perform(self, engine: 'Engine', entity: Entity) -> ActionResult:
|
||||||
|
LOG.info('Attack! %s', self.target)
|
||||||
|
return ActionResult(self, success=True)
|
||||||
|
|
|
@ -66,7 +66,26 @@ class Engine:
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
|
|
||||||
action.perform(self, self.hero)
|
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())
|
directions = list(Direction.all())
|
||||||
moved_entities: MutableSet[Entity] = {self.hero}
|
moved_entities: MutableSet[Entity] = {self.hero}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# Eryn Wells <eryn@erynwells.me>
|
# Eryn Wells <eryn@erynwells.me>
|
||||||
|
|
||||||
import tcod
|
import tcod
|
||||||
from .actions import Action, ExitAction, MovePlayerAction, RegenerateRoomsAction
|
from .actions import Action, ExitAction, RegenerateRoomsAction, BumpAction
|
||||||
from .geometry import Direction
|
from .geometry import Direction
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -16,21 +16,21 @@ class EventHandler(tcod.event.EventDispatch[Action]):
|
||||||
sym = event.sym
|
sym = event.sym
|
||||||
|
|
||||||
if sym == tcod.event.KeySym.b:
|
if sym == tcod.event.KeySym.b:
|
||||||
action = MovePlayerAction(Direction.SouthWest)
|
action = BumpAction(Direction.SouthWest)
|
||||||
elif sym == tcod.event.KeySym.h:
|
elif sym == tcod.event.KeySym.h:
|
||||||
action = MovePlayerAction(Direction.West)
|
action = BumpAction(Direction.West)
|
||||||
elif sym == tcod.event.KeySym.j:
|
elif sym == tcod.event.KeySym.j:
|
||||||
action = MovePlayerAction(Direction.South)
|
action = BumpAction(Direction.South)
|
||||||
elif sym == tcod.event.KeySym.k:
|
elif sym == tcod.event.KeySym.k:
|
||||||
action = MovePlayerAction(Direction.North)
|
action = BumpAction(Direction.North)
|
||||||
elif sym == tcod.event.KeySym.l:
|
elif sym == tcod.event.KeySym.l:
|
||||||
action = MovePlayerAction(Direction.East)
|
action = BumpAction(Direction.East)
|
||||||
elif sym == tcod.event.KeySym.n:
|
elif sym == tcod.event.KeySym.n:
|
||||||
action = MovePlayerAction(Direction.SouthEast)
|
action = BumpAction(Direction.SouthEast)
|
||||||
elif sym == tcod.event.KeySym.u:
|
elif sym == tcod.event.KeySym.u:
|
||||||
action = MovePlayerAction(Direction.NorthEast)
|
action = BumpAction(Direction.NorthEast)
|
||||||
elif sym == tcod.event.KeySym.y:
|
elif sym == tcod.event.KeySym.y:
|
||||||
action = MovePlayerAction(Direction.NorthWest)
|
action = BumpAction(Direction.NorthWest)
|
||||||
elif sym == tcod.event.KeySym.SPACE:
|
elif sym == tcod.event.KeySym.SPACE:
|
||||||
action = RegenerateRoomsAction()
|
action = RegenerateRoomsAction()
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class Vector:
|
||||||
yield self.dy
|
yield self.dy
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'(δx:{self.x}, δy:{self.y})'
|
return f'(δx:{self.dx}, δy:{self.dy})'
|
||||||
|
|
||||||
class Direction:
|
class Direction:
|
||||||
North = Vector(0, -1)
|
North = Vector(0, -1)
|
||||||
|
@ -49,6 +49,7 @@ class Direction:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls) -> Iterator['Direction']:
|
def all(cls) -> Iterator['Direction']:
|
||||||
|
'''Iterate through all directions, starting with North and proceeding clockwise'''
|
||||||
yield Direction.North
|
yield Direction.North
|
||||||
yield Direction.NorthEast
|
yield Direction.NorthEast
|
||||||
yield Direction.East
|
yield Direction.East
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue