from pathlib import Path import pytest from engine.ai import AIController from engine.character import Character from engine.defs import DefRegistry from engine.entity import EntityPosition from engine.map import Map from engine.tile import Tile, TileLayerInstance from engine.world import World from resources.logic.ai import run_ai_tick DEFS_DIR = Path(__file__).resolve().parent.parent / "resources" / "defs" @pytest.fixture() def registry(): return DefRegistry.load(DEFS_DIR) def make_world(registry): field = Map("field", 5, 5, 1) for x in range(5): for y in range(5): field.set_tile(x, y, 0, Tile(flooring=TileLayerInstance("grass"))) world = World(registry=registry) world.add_map(field) return world def make_human(registry, **kwargs) -> Character: return Character(species_id="human", default_body_parts=registry.new_default_body_parts("human"), **kwargs) class FixedRng: """A random.Random look-alike that always picks the same direction/interval.""" def __init__(self, choice_index: int = 0, uniform_value: float = 0.5): self.choice_index = choice_index self.uniform_value = uniform_value def randint(self, a, b): return a def choice(self, seq): return seq[self.choice_index] def uniform(self, a, b): return a + (b - a) * self.uniform_value def test_aicontroller_defaults_to_empty_scratch_state(): controller = AIController(behavior_id="wander") assert controller.state == {} def test_two_aicontrollers_have_independent_state_dicts(): a = AIController(behavior_id="wander") b = AIController(behavior_id="wander") a.state["next_move_at"] = 5.0 assert b.state == {} def test_run_ai_tick_is_a_noop_without_an_ai_slot(registry): world = make_world(registry) human = make_human(registry, position=EntityPosition("field", 2, 2, 0)) world.add_entity(human) run_ai_tick(human, world, registry, now=0.0, rng=FixedRng()) assert human.ai is None assert (human.position.x, human.position.y) == (2, 2) def test_run_ai_tick_is_a_noop_for_an_unregistered_behavior_id(registry): world = make_world(registry) human = make_human( registry, position=EntityPosition("field", 2, 2, 0), ai=AIController(behavior_id="does_not_exist") ) world.add_entity(human) run_ai_tick(human, world, registry, now=0.0, rng=FixedRng()) assert (human.position.x, human.position.y) == (2, 2) def test_wander_moves_the_character_on_its_first_tick(registry): world = make_world(registry) human = make_human(registry, position=EntityPosition("field", 2, 2, 0), ai=AIController(behavior_id="wander")) world.add_entity(human) run_ai_tick(human, world, registry, now=0.0, rng=FixedRng(choice_index=1)) # (1, 0) assert (human.position.x, human.position.y) == (3, 2) assert human.ai.state["next_move_at"] > 0.0 def test_wander_does_nothing_before_its_cooldown_elapses(registry): world = make_world(registry) human = make_human(registry, position=EntityPosition("field", 2, 2, 0), ai=AIController(behavior_id="wander")) world.add_entity(human) run_ai_tick(human, world, registry, now=0.0, rng=FixedRng(choice_index=1)) moved_to = (human.position.x, human.position.y) run_ai_tick(human, world, registry, now=0.1, rng=FixedRng(choice_index=0)) # still on cooldown assert (human.position.x, human.position.y) == moved_to def test_wander_moves_again_once_its_cooldown_expires(registry): world = make_world(registry) human = make_human(registry, position=EntityPosition("field", 2, 2, 0), ai=AIController(behavior_id="wander")) world.add_entity(human) run_ai_tick(human, world, registry, now=0.0, rng=FixedRng(choice_index=1)) # -> (3, 2) next_move_at = human.ai.state["next_move_at"] run_ai_tick(human, world, registry, now=next_move_at, rng=FixedRng(choice_index=3)) # (0, 1) assert (human.position.x, human.position.y) == (3, 3)