Mi2dRPGamEng/tests/test_ai.py

123 lines
3.9 KiB
Python

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)