From be433a9449233bf0f84a6a4508055e462542fd84 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Mon, 10 Jan 2022 13:21:17 -0500 Subject: [PATCH] Add movement AI to npcs --- actions.py | 5 +++++ components/ai.py | 39 +++++++++++++++++++++++++++++++++++++-- engine.py | 5 +++-- entity.py | 36 +++++++++++++++++++++++++++++++++++- entity_factories.py | 19 ++++++++++++------- game_map.py | 19 ++++++++++++++++++- 6 files changed, 110 insertions(+), 13 deletions(-) diff --git a/actions.py b/actions.py index 08136d3..70dc44e 100644 --- a/actions.py +++ b/actions.py @@ -34,6 +34,11 @@ class EscapeAction(Action): raise SystemExit() +class WaitAction(Action): + def perform(self) -> None: + pass + + class ActionWithDirection(Action): def __init__(self, entity, dx: int, dy: int): super().__init__(entity) diff --git a/components/ai.py b/components/ai.py index 83aa356..f3e664c 100644 --- a/components/ai.py +++ b/components/ai.py @@ -1,15 +1,20 @@ from __future__ import annotations -from typing import List, Tuple +from typing import List, Tuple, TYPE_CHECKING import numpy as np # type: ignore import tcod -from actions import Action +from actions import Action, MeleeAction, MovementAction, WaitAction from components.base_component import BaseComponent +if TYPE_CHECKING: + from entity import Actor + class BaseAI(Action, BaseComponent): + entity: Actor + def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]: """Compute and return a path to the target position. @@ -38,3 +43,33 @@ class BaseAI(Action, BaseComponent): # Convert from List[List[int]] to List[Tuple[int, int]]. return [(index[0], index[1]) for index in path] + + +class HostileEnemy(BaseAI): + def __init__(self, entity: Actor): + super().__init__(entity) + self.path: List[Tuple[int, int]] = [] + + def perform(self) -> None: + target = self.engine.player + dx = target.x - self.entity.x + dy = target.y - self.entity.y + + # Chebyshev distance + distance = max(abs(dx), abs(dy)) + + if self.engine.game_map.visible[self.entity.x, self.entity.y]: + if distance <= 1: + return MeleeAction(self.entity, dx, dy).perform() + + self.path = self.get_path_to(target.x, target.y) + + if self.path: + dest_x, dest_y = self.path.pop(0) + return MovementAction( + self.entity, + dest_x - self.entity.x, + dest_y - self.entity.y + ).perform() + + return WaitAction(self.entity).perform() \ No newline at end of file diff --git a/engine.py b/engine.py index 056a720..fa86f00 100644 --- a/engine.py +++ b/engine.py @@ -24,8 +24,9 @@ class Engine: self.player = player def handle_enemy_turns(self) -> None: - for entity in self.game_map.entities - {self.player}: - print(f'The {entity.name} wonders when it will get to take a real turn.') + for entity in set(self.game_map.actors) - {self.player}: + if entity.ai: + entity.ai.perform() def update_fov(self) -> None: """Recompute the visible area based on the player's point of view.""" diff --git a/entity.py b/entity.py index 615c66f..70eb73e 100644 --- a/entity.py +++ b/entity.py @@ -1,9 +1,11 @@ from __future__ import annotations import copy -from typing import Optional, Tuple, TypeVar, TYPE_CHECKING +from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING if TYPE_CHECKING: + from components.ai import BaseAI + from components.fighter import Fighter from game_map import GameMap T = TypeVar("T", bound="Entity") @@ -61,3 +63,35 @@ class Entity: # Move the entity by a given amount self.x += dx self.y += dy + + +class Actor(Entity): + def __init__( + self, + *, + x: int = 0, + y: int = 0, + char: str = "?", + color: Tuple[int, int, int] = (255, 255, 255), + name: str = "", + ai_cls: Type[BaseAI], + fighter: Fighter + ): + super().__init__( + x=x, + y=y, + char=char, + color=color, + name=name, + blocks_movement=True + ) + + self.ai = Optional[BaseAI] = ai_cls(self) + + self.fighter = fighter + self.fighter.entity = self + + @property + def is_alive(self) -> bool: + """Returns True as long as this actor can perform actions.""" + return bool(self.ai) diff --git a/entity_factories.py b/entity_factories.py index c505af3..bab7644 100644 --- a/entity_factories.py +++ b/entity_factories.py @@ -1,21 +1,26 @@ -from entity import Entity +from components.ai import HostileEnemy +from components.fighter import Fighter +from entity import Actor -player = Entity( +player = Actor( char="@", color=(255, 255, 255), name="PLayer", - blocks_movement=True + ai_cls=HostileEnemy, + fighter=Fighter(hp=30, defense=2, power=5), ) -orc = Entity( +orc = Actor( char="o", color=(63, 127, 63), name="Orc", - blocks_movement=True + ai_cls=HostileEnemy, + fighter=Fighter(hp=10, defense=0, power=3), ) -troll = Entity( +troll = Actor( char="T", color=(0, 127, 0), name="Troll", - blocks_movement=True + ai_cls=HostileEnemy, + fighter=Fighter(hp=16, defense=1, power=4) ) diff --git a/game_map.py b/game_map.py index 53df5d7..f9fbaca 100644 --- a/game_map.py +++ b/game_map.py @@ -1,10 +1,11 @@ from __future__ import annotations -from typing import Iterable, Optional, TYPE_CHECKING +from typing import Iterable, Iterator, Optional, TYPE_CHECKING import numpy as np # type: ignore from tcod.console import Console +from entity import Actor import tile_types if TYPE_CHECKING: @@ -36,6 +37,15 @@ class GameMap: order="F" ) # Tiles the player has seen before + @property + def actors(self) -> Iterator[Actor]: + """Iterate over this map's living actors.""" + yield from ( + entity + for entity in self.entities + if isinstance(entity, Actor) and entity.is_alive + ) + def get_blocking_entity_at_location( self, location_x: int, @@ -51,6 +61,13 @@ class GameMap: return None + def get_actor_at_location(self, x: int, y: int) -> Optional[Actor]: + for actor in self.actors: + if actor.x == x and actor.y == y: + return actor + + return None + def in_bounds(self, x: int, y: int) -> bool: """Return True if x and y are inside the bounds of the map.""" return 0 <= x < self.width and 0 <= y < self.height