Removed identical local copies from 45 test files. These fixtures are already defined in conftest.py.
238 lines
6.5 KiB
Python
238 lines
6.5 KiB
Python
"""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
|