"""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)