While the scrolls themselves won't be around forever, the tools used to make them work will be.
125 lines
2.9 KiB
Python
125 lines
2.9 KiB
Python
from __future__ import annotations
|
|
from typing import List, Tuple, TYPE_CHECKING
|
|
import random
|
|
|
|
import numpy as np
|
|
import tcod
|
|
|
|
from actions import (
|
|
BaseAction,
|
|
MeleeAction,
|
|
MovementAction,
|
|
BumpAction,
|
|
WaitAction,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from entity import Entity
|
|
|
|
class BaseAI:
|
|
"""Base type for creature AI, with various utilities."""
|
|
entity: Entity
|
|
|
|
def __init__(self, entity: Entity):
|
|
#grab the entity
|
|
self.entity = entity
|
|
|
|
#utilities
|
|
self.path: List[Tuple[int, int]] = []
|
|
|
|
def process(self) -> BaseAction:
|
|
"""Allow the AI to think."""
|
|
raise NotImplementedError()
|
|
|
|
#utils
|
|
def generate_path_to(self, dest_x: int, dest_y: int) -> List[Tuple[int, int]]:
|
|
#copy the walkables
|
|
cost = np.array(self.entity.floor_map.tiles["walkable"], dtype=np.int8)
|
|
|
|
#higher numbers deter path-finding this way
|
|
for entity in self.entity.floor_map.entities:
|
|
if not entity.walkable and cost[entity.x, entity.y]:
|
|
cost[entity.x, entity.y] += 100
|
|
|
|
graph = tcod.path.SimpleGraph(cost=cost, cardinal=10, diagonal=14)
|
|
pathfinder = tcod.path.Pathfinder(graph)
|
|
|
|
pathfinder.add_root((self.entity.x, self.entity.y)) #start pos
|
|
|
|
#make the path, omitting the start pos
|
|
path: List[List[int]] = pathfinder.path_to((dest_x, dest_y))[1:].tolist()
|
|
|
|
#return the path, after mapping it to a list of tuples
|
|
return [(index[0], index[1]) for index in path]
|
|
|
|
|
|
class AggressiveWhenSeen(BaseAI):
|
|
"""
|
|
If I'm close enough to the player, attack.
|
|
If I see where the player is, approach that point.
|
|
If I know where the player has been, approach that point.
|
|
If all else fails, idle.
|
|
"""
|
|
def process(self) -> BaseAction:
|
|
target = self.entity.floor_map.player #TODO: friendly fire?
|
|
xdir = target.x - self.entity.x
|
|
ydir = target.y - self.entity.y
|
|
distance = max(abs(xdir), abs(ydir))
|
|
|
|
#if the player can see me, create or update my path
|
|
if self.entity.floor_map.visible[self.entity.x, self.entity.y]:
|
|
#if I'm close enough to attack
|
|
if distance <= 1:
|
|
return MeleeAction(self.entity, xdir, ydir)
|
|
|
|
self.path = self.generate_path_to(target.x, target.y)
|
|
|
|
#if I have a path to follow, regardless of current visability
|
|
if self.path:
|
|
dest_x, dest_y = self.path.pop(0)
|
|
return MovementAction(
|
|
entity = self.entity,
|
|
xdir = dest_x - self.entity.x,
|
|
ydir = dest_y - self.entity.y,
|
|
)
|
|
|
|
#idle
|
|
return WaitAction(self.entity)
|
|
|
|
|
|
class ConfusedAI(BaseAI):
|
|
"""
|
|
"""
|
|
parent_ai: BaseAI
|
|
duration: int
|
|
|
|
def __init__(self, entity: Entity, duration: int):
|
|
super().__init__(entity)
|
|
self.parent_ai = entity.ai
|
|
self.duration = duration
|
|
|
|
def process(self) -> BaseAction:
|
|
if self.duration <= 0:
|
|
self.entity.ai = self.parent_ai
|
|
return self.entity.ai.process()
|
|
|
|
self.duration -= 1
|
|
|
|
xdir, ydir = random.choice(
|
|
[
|
|
[ -1, -1],
|
|
[ -1, 0],
|
|
[ -1, 1],
|
|
|
|
[ 0, -1],
|
|
#[ 0, 0],
|
|
[ 0, 1],
|
|
|
|
[ 1, -1],
|
|
[ 1, 0],
|
|
[ 1, 1],
|
|
]
|
|
)
|
|
|
|
#Oh, this allows for friendly fire!
|
|
return BumpAction(self.entity, xdir, ydir) |