Refactor Action into Action and ActionWithActor
The base class Actor doesn't declare a (optional) actor attribute. The ActionWithActor has a non-optional actor attribute. This makes the type checker happier, and means we can have some actions that don't have actors.
This commit is contained in:
parent
8efd3ce207
commit
7e00f58a40
5 changed files with 72 additions and 39 deletions
|
@ -1,6 +1,6 @@
|
|||
# Eryn Wells <eryn@erynwells.me>
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..object import Actor
|
||||
from .result import ActionResult
|
||||
|
@ -8,12 +8,11 @@ from .result import ActionResult
|
|||
if TYPE_CHECKING:
|
||||
from ..engine import Engine
|
||||
|
||||
|
||||
class Action:
|
||||
'''An action that an Entity should perform.'''
|
||||
|
||||
def __init__(self, actor: Optional[Actor] = None):
|
||||
self.actor = actor
|
||||
'''An action with no specific actor'''
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
'''Perform this action.
|
||||
|
||||
|
@ -28,7 +27,7 @@ class Action:
|
|||
A result object reflecting how the action was handled, and what follow-up actions, if any, are needed to
|
||||
complete the action.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
return self.success()
|
||||
|
||||
def failure(self) -> ActionResult:
|
||||
'''Create an ActionResult indicating failure with no follow-up'''
|
||||
|
@ -38,6 +37,20 @@ class Action:
|
|||
'''Create an ActionResult indicating success with no follow-up'''
|
||||
return ActionResult(self, success=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def __repr__(self):
|
||||
return f'{self.__class__.__name__}()'
|
||||
|
||||
|
||||
class ActionWithActor(Action):
|
||||
'''An action that assigned to an actor'''
|
||||
|
||||
def __init__(self, actor: Actor):
|
||||
super().__init__()
|
||||
self.actor = actor
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f'{self.__class__.__name__} for {self.actor!s}'
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
#!/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
|
||||
|
@ -18,13 +15,14 @@ Action : Base class of all actions
|
|||
WaitAction
|
||||
'''
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .. import items
|
||||
from .. import log
|
||||
from ..geometry import Vector
|
||||
from ..object import Actor, Item
|
||||
from .action import Action
|
||||
from .action import Action, ActionWithActor
|
||||
from .result import ActionResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -34,9 +32,6 @@ if TYPE_CHECKING:
|
|||
class ExitAction(Action):
|
||||
'''Exit the game.'''
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(None)
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
raise SystemExit()
|
||||
|
||||
|
@ -45,12 +40,10 @@ class RegenerateRoomsAction(Action):
|
|||
'''Regenerate the dungeon map'''
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
return ActionResult(self, success=False)
|
||||
|
||||
# pylint: disable=abstract-method
|
||||
return self.failure()
|
||||
|
||||
|
||||
class MoveAction(Action):
|
||||
class MoveAction(ActionWithActor):
|
||||
'''An abstract Action that requires a direction to complete.'''
|
||||
|
||||
def __init__(self, actor: Actor, direction: Vector):
|
||||
|
@ -100,6 +93,8 @@ class BumpAction(MoveAction):
|
|||
if not position_is_in_bounds or not position_is_walkable:
|
||||
return self.failure()
|
||||
|
||||
# TODO: I'm passing entity_occupying_position into the ActionResult below, but the type checker doesn't
|
||||
# understand that the entity is an Actor. I think I need some additional checks here.
|
||||
if entity_occupying_position:
|
||||
assert entity_occupying_position.blocks_movement
|
||||
return ActionResult(self, alternate=MeleeAction(self.actor, self.direction, entity_occupying_position))
|
||||
|
@ -111,15 +106,19 @@ class WalkAction(MoveAction):
|
|||
'''Walk one step in the given direction.'''
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
new_position = self.actor.position + self.direction
|
||||
actor = self.actor
|
||||
|
||||
assert actor.fighter
|
||||
|
||||
new_position = actor.position + self.direction
|
||||
|
||||
log.ACTIONS.debug('Moving %s to %s', self.actor, new_position)
|
||||
self.actor.position = new_position
|
||||
actor.position = new_position
|
||||
|
||||
try:
|
||||
should_recover_hit_points = self.actor.fighter.passively_recover_hit_points(5)
|
||||
should_recover_hit_points = actor.fighter.passively_recover_hit_points(5)
|
||||
if should_recover_hit_points:
|
||||
return ActionResult(self, alternate=HealAction(self.actor, 1))
|
||||
return ActionResult(self, alternate=HealAction(actor, random.randint(1, 3)))
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
@ -134,8 +133,13 @@ class MeleeAction(MoveAction):
|
|||
self.target = target
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
assert self.actor.fighter and self.target.fighter
|
||||
|
||||
fighter = self.actor.fighter
|
||||
target_fighter = self.target.fighter
|
||||
|
||||
try:
|
||||
damage = self.actor.fighter.attack_power - self.target.fighter.defense
|
||||
damage = fighter.attack_power - target_fighter.defense
|
||||
if damage > 0 and self.target:
|
||||
log.ACTIONS.debug('%s attacks %s for %d damage!', self.actor, self.target, damage)
|
||||
self.target.fighter.hit_points -= damage
|
||||
|
@ -160,21 +164,24 @@ class MeleeAction(MoveAction):
|
|||
return self.success()
|
||||
|
||||
|
||||
class WaitAction(Action):
|
||||
class WaitAction(ActionWithActor):
|
||||
'''Wait a turn'''
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
log.ACTIONS.debug('%s is waiting a turn', self.actor)
|
||||
|
||||
if self.actor == engine.hero:
|
||||
should_recover_hit_points = self.actor.fighter.passively_recover_hit_points(10)
|
||||
assert self.actor.fighter
|
||||
|
||||
fighter = self.actor.fighter
|
||||
should_recover_hit_points = fighter.passively_recover_hit_points(20)
|
||||
if should_recover_hit_points:
|
||||
return ActionResult(self, alternate=HealAction(self.actor, 1))
|
||||
return ActionResult(self, alternate=HealAction(self.actor, random.randint(1, 3)))
|
||||
|
||||
return self.success()
|
||||
|
||||
|
||||
class DieAction(Action):
|
||||
class DieAction(ActionWithActor):
|
||||
'''Kill an Actor'''
|
||||
|
||||
def perform(self, engine: 'Engine') -> ActionResult:
|
||||
|
@ -193,7 +200,7 @@ class DieAction(Action):
|
|||
return self.success()
|
||||
|
||||
|
||||
class DropItemAction(Action):
|
||||
class DropItemAction(ActionWithActor):
|
||||
'''Drop an item'''
|
||||
|
||||
def __init__(self, actor: 'Actor', item: 'Item'):
|
||||
|
@ -205,7 +212,7 @@ class DropItemAction(Action):
|
|||
return self.success()
|
||||
|
||||
|
||||
class HealAction(Action):
|
||||
class HealAction(ActionWithActor):
|
||||
'''Heal a target actor some number of hit points'''
|
||||
|
||||
def __init__(self, actor: 'Actor', hit_points_to_recover: int):
|
||||
|
|
|
@ -7,7 +7,7 @@ import numpy as np
|
|||
import tcod
|
||||
|
||||
from . import log
|
||||
from .actions.action import Action
|
||||
from .actions.action import ActionWithActor
|
||||
from .actions.game import BumpAction, WaitAction
|
||||
from .components import Component
|
||||
from .geometry import Direction, Point
|
||||
|
@ -26,7 +26,7 @@ class AI(Component):
|
|||
super().__init__()
|
||||
self.entity = entity
|
||||
|
||||
def act(self, engine: 'Engine') -> Optional[Action]:
|
||||
def act(self, engine: 'Engine') -> Optional[ActionWithActor]:
|
||||
'''Produce an action to perform'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -38,7 +38,7 @@ class HostileEnemy(AI):
|
|||
beeline for her.
|
||||
'''
|
||||
|
||||
def act(self, engine: 'Engine') -> Optional[Action]:
|
||||
def act(self, engine: 'Engine') -> Optional[ActionWithActor]:
|
||||
visible_tiles = tcod.map.compute_fov(
|
||||
engine.map.tiles['transparent'],
|
||||
pov=tuple(self.entity.position),
|
||||
|
|
|
@ -9,7 +9,7 @@ import tcod
|
|||
|
||||
from . import log
|
||||
from . import monsters
|
||||
from .actions.action import Action
|
||||
from .actions.action import Action, ActionWithActor
|
||||
from .actions.result import ActionResult
|
||||
from .ai import HostileEnemy
|
||||
from .configuration import Configuration
|
||||
|
@ -125,6 +125,10 @@ class Engine:
|
|||
def process_input_action(self, action: Action):
|
||||
'''Process an Action from player input'''
|
||||
|
||||
if not isinstance(action, ActionWithActor):
|
||||
log.ACTIONS_TREE.error('Attempted to process input action with no actor')
|
||||
return
|
||||
|
||||
log.ACTIONS_TREE.info('Processing Hero Actions')
|
||||
log.ACTIONS_TREE.info('|-> %s', action.actor)
|
||||
|
||||
|
@ -165,10 +169,11 @@ class Engine:
|
|||
if self.map.visible[tuple(ent.position)]:
|
||||
log.ACTIONS_TREE.info('%s-> %s', '|' if i < len(entities) - 1 else '`', ent)
|
||||
|
||||
action = ent_ai.act(self)
|
||||
action = ent_ai.act(engine=self)
|
||||
if action:
|
||||
self._perform_action_until_done(action)
|
||||
|
||||
def _perform_action_until_done(self, action: Action) -> ActionResult:
|
||||
def _perform_action_until_done(self, action: ActionWithActor) -> ActionResult:
|
||||
'''Perform the given action and any alternate follow-up actions until the action chain is done.'''
|
||||
result = action.perform(self)
|
||||
|
||||
|
|
|
@ -102,11 +102,17 @@ class Actor(Entity):
|
|||
position: Optional[Point] = None,
|
||||
blocks_movement: Optional[bool] = True,
|
||||
render_order: RenderOrder = RenderOrder.ACTOR,
|
||||
ai: Optional[Type['AI']] = None,
|
||||
ai: Optional['AI'] = None,
|
||||
fighter: Optional[Fighter] = None,
|
||||
fg: Optional[Tuple[int, int, int]] = None,
|
||||
bg: Optional[Tuple[int, int, int]] = None):
|
||||
super().__init__(symbol, position=position, blocks_movement=blocks_movement, fg=fg, bg=bg, render_order=render_order)
|
||||
super().__init__(
|
||||
symbol,
|
||||
position=position,
|
||||
blocks_movement=blocks_movement,
|
||||
fg=fg,
|
||||
bg=bg,
|
||||
render_order=render_order)
|
||||
|
||||
# Components
|
||||
self.ai = ai
|
||||
|
@ -152,13 +158,14 @@ class Hero(Actor):
|
|||
return 8
|
||||
|
||||
def __str__(self) -> str:
|
||||
assert self.fighter
|
||||
return f'Hero!{self.identifier} at {self.position} with {self.fighter.hit_points}/{self.fighter.maximum_hit_points} hp'
|
||||
|
||||
|
||||
class Monster(Actor):
|
||||
'''An instance of a Species'''
|
||||
|
||||
def __init__(self, species: Species, ai_class: Type['AI'], position: Point = None):
|
||||
def __init__(self, species: Species, ai_class: Type['AI'], position: Optional[Point] = None):
|
||||
fighter = Fighter(
|
||||
maximum_hit_points=species.maximum_hit_points,
|
||||
attack_power=species.attack_power,
|
||||
|
@ -187,13 +194,14 @@ class Monster(Actor):
|
|||
return True
|
||||
|
||||
def __str__(self) -> str:
|
||||
assert self.fighter
|
||||
return f'{self.name}!{self.identifier} with {self.fighter.hit_points}/{self.fighter.maximum_hit_points} hp at {self.position}'
|
||||
|
||||
|
||||
class Item(Entity):
|
||||
'''An instance of an Item'''
|
||||
|
||||
def __init__(self, kind: items.Item, position: Point = None, name: str = None):
|
||||
def __init__(self, kind: items.Item, position: Optional[Point] = None, name: Optional[str] = None):
|
||||
super().__init__(kind.symbol,
|
||||
position=position,
|
||||
blocks_movement=False,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue