from __future__ import annotations import random from typing import List, Optional, Tuple, TYPE_CHECKING import numpy as np # type: ignore import tcod from actions import Action, BumpAction, MeleeAction, MovementAction, WaitAction if TYPE_CHECKING: from entity import Actor class BaseAI(Action): def get_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]: """Compute and return a path to the target position. IF there is no valid path then returns an empty list. """ # Copy the walkable array. cost = np.array(self.entity.gamemap.tiles["walkable"], dtype=np.int8) for entity in self.entity.gamemap.entities: # Check that an entity blocks movement and the cost isn't zero (blocking.) if entity.blocks_movement and cost[entity.x, entity.y]: # Add to the cost of a blocked position. # A lower number means more enemies will crowd behind each other in # hallways. A higher number means enemies will take longer paths in # order to surround the player. cost[entity.x, entity.y] += 10 # Create a graph from the cost array and pass that graph to a new pathfinder. graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3) pathfinder = tcod.path.Pathfinder(graph) pathfinder.add_root((self.entity.x, self.entity.y)) # Start position # Compute the path to the destination and remove the starting point. path: List[List[int]] = pathfinder.path_to((dest_x, dest_y))[1:].tolist() # Convert from List[List[int]] to List[Tuple[int, int]]. return [(index[0], index[1]) for index in path] class ConfusedEnemy(BaseAI): """ A confused enemy will stumble around aimlessly for a given number of turns, then reverts back to its previous AI. If an actor occupies a tile it is randomly moving into, it will attack. """ def __init__( self, entity: Actor, previous_ai: Optional[BaseAI], turns_remaining: int ): super().__init__(entity) self.previous_ai = previous_ai self.turns_remaining = turns_remaining def perform(self) -> None: # Rever the AI back to the original state if the effect has run its course. if self.turns_remaining <= 0: self.engine.message_log.add_message( f"The {self.entity.name} is no longer confused." ) self.entity.ai = self.previous_ai else: # Pick a random direction direction_x, direction_y = random.choice( [ (-1, -1), # Northwest (0, -1), # North (1, -1), # Northeast (-1, 0), # West (1, 0), # East (-1, 1), # Southwest (0, 1), # South (1, 1), # Southeast ] ) self.turns_remaining -= 1 # The actor will either try to move or attack in the chosen random direction. # It's possible the actor will just bump into the wall, wasting a turn. return BumpAction(self.entity, direction_x, direction_y).perform() 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()