The variant handler now supports prefix matching for directional variants. This allows 'pa hi' to match 'parry high', 'pa lo' to match 'parry low', etc. Implementation: - First tries exact match on variant key - Falls back to prefix matching if no exact match - Returns unique match if exactly one variant starts with the prefix - Shows disambiguation message if multiple variants match - Shows error with valid options if no variants match Tests cover exact match, prefix match, ambiguous prefix, no match, single-char prefix, case-insensitivity, and preservation of target args.
213 lines
6.3 KiB
Python
213 lines
6.3 KiB
Python
"""Tests for variant prefix matching in combat commands."""
|
|
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
import mudlib.commands.movement as movement_mod
|
|
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
|
|
|
|
|
|
@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(autouse=True)
|
|
def mock_world():
|
|
"""Inject a mock world for send_nearby_message."""
|
|
fake_world = MagicMock()
|
|
fake_world.width = 256
|
|
fake_world.height = 256
|
|
old = movement_mod.world
|
|
movement_mod.world = fake_world
|
|
yield fake_world
|
|
movement_mod.world = old
|
|
|
|
|
|
@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):
|
|
p = Player(name="Goku", x=0, y=0, reader=mock_reader, writer=mock_writer)
|
|
players[p.name] = p
|
|
return p
|
|
|
|
|
|
@pytest.fixture
|
|
def target(mock_reader, mock_writer):
|
|
t = Player(name="Vegeta", x=0, y=0, reader=mock_reader, writer=mock_writer)
|
|
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)
|