mud/tests/test_verb_dispatch.py
Jared Miller d2de6bdc16
Add use command for verb-based interaction
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.
2026-02-11 21:47:33 -05:00

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