diff --git a/erynrl/actions/action.py b/erynrl/actions/action.py index 3dd865a..584d102 100644 --- a/erynrl/actions/action.py +++ b/erynrl/actions/action.py @@ -1,6 +1,6 @@ # Eryn Wells -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}' diff --git a/erynrl/actions/game.py b/erynrl/actions/game.py index 03f7438..0e70fbd 100644 --- a/erynrl/actions/game.py +++ b/erynrl/actions/game.py @@ -1,6 +1,3 @@ -#!/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 @@ -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): diff --git a/erynrl/ai.py b/erynrl/ai.py index 05c761e..ade0b33 100644 --- a/erynrl/ai.py +++ b/erynrl/ai.py @@ -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), diff --git a/erynrl/engine.py b/erynrl/engine.py index c8a44ae..86ad536 100644 --- a/erynrl/engine.py +++ b/erynrl/engine.py @@ -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) - self._perform_action_until_done(action) + 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) diff --git a/erynrl/object.py b/erynrl/object.py index 4d3c813..7b2c1b3 100644 --- a/erynrl/object.py +++ b/erynrl/object.py @@ -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,