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:
Eryn Wells 2023-02-12 16:34:37 -08:00
parent 8efd3ce207
commit 7e00f58a40
5 changed files with 72 additions and 39 deletions

View file

@ -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}'

View file

@ -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):