Implements a TDD-built 'use' command that lets players invoke object verbs with optional targets: - use X - calls X's use verb - use X on Y - calls X's use verb with Y as args - Proper error messages for missing objects/verbs - Tests cover all edge cases including inventory/ground search Also fixes type checking issue in verb dispatch where get_verb could return None.
195 lines
5.5 KiB
Python
195 lines
5.5 KiB
Python
"""Tests for verb dispatch fallback in command system."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.commands import dispatch
|
|
from mudlib.player import Player
|
|
from mudlib.thing import Thing
|
|
from mudlib.verbs import verb
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@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 test_zone():
|
|
terrain = [["." for _ in range(10)] for _ in range(10)]
|
|
return Zone(name="testzone", width=10, height=10, terrain=terrain)
|
|
|
|
|
|
@pytest.fixture
|
|
def player(mock_reader, mock_writer, test_zone):
|
|
return Player(
|
|
name="TestPlayer",
|
|
x=5,
|
|
y=5,
|
|
reader=mock_reader,
|
|
writer=mock_writer,
|
|
location=test_zone,
|
|
)
|
|
|
|
|
|
class Fountain(Thing):
|
|
"""Test object with drink verb."""
|
|
|
|
@verb("drink")
|
|
async def drink(self, player, args):
|
|
await player.send("You drink from the fountain.\r\n")
|
|
|
|
@verb("splash")
|
|
async def splash(self, player, args):
|
|
await player.send(f"You splash the fountain{' ' + args if args else ''}.\r\n")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_simple(player, test_zone):
|
|
"""Test 'drink fountain' dispatches to fountain's drink verb."""
|
|
_fountain = Fountain(
|
|
name="fountain",
|
|
description="a stone fountain",
|
|
portable=False,
|
|
location=test_zone,
|
|
x=5,
|
|
y=5,
|
|
)
|
|
|
|
await dispatch(player, "drink fountain")
|
|
|
|
player.writer.write.assert_called_once_with("You drink from the fountain.\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_with_extra_args(player, test_zone):
|
|
"""Test 'splash fountain hard' passes 'hard' as extra args."""
|
|
_fountain = Fountain(
|
|
name="fountain",
|
|
description="a stone fountain",
|
|
portable=False,
|
|
location=test_zone,
|
|
x=5,
|
|
y=5,
|
|
)
|
|
|
|
await dispatch(player, "splash fountain hard")
|
|
|
|
player.writer.write.assert_called_once_with("You splash the fountain hard.\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_object_lacks_verb(player, test_zone):
|
|
"""Test 'drink rock' fails if rock doesn't have drink verb."""
|
|
_rock = Thing(
|
|
name="rock",
|
|
description="a plain rock",
|
|
portable=True,
|
|
location=test_zone,
|
|
x=5,
|
|
y=5,
|
|
)
|
|
|
|
await dispatch(player, "drink rock")
|
|
|
|
player.writer.write.assert_called_once_with("Unknown command: drink\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_object_not_found(player, test_zone):
|
|
"""Test 'drink flurb' fails if flurb doesn't exist."""
|
|
await dispatch(player, "drink flurb")
|
|
|
|
player.writer.write.assert_called_once_with("Unknown command: drink\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_no_args(player, test_zone):
|
|
"""Test 'drink' with no args doesn't try verb dispatch."""
|
|
await dispatch(player, "drink")
|
|
|
|
player.writer.write.assert_called_once_with("Unknown command: drink\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_doesnt_intercept_real_commands(player, test_zone):
|
|
"""Test that verb dispatch doesn't interfere with registered commands."""
|
|
from mudlib.commands import CommandDefinition, register, unregister
|
|
|
|
called = False
|
|
|
|
async def test_command(player, args):
|
|
nonlocal called
|
|
called = True
|
|
await player.send("Real command executed.\r\n")
|
|
|
|
register(CommandDefinition("test", test_command))
|
|
try:
|
|
await dispatch(player, "test something")
|
|
assert called
|
|
player.writer.write.assert_called_once_with("Real command executed.\r\n")
|
|
finally:
|
|
unregister("test")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_finds_inventory_object(player, test_zone):
|
|
"""Test verb dispatch finds object in inventory."""
|
|
|
|
class Potion(Thing):
|
|
@verb("drink")
|
|
async def drink(self, player, args):
|
|
await player.send("You drink the potion.\r\n")
|
|
|
|
_potion = Potion(
|
|
name="potion",
|
|
description="a health potion",
|
|
portable=True,
|
|
location=player,
|
|
)
|
|
|
|
await dispatch(player, "drink potion")
|
|
|
|
player.writer.write.assert_called_once_with("You drink the potion.\r\n")
|
|
player.writer.drain.assert_awaited_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_verb_dispatch_doesnt_fire_on_ambiguous_command(player, test_zone):
|
|
"""Test verb dispatch doesn't fire when command is ambiguous."""
|
|
from mudlib.commands import CommandDefinition, register, unregister
|
|
|
|
async def test_one(player, args):
|
|
await player.send("test one\r\n")
|
|
|
|
async def test_two(player, args):
|
|
await player.send("test two\r\n")
|
|
|
|
register(CommandDefinition("testone", test_one))
|
|
register(CommandDefinition("testtwo", test_two))
|
|
try:
|
|
# "test" matches both testone and testtwo
|
|
await dispatch(player, "test something")
|
|
|
|
# Should get ambiguous command message, not verb dispatch
|
|
written_text = player.writer.write.call_args[0][0].lower()
|
|
# The ambiguous message is "testone or testtwo?" not "ambiguous"
|
|
assert "or" in written_text and "?" in written_text
|
|
finally:
|
|
unregister("testone")
|
|
unregister("testtwo")
|