Move quit event handling to the interface and flatten the Action class hierarchy. There are no longer any actions that don't take an Actor. This has the happy side effect of resolving some pylint errors too. :)
138 lines
5.4 KiB
Python
138 lines
5.4 KiB
Python
# Eryn Wells <eryn@erynwells.me>
|
|
|
|
import random
|
|
from typing import TYPE_CHECKING, List, Optional
|
|
|
|
import numpy as np
|
|
import tcod
|
|
|
|
from . import log
|
|
from .actions.action import Action
|
|
from .actions.game import BumpAction, WaitAction
|
|
from .components import Component
|
|
from .geometry import Direction, Point
|
|
from .object import Entity
|
|
|
|
if TYPE_CHECKING:
|
|
from .engine import Engine
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
|
|
class AI(Component):
|
|
'''An abstract class providing AI for an entity.'''
|
|
|
|
def __init__(self, entity: Entity) -> None:
|
|
super().__init__()
|
|
self.entity = entity
|
|
|
|
def act(self, engine: 'Engine') -> Optional[Action]:
|
|
'''Produce an action to perform'''
|
|
raise NotImplementedError()
|
|
|
|
|
|
class HostileEnemy(AI):
|
|
'''Entity AI for a hostile enemy.
|
|
|
|
The entity will wander around until it sees the hero, at which point it will
|
|
beeline for her.
|
|
'''
|
|
|
|
def act(self, engine: 'Engine') -> Optional[Action]:
|
|
visible_tiles = tcod.map.compute_fov(
|
|
engine.map.tiles['transparent'],
|
|
pov=tuple(self.entity.position),
|
|
radius=self.entity.sight_radius)
|
|
|
|
if engine.map.visible[tuple(self.entity.position)]:
|
|
log.AI.debug("AI for %s", self.entity)
|
|
|
|
hero_position = engine.hero.position
|
|
hero_is_visible = visible_tiles[hero_position.x, hero_position.y]
|
|
|
|
if hero_is_visible:
|
|
path_to_hero = self.get_path_to(hero_position, engine)
|
|
assert len(path_to_hero) > 0, f'{self.entity} attempting to find a path to hero while on top of the hero!'
|
|
|
|
entity_position = self.entity.position
|
|
|
|
if engine.map.visible[tuple(self.entity.position)]:
|
|
log.AI.debug('|-> Path to hero %s', path_to_hero)
|
|
|
|
next_position = path_to_hero.pop(0) if len(path_to_hero) > 1 else hero_position
|
|
direction_to_next_position = entity_position.direction_to_adjacent_point(next_position)
|
|
|
|
if engine.map.visible[tuple(self.entity.position)]:
|
|
log.AI.info('`-> Hero is visible to %s, bumping %s (%s)',
|
|
self.entity, direction_to_next_position, next_position)
|
|
|
|
return BumpAction(self.entity, direction_to_next_position)
|
|
else:
|
|
move_or_wait_chance = random.random()
|
|
if move_or_wait_chance <= 0.7:
|
|
# Pick a random adjacent tile to move to
|
|
directions = list(Direction.all())
|
|
while len(directions) > 0:
|
|
direction = random.choice(directions)
|
|
directions.remove(direction)
|
|
new_position = self.entity.position + direction
|
|
overlaps_existing_entity = any(new_position == ent.position for ent in engine.entities)
|
|
try:
|
|
point_is_walkable = engine.map.point_is_walkable(new_position)
|
|
except ValueError:
|
|
point_is_walkable = False
|
|
if not overlaps_existing_entity and point_is_walkable:
|
|
if engine.map.visible[tuple(self.entity.position)]:
|
|
log.AI.info('Hero is NOT visible to %s, bumping %s randomly', self.entity, direction)
|
|
action = BumpAction(self.entity, direction)
|
|
break
|
|
else:
|
|
# If this entity somehow can't move anywhere, just wait
|
|
if engine.map.visible[tuple(self.entity.position)]:
|
|
log.AI.info("Hero is NOT visible to %s and it can't move anywhere, waiting", self.entity)
|
|
action = WaitAction(self.entity)
|
|
|
|
return action
|
|
else:
|
|
return WaitAction(self.entity)
|
|
|
|
def get_path_to(self, point: Point, engine: 'Engine') -> List[Point]:
|
|
'''Compute a path to the given position.
|
|
|
|
Copied from the Roguelike tutorial. :)
|
|
|
|
Arguments
|
|
---------
|
|
point : Point
|
|
The target point
|
|
engine : Engine
|
|
The game engine
|
|
|
|
Returns
|
|
-------
|
|
List[Point]
|
|
An array of Points representing a path from the Entity's position to the target point
|
|
'''
|
|
# Copy the walkable array
|
|
cost = np.array(engine.map.tiles['walkable'], dtype=np.int8)
|
|
|
|
for ent in engine.entities:
|
|
# Check that an entity blocks movement and the cost isn't zero (blocking)
|
|
position = ent.position
|
|
if ent.blocks_movement and cost[position.x, position.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[position.x, position.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)
|
|
|
|
# Set the starting position
|
|
pathfinder.add_root(tuple(self.entity.position))
|
|
|
|
# Compute the path to the destination and remove the starting point.
|
|
path: List[List[int]] = pathfinder.path_to(tuple(point))[1:].tolist()
|
|
|
|
# Convert from List[List[int]] to List[Tuple[int, int]].
|
|
return [Point(index[0], index[1]) for index in path]
|