mud/tests/test_variant_prefix.py
Jared Miller 404a1cdf0c
Migrate movement to use player.location (Zone)
Movement commands now access the zone through player.location instead of
a module-level world variable. send_nearby_message uses
zone.contents_near() to find nearby entities, eliminating the need for
the global players dict and manual distance calculations.

Tests updated to create zones and add entities via location assignment.
2026-02-11 19:28:27 -05:00

229 lines
6.8 KiB
Python

"""Tests for variant prefix matching in combat commands."""
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.combat import commands as combat_commands
from mudlib.combat.engine import active_encounters
from mudlib.combat.moves import load_moves
from mudlib.player import Player, players
from mudlib.zone import Zone
@pytest.fixture(autouse=True)
def clear_state():
"""Clear encounters and players before and after each test."""
active_encounters.clear()
players.clear()
yield
active_encounters.clear()
players.clear()
@pytest.fixture
def test_zone():
"""Create a test zone for players."""
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(autouse=True)
def inject_world_for_combat(test_zone):
"""Inject test_zone into combat commands (still uses module-level world)."""
old_combat = combat_commands.world
combat_commands.world = test_zone
yield test_zone
combat_commands.world = old_combat
@pytest.fixture
def mock_writer():
writer = MagicMock()
writer.write = MagicMock()
writer.drain = AsyncMock()
return writer
@pytest.fixture
def mock_reader():
return MagicMock()
@pytest.fixture
def player(mock_reader, mock_writer, test_zone):
p = Player(name="Goku", 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 target(mock_reader, mock_writer, test_zone):
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
t.location = test_zone
test_zone._contents.append(t)
players[t.name] = t
return t
@pytest.fixture
def moves():
"""Load combat moves from content directory."""
content_dir = Path(__file__).parent.parent / "content" / "combat"
return load_moves(content_dir)
# --- variant prefix matching tests ---
@pytest.mark.asyncio
async def test_exact_match_still_works(player, target, moves):
"""Test that exact variant names still work (e.g., 'high' matches 'high')."""
variant_moves = {
"high": moves["parry high"],
"low": moves["parry low"],
}
handler = combat_commands.make_variant_handler(
"parry", variant_moves, combat_commands.do_defend
)
await handler(player, "high")
# Should succeed without error
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert not any("unknown" in msg.lower() for msg in messages)
assert not any("ambiguous" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_prefix_match_works(player, target, moves):
"""Test that prefix matching resolves unique prefixes (e.g., 'hi' -> 'high')."""
variant_moves = {
"high": moves["parry high"],
"low": moves["parry low"],
}
handler = combat_commands.make_variant_handler(
"parry", variant_moves, combat_commands.do_defend
)
# Test "hi" -> "high"
player.writer.write.reset_mock()
await handler(player, "hi")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert not any("unknown" in msg.lower() for msg in messages)
assert not any("ambiguous" in msg.lower() for msg in messages)
# Test "lo" -> "low"
player.writer.write.reset_mock()
await handler(player, "lo")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert not any("unknown" in msg.lower() for msg in messages)
assert not any("ambiguous" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_ambiguous_prefix_shows_disambiguation(player, moves):
"""Test that ambiguous prefix shows options (e.g., 'l' for 'left'/'long')."""
# Create hypothetical moves with conflicting prefixes
variant_moves = {
"left": moves["punch left"],
"long": moves["punch left"], # reuse same move, just for testing keys
}
handler = combat_commands.make_variant_handler(
"punch", variant_moves, combat_commands.do_attack
)
await handler(player, "l")
messages = [call[0][0] for call in player.writer.write.call_args_list]
output = "".join(messages).lower()
assert "ambiguous" in output
assert "left" in output
assert "long" in output
@pytest.mark.asyncio
async def test_no_match_shows_error(player, moves):
"""Test that no matching variant shows error with valid options."""
variant_moves = {
"high": moves["parry high"],
"low": moves["parry low"],
}
handler = combat_commands.make_variant_handler(
"parry", variant_moves, combat_commands.do_defend
)
await handler(player, "middle")
messages = [call[0][0] for call in player.writer.write.call_args_list]
output = "".join(messages).lower()
assert "unknown" in output
assert "high" in output or "low" in output
@pytest.mark.asyncio
async def test_prefix_with_target_args(player, target, moves):
"""Test that prefix matching preserves target arguments."""
variant_moves = {
"left": moves["punch left"],
"right": moves["punch right"],
}
handler = combat_commands.make_variant_handler(
"punch", variant_moves, combat_commands.do_attack
)
await handler(player, "le Vegeta")
# Should start combat with target
from mudlib.combat.engine import get_encounter
encounter = get_encounter(player)
assert encounter is not None
assert encounter.defender is target
@pytest.mark.asyncio
async def test_single_char_prefix_match(player, moves):
"""Test that single-char prefix works when unambiguous (e.g., 'h' -> 'high')."""
variant_moves = {
"high": moves["parry high"],
"low": moves["parry low"],
}
handler = combat_commands.make_variant_handler(
"parry", variant_moves, combat_commands.do_defend
)
await handler(player, "h")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert not any("unknown" in msg.lower() for msg in messages)
assert not any("ambiguous" in msg.lower() for msg in messages)
@pytest.mark.asyncio
async def test_case_insensitive_prefix_match(player, moves):
"""Test that prefix matching is case-insensitive."""
variant_moves = {
"high": moves["parry high"],
"low": moves["parry low"],
}
handler = combat_commands.make_variant_handler(
"parry", variant_moves, combat_commands.do_defend
)
await handler(player, "HI")
messages = [call[0][0] for call in player.writer.write.call_args_list]
assert not any("unknown" in msg.lower() for msg in messages)
assert not any("ambiguous" in msg.lower() for msg in messages)