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.
229 lines
6.8 KiB
Python
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)
|