From b604ff30ecbed86d0d23409f7669a6f7f13a5af6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 May 2022 23:38:48 -0700 Subject: [PATCH] Implement a basic AI for HostileMonster This AI will walk randomly around the dungeon (pausing periodically) and if the Hero comes into view, will b-line and attack --- roguebasin/ai.py | 100 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 95 insertions(+), 5 deletions(-) diff --git a/roguebasin/ai.py b/roguebasin/ai.py index f758fb2..eb54f5c 100644 --- a/roguebasin/ai.py +++ b/roguebasin/ai.py @@ -1,22 +1,112 @@ # Eryn Wells -from typing import TYPE_CHECKING +import logging +from os import path +import random +from typing import TYPE_CHECKING, List, Optional -from .actions import Action, WaitAction +import numpy as np +import tcod + +from .actions import Action, BumpAction, MeleeAction, WaitAction from .components import Component +from .geometry import Direction, Point from .object import Entity if TYPE_CHECKING: from .engine import Engine +LOG = logging.getLogger(__name__) + class AI(Component): def __init__(self, entity: Entity) -> None: super().__init__() self.entity = entity - def act(self, engine: 'Engine') -> Action: + def act(self, engine: 'Engine') -> Optional[Action]: + '''Produce an action to perform''' raise NotImplementedError() class HostileEnemy(AI): - def act(self, engine: 'Engine') -> Action: - return WaitAction(self.entity) \ No newline at end of file + 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) + + 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) + entity_position = self.entity.position + + next_position = path_to_hero.pop(0) if len(path_to_hero) else hero_position + direction_to_next_position = entity_position.direction_to_adjacent_point(next_position) + LOG.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) + tile_is_walkable = engine.map.tile_is_walkable(new_position) + if not overlaps_existing_entity and tile_is_walkable: + LOG.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 + LOG.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] \ No newline at end of file