"""Tests for mob AI integration with behavior states.""" from pathlib import Path import pytest from mudlib.combat.commands import combat_moves from mudlib.combat.engine import active_encounters from mudlib.combat.moves import load_moves from mudlib.entity import Mob from mudlib.mob_ai import process_mob_movement from mudlib.mobs import mobs from mudlib.npc_behavior import transition_state from mudlib.player import Player, players from mudlib.zone import Zone @pytest.fixture(autouse=True) def clear_state(): """Clear mobs, encounters, and players before and after each test.""" mobs.clear() active_encounters.clear() players.clear() yield mobs.clear() active_encounters.clear() players.clear() @pytest.fixture def test_zone(): """Create a test zone for entities.""" terrain = [["." for _ in range(256)] for _ in range(256)] zone = Zone( name="testzone", width=256, height=256, toroidal=True, terrain=terrain, impassable=set(), ) return zone @pytest.fixture def player(mock_reader, mock_writer, test_zone): p = Player(name="Hero", x=0, y=0, reader=mock_reader, writer=mock_writer) p.location = test_zone test_zone._contents.append(p) players[p.name] = p return p @pytest.fixture def moves(): """Load combat moves from content directory.""" content_dir = Path(__file__).parent.parent / "content" / "combat" return load_moves(content_dir) @pytest.fixture(autouse=True) def inject_moves(moves): """Inject loaded moves into combat commands module.""" combat_moves.update(moves) yield combat_moves.clear() class TestConverseBehaviorBlocksMovement: @pytest.mark.asyncio async def test_converse_state_prevents_wander(self, test_zone): """Mob in converse state doesn't wander (stays put).""" mob = Mob( name="librarian", x=10, y=10, location=test_zone, home_x_min=0, home_x_max=5, home_y_min=0, home_y_max=5, ) mobs.append(mob) mob.next_action_at = 0.0 # cooldown expired # Transition to converse state transition_state(mob, "converse") # Process movement await process_mob_movement() # Mob should not have moved assert mob.x == 10 assert mob.y == 10 class TestWorkingBehaviorBlocksMovement: @pytest.mark.asyncio async def test_working_state_prevents_wander(self, test_zone): """Mob in working state doesn't wander (stays at post).""" mob = Mob( name="blacksmith", x=20, y=20, location=test_zone, home_x_min=0, home_x_max=5, home_y_min=0, home_y_max=5, ) mobs.append(mob) mob.next_action_at = 0.0 # cooldown expired # Transition to working state transition_state(mob, "working") # Process movement await process_mob_movement() # Mob should not have moved assert mob.x == 20 assert mob.y == 20 class TestPatrolBehaviorUsesWaypoints: @pytest.mark.asyncio async def test_patrol_moves_toward_waypoint_not_home(self, test_zone): """Mob in patrol state moves toward waypoint, not home region.""" mob = Mob( name="guard", x=10, y=10, location=test_zone, home_x_min=0, home_x_max=5, home_y_min=0, home_y_max=5, ) mobs.append(mob) mob.next_action_at = 0.0 # Set patrol with waypoint to the east waypoints = [{"x": 20, "y": 10}] transition_state(mob, "patrol", {"waypoints": waypoints, "waypoint_index": 0}) # Process movement await process_mob_movement() # Mob should have moved east (toward waypoint at 20,10) # NOT west toward home region (0-5, 0-5) assert mob.x == 11 assert mob.y == 10 @pytest.mark.asyncio async def test_patrol_advances_waypoint_on_arrival(self, test_zone): """Mob in patrol state advances waypoint when reaching it.""" waypoints = [{"x": 10, "y": 10}, {"x": 20, "y": 20}] mob = Mob( name="guard", x=10, y=10, location=test_zone, home_x_min=0, home_x_max=5, home_y_min=0, home_y_max=5, ) mobs.append(mob) mob.next_action_at = 0.0 transition_state(mob, "patrol", {"waypoints": waypoints, "waypoint_index": 0}) # At first waypoint already await process_mob_movement() # Waypoint should advance (handled by process_behavior in npc_behavior) # But movement is skipped since we're at the waypoint # Mob advances waypoint, then on next tick moves toward new waypoint assert mob.behavior_data["waypoint_index"] == 1 class TestFleeBehavior: @pytest.mark.asyncio async def test_flee_moves_away_from_threat(self, test_zone): """Mob in flee state moves away from threat.""" mob = Mob( name="rabbit", x=10, y=10, location=test_zone, home_x_min=8, home_x_max=12, home_y_min=8, home_y_max=12, ) mobs.append(mob) mob.next_action_at = 0.0 # Threat to the east flee_data = {"flee_from": {"x": 15, "y": 10}} transition_state(mob, "flee", flee_data) # Process movement await process_mob_movement() # Mob should have moved west (away from threat) assert mob.x == 9 assert mob.y == 10 class TestIdleBehaviorUsesHomeRegion: @pytest.mark.asyncio async def test_idle_uses_original_wander_logic(self, test_zone): """Mob in idle state uses original home-region wander logic.""" mob = Mob( name="wanderer", x=50, y=50, location=test_zone, home_x_min=0, home_x_max=5, home_y_min=0, home_y_max=5, ) mobs.append(mob) mob.next_action_at = 0.0 # Explicitly set idle state transition_state(mob, "idle") original_x = mob.x original_y = mob.y # Process movement await process_mob_movement() # Mob should have moved toward home region (toward 0-5, 0-5) # Since mob is at (50, 50) and home is (0-5, 0-5), should move west or north assert mob.x < original_x or mob.y < original_y