Compare commits
No commits in common. "e724abb92662f03420c36415ebf361b7f9bcb1ce" and "7d4a75f9732165dfadfb8dac5abaffa8ba544589" have entirely different histories.
e724abb926
...
7d4a75f973
17 changed files with 12 additions and 1582 deletions
|
|
@ -27,8 +27,6 @@ Telnet MUD engine built on telnetlib3. Python 3.12+, managed with uv.
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
Consult `docs/index.rst` when starting unfamiliar work to find relevant docs.
|
|
||||||
|
|
||||||
Three categories in `docs/`. Plain text or rst, not markdown.
|
Three categories in `docs/`. Plain text or rst, not markdown.
|
||||||
|
|
||||||
- `docs/how/` - how things work. write one when you build something non-obvious.
|
- `docs/how/` - how things work. write one when you build something non-obvious.
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,7 @@ portals:
|
||||||
depends on: phase 3 (portals and containers are Things)
|
depends on: phase 3 (portals and containers are Things)
|
||||||
enables: multi-zone world, interior spaces, locked doors, bags, keys
|
enables: multi-zone world, interior spaces, locked doors, bags, keys
|
||||||
refs: object-model.rst (Container, Portal sections),
|
refs: object-model.rst (Container, Portal sections),
|
||||||
zones-and-building.rst (zone data format, hand-built zones),
|
zones-and-building.rst (zone data format, hand-built zones)
|
||||||
persistence.txt (what's persisted vs runtime — container contents not yet)
|
|
||||||
|
|
||||||
|
|
||||||
phase 5: verbs + interaction
|
phase 5: verbs + interaction
|
||||||
|
|
|
||||||
|
|
@ -145,24 +145,7 @@ async def dispatch(player: Player, raw_input: str) -> None:
|
||||||
result = resolve_prefix(command)
|
result = resolve_prefix(command)
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
# No matches - try verb dispatch as fallback
|
# No matches
|
||||||
if args:
|
|
||||||
from mudlib.verbs import find_object
|
|
||||||
|
|
||||||
# Split args into target name and any extra arguments
|
|
||||||
target_parts = args.split(maxsplit=1)
|
|
||||||
target_name = target_parts[0]
|
|
||||||
extra_args = target_parts[1] if len(target_parts) > 1 else ""
|
|
||||||
|
|
||||||
# Try to find the object
|
|
||||||
obj = find_object(target_name, player)
|
|
||||||
if obj is not None and obj.has_verb(command):
|
|
||||||
handler = obj.get_verb(command)
|
|
||||||
assert handler is not None # has_verb checked above
|
|
||||||
await handler(player, extra_args)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Still no match - show unknown command
|
|
||||||
player.writer.write(f"Unknown command: {command}\r\n")
|
player.writer.write(f"Unknown command: {command}\r\n")
|
||||||
await player.writer.drain()
|
await player.writer.drain()
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
"""Examine command for detailed object inspection."""
|
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
|
||||||
from mudlib.entity import Entity
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
|
|
||||||
def _find_object_in_inventory(name: str, player: Player) -> Thing | Entity | None:
|
|
||||||
"""Find an object in player inventory by name or alias."""
|
|
||||||
name_lower = name.lower()
|
|
||||||
for obj in player.contents:
|
|
||||||
# Only examine Things and Entities
|
|
||||||
if not isinstance(obj, (Thing, Entity)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Match by name
|
|
||||||
if obj.name.lower() == name_lower:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# Match by alias (Things have aliases)
|
|
||||||
if isinstance(obj, Thing) and name_lower in (a.lower() for a in obj.aliases):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _find_object_at_position(name: str, player: Player) -> Thing | Entity | None:
|
|
||||||
"""Find an object on the ground at player position by name or alias."""
|
|
||||||
zone = player.location
|
|
||||||
if zone is None or not isinstance(zone, Zone):
|
|
||||||
return None
|
|
||||||
|
|
||||||
name_lower = name.lower()
|
|
||||||
for obj in zone.contents_at(player.x, player.y):
|
|
||||||
# Only examine Things and Entities
|
|
||||||
if not isinstance(obj, (Thing, Entity)):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Match by name
|
|
||||||
if obj.name.lower() == name_lower:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# Match by alias (Things have aliases)
|
|
||||||
if isinstance(obj, Thing) and name_lower in (a.lower() for a in obj.aliases):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def cmd_examine(player: Player, args: str) -> None:
|
|
||||||
"""Examine an object in detail."""
|
|
||||||
if not args.strip():
|
|
||||||
await player.send("Examine what?\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
target_name = args.strip()
|
|
||||||
|
|
||||||
# Search inventory first
|
|
||||||
found = _find_object_in_inventory(target_name, player)
|
|
||||||
|
|
||||||
# Then search ground
|
|
||||||
if not found:
|
|
||||||
found = _find_object_at_position(target_name, player)
|
|
||||||
|
|
||||||
# Not found anywhere
|
|
||||||
if not found:
|
|
||||||
await player.send("You don't see that here.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Show description (both Thing and Entity have description)
|
|
||||||
desc = getattr(found, "description", "")
|
|
||||||
if desc:
|
|
||||||
await player.send(f"{desc}\r\n")
|
|
||||||
else:
|
|
||||||
await player.send("You see nothing special.\r\n")
|
|
||||||
|
|
||||||
|
|
||||||
register(CommandDefinition("examine", cmd_examine, aliases=["ex"], mode="*"))
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
from mudlib.commands import CommandDefinition, register
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.verbs import find_object
|
|
||||||
|
|
||||||
|
|
||||||
async def cmd_use(player: Player, args: str) -> None:
|
|
||||||
"""Use an object, optionally on a target."""
|
|
||||||
args = args.strip()
|
|
||||||
|
|
||||||
if not args:
|
|
||||||
await player.send("Use what?\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Parse "use X on Y" syntax
|
|
||||||
if " on " in args:
|
|
||||||
parts = args.split(" on ", 1)
|
|
||||||
object_name = parts[0].strip()
|
|
||||||
target_args = parts[1].strip()
|
|
||||||
else:
|
|
||||||
object_name = args
|
|
||||||
target_args = ""
|
|
||||||
|
|
||||||
# Find the object
|
|
||||||
obj = find_object(object_name, player)
|
|
||||||
if not obj:
|
|
||||||
await player.send("You don't see that here.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check if object has use verb
|
|
||||||
if not obj.has_verb("use"):
|
|
||||||
await player.send("You can't use that.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Call the use verb
|
|
||||||
use_handler = obj.get_verb("use")
|
|
||||||
assert use_handler is not None # has_verb checked above
|
|
||||||
await use_handler(player, target_args)
|
|
||||||
|
|
||||||
|
|
||||||
register(CommandDefinition("use", cmd_use, mode="*"))
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,24 +22,11 @@ class Object:
|
||||||
_contents: list[Object] = field(
|
_contents: list[Object] = field(
|
||||||
default_factory=list, init=False, repr=False, compare=False
|
default_factory=list, init=False, repr=False, compare=False
|
||||||
)
|
)
|
||||||
_verbs: dict[str, Callable[..., Awaitable[None]]] = field(
|
|
||||||
default_factory=dict, init=False, repr=False, compare=False
|
|
||||||
)
|
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
if self.location is not None:
|
if self.location is not None:
|
||||||
self.location._contents.append(self)
|
self.location._contents.append(self)
|
||||||
|
|
||||||
# Auto-register verbs from @verb decorated methods
|
|
||||||
for attr_name in dir(self):
|
|
||||||
# Skip private/magic attributes
|
|
||||||
if attr_name.startswith("_"):
|
|
||||||
continue
|
|
||||||
attr = getattr(self, attr_name)
|
|
||||||
# Check if this is a verb-decorated method
|
|
||||||
if hasattr(attr, "_verb_name"):
|
|
||||||
self.register_verb(attr._verb_name, attr)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contents(self) -> list[Object]:
|
def contents(self) -> list[Object]:
|
||||||
"""Everything whose location is this object."""
|
"""Everything whose location is this object."""
|
||||||
|
|
@ -75,15 +61,3 @@ class Object:
|
||||||
def can_accept(self, obj: Object) -> bool:
|
def can_accept(self, obj: Object) -> bool:
|
||||||
"""Whether this object accepts obj as contents. Default: no."""
|
"""Whether this object accepts obj as contents. Default: no."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def register_verb(self, name: str, handler: Callable[..., Awaitable[None]]) -> None:
|
|
||||||
"""Register a verb handler on this object."""
|
|
||||||
self._verbs[name] = handler
|
|
||||||
|
|
||||||
def get_verb(self, name: str) -> Callable[..., Awaitable[None]] | None:
|
|
||||||
"""Get a verb handler by name, or None if not found."""
|
|
||||||
return self._verbs.get(name)
|
|
||||||
|
|
||||||
def has_verb(self, name: str) -> bool:
|
|
||||||
"""Check if this object has a verb registered."""
|
|
||||||
return name in self._verbs
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import mudlib.combat.commands
|
||||||
import mudlib.commands
|
import mudlib.commands
|
||||||
import mudlib.commands.containers
|
import mudlib.commands.containers
|
||||||
import mudlib.commands.edit
|
import mudlib.commands.edit
|
||||||
import mudlib.commands.examine
|
|
||||||
import mudlib.commands.fly
|
import mudlib.commands.fly
|
||||||
import mudlib.commands.help
|
import mudlib.commands.help
|
||||||
import mudlib.commands.look
|
import mudlib.commands.look
|
||||||
|
|
@ -26,7 +25,6 @@ import mudlib.commands.quit
|
||||||
import mudlib.commands.reload
|
import mudlib.commands.reload
|
||||||
import mudlib.commands.spawn
|
import mudlib.commands.spawn
|
||||||
import mudlib.commands.things
|
import mudlib.commands.things
|
||||||
import mudlib.commands.use
|
|
||||||
from mudlib.caps import parse_mtts
|
from mudlib.caps import parse_mtts
|
||||||
from mudlib.combat.commands import register_combat_commands
|
from mudlib.combat.commands import register_combat_commands
|
||||||
from mudlib.combat.engine import process_combat
|
from mudlib.combat.engine import process_combat
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
"""Thing template loading, registry, and spawning."""
|
"""Thing template loading, registry, and spawning."""
|
||||||
|
|
||||||
import functools
|
|
||||||
import importlib
|
|
||||||
import logging
|
|
||||||
import tomllib
|
import tomllib
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -12,8 +8,6 @@ from mudlib.container import Container
|
||||||
from mudlib.object import Object
|
from mudlib.object import Object
|
||||||
from mudlib.thing import Thing
|
from mudlib.thing import Thing
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ThingTemplate:
|
class ThingTemplate:
|
||||||
|
|
@ -27,25 +21,12 @@ class ThingTemplate:
|
||||||
capacity: int | None = None
|
capacity: int | None = None
|
||||||
closed: bool = False
|
closed: bool = False
|
||||||
locked: bool = False
|
locked: bool = False
|
||||||
# Verb handlers (verb_name -> module:function reference)
|
|
||||||
verbs: dict[str, str] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
# Module-level registry
|
# Module-level registry
|
||||||
thing_templates: dict[str, ThingTemplate] = {}
|
thing_templates: dict[str, ThingTemplate] = {}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_handler(ref: str) -> Callable[..., Awaitable[None]] | None:
|
|
||||||
"""Resolve a 'module:function' reference to a callable."""
|
|
||||||
module_path, _, func_name = ref.rpartition(":")
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(module_path)
|
|
||||||
return getattr(module, func_name)
|
|
||||||
except (ImportError, AttributeError) as e:
|
|
||||||
logger.warning("Failed to resolve verb handler '%s': %s", ref, e)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def load_thing_template(path: Path) -> ThingTemplate:
|
def load_thing_template(path: Path) -> ThingTemplate:
|
||||||
"""Parse a thing TOML file into a ThingTemplate."""
|
"""Parse a thing TOML file into a ThingTemplate."""
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
|
|
@ -58,7 +39,6 @@ def load_thing_template(path: Path) -> ThingTemplate:
|
||||||
capacity=data.get("capacity"),
|
capacity=data.get("capacity"),
|
||||||
closed=data.get("closed", False),
|
closed=data.get("closed", False),
|
||||||
locked=data.get("locked", False),
|
locked=data.get("locked", False),
|
||||||
verbs=data.get("verbs", {}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,7 +61,7 @@ def spawn_thing(
|
||||||
"""Create a Thing instance from a template at the given location."""
|
"""Create a Thing instance from a template at the given location."""
|
||||||
# If template has capacity, spawn a Container instead of a Thing
|
# If template has capacity, spawn a Container instead of a Thing
|
||||||
if template.capacity is not None:
|
if template.capacity is not None:
|
||||||
obj = Container(
|
return Container(
|
||||||
name=template.name,
|
name=template.name,
|
||||||
description=template.description,
|
description=template.description,
|
||||||
portable=template.portable,
|
portable=template.portable,
|
||||||
|
|
@ -93,24 +73,13 @@ def spawn_thing(
|
||||||
x=x,
|
x=x,
|
||||||
y=y,
|
y=y,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
obj = Thing(
|
|
||||||
name=template.name,
|
|
||||||
description=template.description,
|
|
||||||
portable=template.portable,
|
|
||||||
aliases=list(template.aliases),
|
|
||||||
location=location,
|
|
||||||
x=x,
|
|
||||||
y=y,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Register verbs from the template
|
return Thing(
|
||||||
for verb_name, handler_ref in template.verbs.items():
|
name=template.name,
|
||||||
handler = _resolve_handler(handler_ref)
|
description=template.description,
|
||||||
if handler is None:
|
portable=template.portable,
|
||||||
continue
|
aliases=list(template.aliases),
|
||||||
# Bind the object as first argument using partial
|
location=location,
|
||||||
bound_handler = functools.partial(handler, obj)
|
x=x,
|
||||||
obj.register_verb(verb_name, bound_handler)
|
y=y,
|
||||||
|
)
|
||||||
return obj
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
"""Verb handlers for object interactions."""
|
|
||||||
|
|
||||||
|
|
||||||
async def unlock_handler(obj, player, args):
|
|
||||||
"""
|
|
||||||
Handle unlocking a container.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj: The object being unlocked (should be a Container)
|
|
||||||
player: The player attempting to unlock
|
|
||||||
args: Additional arguments (unused)
|
|
||||||
"""
|
|
||||||
from mudlib.container import Container
|
|
||||||
|
|
||||||
if not isinstance(obj, Container):
|
|
||||||
await player.send("That can't be unlocked.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not obj.locked:
|
|
||||||
await player.send("That's not locked.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check for key in player inventory
|
|
||||||
key = None
|
|
||||||
for item in player.contents:
|
|
||||||
if item.name.lower() == "key" or "key" in [
|
|
||||||
a.lower() for a in getattr(item, "aliases", [])
|
|
||||||
]:
|
|
||||||
key = item
|
|
||||||
break
|
|
||||||
|
|
||||||
if key is None:
|
|
||||||
await player.send("You don't have a key.\r\n")
|
|
||||||
return
|
|
||||||
|
|
||||||
obj.locked = False
|
|
||||||
obj.closed = False
|
|
||||||
await player.send(f"You unlock the {obj.name} with the {key.name}.\r\n")
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
"""Verb system for interactive objects."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from mudlib.object import Object
|
|
||||||
from mudlib.player import Player
|
|
||||||
|
|
||||||
|
|
||||||
def verb(name: str) -> Callable:
|
|
||||||
"""Decorator to mark a method as a verb handler.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
class Fountain(Thing):
|
|
||||||
@verb("drink")
|
|
||||||
async def drink(self, player, args):
|
|
||||||
await player.send("You drink from the fountain.\\r\\n")
|
|
||||||
"""
|
|
||||||
|
|
||||||
def decorator(func: Callable) -> Callable:
|
|
||||||
func._verb_name = name # type: ignore
|
|
||||||
return func
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def find_object(name: str, player: Player) -> Object | None:
|
|
||||||
"""Find an object by name or alias.
|
|
||||||
|
|
||||||
Searches inventory first, then ground at player position.
|
|
||||||
Works on all Objects that have a name and optional aliases attribute.
|
|
||||||
"""
|
|
||||||
name_lower = name.lower()
|
|
||||||
|
|
||||||
# Search inventory first
|
|
||||||
for obj in player.contents:
|
|
||||||
if obj.name.lower() == name_lower:
|
|
||||||
return obj
|
|
||||||
# Check aliases if the object has them
|
|
||||||
aliases = getattr(obj, "aliases", None)
|
|
||||||
if aliases and name_lower in (a.lower() for a in aliases):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
# Search ground at player position
|
|
||||||
if player.location is not None:
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
if isinstance(player.location, Zone):
|
|
||||||
for obj in player.location.contents_at(player.x, player.y):
|
|
||||||
if obj is player:
|
|
||||||
continue
|
|
||||||
if obj.name.lower() == name_lower:
|
|
||||||
return obj
|
|
||||||
# Check aliases if the object has them
|
|
||||||
aliases = getattr(obj, "aliases", None)
|
|
||||||
if aliases and name_lower in (a.lower() for a in aliases):
|
|
||||||
return obj
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
"""Tests for mudlib."""
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.commands.examine import cmd_examine
|
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.entity import Mob
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_no_args(player, mock_writer):
|
|
||||||
"""examine with no args prompts what to examine"""
|
|
||||||
await cmd_examine(player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "Examine what?" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_thing_on_ground_by_name(player, test_zone, mock_writer):
|
|
||||||
"""examine finds Thing on ground by name and shows description"""
|
|
||||||
_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="A smooth gray rock with moss on one side.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "rock")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A smooth gray rock with moss on one side." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_thing_on_ground_by_alias(player, test_zone, mock_writer):
|
|
||||||
"""examine finds Thing on ground by alias"""
|
|
||||||
_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
aliases=["stone"],
|
|
||||||
description="A smooth gray rock.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "stone")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A smooth gray rock." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_thing_in_inventory_by_name(player, test_zone, mock_writer):
|
|
||||||
"""examine finds Thing in inventory by name"""
|
|
||||||
_key = Thing(
|
|
||||||
name="key",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=player,
|
|
||||||
description="A rusty iron key.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "key")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A rusty iron key." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_inventory_before_ground(player, test_zone, mock_writer):
|
|
||||||
"""examine finds in inventory before ground"""
|
|
||||||
_ground_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="A rock on the ground.",
|
|
||||||
)
|
|
||||||
_inventory_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=player,
|
|
||||||
description="A rock in your pocket.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "rock")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A rock in your pocket." in output
|
|
||||||
assert "A rock on the ground." not in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_mob_at_position(player, test_zone, mock_writer):
|
|
||||||
"""examine finds Mob at same position and shows description"""
|
|
||||||
_goblin = Mob(
|
|
||||||
name="goblin",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="A small, greenish creature with beady eyes.",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "goblin")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A small, greenish creature with beady eyes." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_not_found(player, mock_writer):
|
|
||||||
"""examine nonexistent object shows not found message"""
|
|
||||||
await cmd_examine(player, "flurb")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "You don't see that here." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_thing_with_empty_description(player, test_zone, mock_writer):
|
|
||||||
"""examine thing with empty description shows default message"""
|
|
||||||
_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="",
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "rock")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "You see nothing special." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_container_shows_description(player, test_zone, mock_writer):
|
|
||||||
"""examine Container shows description"""
|
|
||||||
_chest = Container(
|
|
||||||
name="chest",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="A sturdy wooden chest with brass hinges.",
|
|
||||||
closed=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_examine(player, "chest")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A sturdy wooden chest with brass hinges." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_examine_alias_ex(player, test_zone, mock_writer):
|
|
||||||
"""ex alias works"""
|
|
||||||
_rock = Thing(
|
|
||||||
name="rock",
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
location=test_zone,
|
|
||||||
description="A smooth rock.",
|
|
||||||
)
|
|
||||||
|
|
||||||
# The command registration will handle the alias, but test function
|
|
||||||
await cmd_examine(player, "rock")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "A smooth rock." in output
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.container import Container
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_on_non_container(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler on non-container gives appropriate error"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
thing = Thing(
|
|
||||||
name="rock",
|
|
||||||
description="a rock",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
await unlock_handler(thing, player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "can't be unlocked" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_on_unlocked_container(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler on unlocked container says it's not locked"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
chest = Container(
|
|
||||||
name="chest",
|
|
||||||
description="a wooden chest",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
closed=True,
|
|
||||||
locked=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
await unlock_handler(chest, player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "not locked" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_without_key(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler on locked container without key in inventory"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
chest = Container(
|
|
||||||
name="chest",
|
|
||||||
description="a wooden chest",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
closed=True,
|
|
||||||
locked=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
await unlock_handler(chest, player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "don't have a key" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_with_key(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler unlocks and opens chest with key in inventory"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
chest = Container(
|
|
||||||
name="chest",
|
|
||||||
description="a wooden chest",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
closed=True,
|
|
||||||
locked=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
key = Thing(
|
|
||||||
name="key",
|
|
||||||
description="a brass key",
|
|
||||||
location=player,
|
|
||||||
)
|
|
||||||
assert key in player.contents
|
|
||||||
|
|
||||||
await unlock_handler(chest, player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "You unlock the chest with the key" in output
|
|
||||||
assert not chest.locked
|
|
||||||
assert not chest.closed
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_key_found_by_alias(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler finds key even if it has a different name"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
chest = Container(
|
|
||||||
name="chest",
|
|
||||||
description="a wooden chest",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
closed=True,
|
|
||||||
locked=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
key = Thing(
|
|
||||||
name="brass key",
|
|
||||||
description="a brass key",
|
|
||||||
location=player,
|
|
||||||
)
|
|
||||||
key.aliases = ["key"]
|
|
||||||
|
|
||||||
await unlock_handler(chest, player, "")
|
|
||||||
|
|
||||||
output = "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
assert "You unlock the chest with the brass key" in output
|
|
||||||
assert not chest.locked
|
|
||||||
assert not chest.closed
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_unlock_handler_preserves_chest_state(player, test_zone, mock_writer):
|
|
||||||
"""unlock_handler changes locked and closed but preserves other state"""
|
|
||||||
from mudlib.verb_handlers import unlock_handler
|
|
||||||
|
|
||||||
chest = Container(
|
|
||||||
name="chest",
|
|
||||||
description="a wooden chest",
|
|
||||||
location=test_zone,
|
|
||||||
x=5,
|
|
||||||
y=5,
|
|
||||||
closed=True,
|
|
||||||
locked=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
inner_thing = Thing(
|
|
||||||
name="coin",
|
|
||||||
description="a gold coin",
|
|
||||||
location=chest,
|
|
||||||
)
|
|
||||||
|
|
||||||
key = Thing(
|
|
||||||
name="key",
|
|
||||||
description="a brass key",
|
|
||||||
location=player,
|
|
||||||
)
|
|
||||||
assert key in player.contents
|
|
||||||
|
|
||||||
await unlock_handler(chest, player, "")
|
|
||||||
|
|
||||||
assert not chest.locked
|
|
||||||
assert not chest.closed
|
|
||||||
assert inner_thing in chest.contents
|
|
||||||
assert chest.location == test_zone
|
|
||||||
|
|
@ -1,245 +0,0 @@
|
||||||
"""Tests for TOML verb support in thing templates."""
|
|
||||||
|
|
||||||
import textwrap
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.things import (
|
|
||||||
Container,
|
|
||||||
ThingTemplate,
|
|
||||||
load_thing_template,
|
|
||||||
spawn_thing,
|
|
||||||
)
|
|
||||||
from mudlib.zone import Zone
|
|
||||||
|
|
||||||
|
|
||||||
# Test handler functions
|
|
||||||
async def _test_verb_handler(obj, player, args):
|
|
||||||
"""Test verb handler that sends a message."""
|
|
||||||
await player.send(f"verb on {obj.name} with args={args}\r\n")
|
|
||||||
|
|
||||||
|
|
||||||
async def _another_test_handler(obj, player, args):
|
|
||||||
"""Another test handler."""
|
|
||||||
await player.send(f"another verb on {obj.name}\r\n")
|
|
||||||
|
|
||||||
|
|
||||||
@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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_thing_template_default_empty_verbs():
|
|
||||||
"""ThingTemplate has empty verbs dict by default."""
|
|
||||||
template = ThingTemplate(name="test", description="a test")
|
|
||||||
assert template.verbs == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_template_with_verbs(tmp_path):
|
|
||||||
"""load_thing_template parses [verbs] section from TOML."""
|
|
||||||
toml = tmp_path / "key.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "key"
|
|
||||||
description = "a brass key"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
assert template.verbs == {"unlock": "tests.test_toml_verbs:_test_verb_handler"}
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_template_without_verbs_section(tmp_path):
|
|
||||||
"""load_thing_template works without [verbs] section (backwards compatible)."""
|
|
||||||
toml = tmp_path / "rock.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "rock"
|
|
||||||
description = "a smooth stone"
|
|
||||||
portable = true
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
assert template.verbs == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_template_with_multiple_verbs(tmp_path):
|
|
||||||
"""load_thing_template handles multiple verbs."""
|
|
||||||
toml = tmp_path / "multi.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "door"
|
|
||||||
description = "a wooden door"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
open = "tests.test_toml_verbs:_another_test_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
assert len(template.verbs) == 2
|
|
||||||
assert "unlock" in template.verbs
|
|
||||||
assert "open" in template.verbs
|
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_thing_registers_verbs(test_zone, tmp_path):
|
|
||||||
"""spawn_thing registers verbs on spawned instance."""
|
|
||||||
toml = tmp_path / "key.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "key"
|
|
||||||
description = "a brass key"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
thing = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
|
|
||||||
assert thing.has_verb("unlock")
|
|
||||||
|
|
||||||
|
|
||||||
def test_spawn_container_registers_verbs(test_zone, tmp_path):
|
|
||||||
"""spawn_thing registers verbs on Container instances too."""
|
|
||||||
toml = tmp_path / "chest.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "chest"
|
|
||||||
description = "a wooden chest"
|
|
||||||
capacity = 10
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
container = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
|
|
||||||
assert isinstance(container, Container)
|
|
||||||
assert container.has_verb("unlock")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_spawned_verb_handler_receives_correct_args(test_zone, player, tmp_path):
|
|
||||||
"""Spawned thing's verb handler can be called and receives (player, args)."""
|
|
||||||
toml = tmp_path / "key.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "key"
|
|
||||||
description = "a brass key"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
thing = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
|
|
||||||
# Call the verb handler
|
|
||||||
handler = thing.get_verb("unlock")
|
|
||||||
assert handler is not None
|
|
||||||
await handler(player, "door")
|
|
||||||
|
|
||||||
# Check that the message was sent
|
|
||||||
player.writer.write.assert_called()
|
|
||||||
call_args = player.writer.write.call_args[0][0]
|
|
||||||
assert "verb on key with args=door" in call_args
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_verb_handler_receives_correct_object_instance(
|
|
||||||
test_zone, player, tmp_path
|
|
||||||
):
|
|
||||||
"""Handler receives the correct object instance as first arg (via partial)."""
|
|
||||||
toml = tmp_path / "key.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "brass key"
|
|
||||||
description = "a brass key"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
thing = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
|
|
||||||
# Call the verb handler
|
|
||||||
handler = thing.get_verb("unlock")
|
|
||||||
assert handler is not None
|
|
||||||
await handler(player, "chest")
|
|
||||||
|
|
||||||
# Check that the object name appears in the message (verifying correct object)
|
|
||||||
player.writer.write.assert_called()
|
|
||||||
call_args = player.writer.write.call_args[0][0]
|
|
||||||
assert "verb on brass key with args=chest" in call_args
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_multiple_verbs_all_registered(test_zone, player, tmp_path):
|
|
||||||
"""Multiple verbs in TOML all get registered."""
|
|
||||||
toml = tmp_path / "door.toml"
|
|
||||||
toml.write_text(
|
|
||||||
textwrap.dedent("""\
|
|
||||||
name = "door"
|
|
||||||
description = "a wooden door"
|
|
||||||
[verbs]
|
|
||||||
unlock = "tests.test_toml_verbs:_test_verb_handler"
|
|
||||||
open = "tests.test_toml_verbs:_another_test_handler"
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
template = load_thing_template(toml)
|
|
||||||
thing = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
|
|
||||||
# Both verbs should be registered
|
|
||||||
assert thing.has_verb("unlock")
|
|
||||||
assert thing.has_verb("open")
|
|
||||||
|
|
||||||
# Both should work
|
|
||||||
unlock_handler = thing.get_verb("unlock")
|
|
||||||
assert unlock_handler is not None
|
|
||||||
await unlock_handler(player, "test1")
|
|
||||||
player.writer.write.assert_called()
|
|
||||||
|
|
||||||
open_handler = thing.get_verb("open")
|
|
||||||
assert open_handler is not None
|
|
||||||
await open_handler(player, "test2")
|
|
||||||
call_args = player.writer.write.call_args[0][0]
|
|
||||||
assert "another verb on door" in call_args
|
|
||||||
|
|
||||||
|
|
||||||
def test_bad_handler_ref_skipped(test_zone):
|
|
||||||
"""A TOML verb with bad handler ref is skipped, not a crash."""
|
|
||||||
template = ThingTemplate(
|
|
||||||
name="bad",
|
|
||||||
description="oops",
|
|
||||||
portable=True,
|
|
||||||
verbs={"zap": "nonexistent.module:fake_handler"},
|
|
||||||
)
|
|
||||||
thing = spawn_thing(template, test_zone, x=5, y=5)
|
|
||||||
assert not thing.has_verb("zap")
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.commands.use import cmd_use
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_output(mock_writer):
|
|
||||||
"""Extract all text written to the mock writer."""
|
|
||||||
return "".join(call[0][0] for call in mock_writer.write.call_args_list)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_no_args(player, mock_writer):
|
|
||||||
"""use with no args should prompt for what to use"""
|
|
||||||
await cmd_use(player, "")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "Use what?" in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_with_verb(player, mock_writer, test_zone):
|
|
||||||
"""use rock should find rock with use verb and call it"""
|
|
||||||
|
|
||||||
class Rock(Thing):
|
|
||||||
@verb("use")
|
|
||||||
async def use_verb(self, player, args):
|
|
||||||
await player.send("You use the rock.\r\n")
|
|
||||||
|
|
||||||
_rock = Rock(
|
|
||||||
name="rock", description="a smooth rock", portable=True, location=test_zone
|
|
||||||
)
|
|
||||||
_rock.x = 5
|
|
||||||
_rock.y = 5
|
|
||||||
|
|
||||||
await cmd_use(player, "rock")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You use the rock." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_without_verb(player, mock_writer, test_zone):
|
|
||||||
"""use rock when rock has no use verb should give error"""
|
|
||||||
_rock = Thing(
|
|
||||||
name="rock", description="a smooth rock", portable=True, location=test_zone
|
|
||||||
)
|
|
||||||
_rock.x = 5
|
|
||||||
_rock.y = 5
|
|
||||||
|
|
||||||
await cmd_use(player, "rock")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You can't use that." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_not_found(player, mock_writer):
|
|
||||||
"""use flurb when nothing matches should give error"""
|
|
||||||
await cmd_use(player, "flurb")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You don't see that here." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_on_target(player, mock_writer, test_zone):
|
|
||||||
"""use key on chest should pass chest as args to verb"""
|
|
||||||
|
|
||||||
class Key(Thing):
|
|
||||||
@verb("use")
|
|
||||||
async def use_verb(self, player, args):
|
|
||||||
await player.send(f"You use the key on {args}.\r\n")
|
|
||||||
|
|
||||||
_key = Key(name="key", description="a brass key", portable=True, location=test_zone)
|
|
||||||
_key.x = 5
|
|
||||||
_key.y = 5
|
|
||||||
|
|
||||||
await cmd_use(player, "key on chest")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You use the key on chest." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_on_nonexistent_target(player, mock_writer, test_zone):
|
|
||||||
"""use key on flurb should still work, passing flurb to verb handler"""
|
|
||||||
|
|
||||||
class Key(Thing):
|
|
||||||
@verb("use")
|
|
||||||
async def use_verb(self, player, args):
|
|
||||||
await player.send(f"You use the key on {args}.\r\n")
|
|
||||||
|
|
||||||
_key = Key(name="key", description="a brass key", portable=True, location=test_zone)
|
|
||||||
_key.x = 5
|
|
||||||
_key.y = 5
|
|
||||||
|
|
||||||
await cmd_use(player, "key on flurb")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You use the key on flurb." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_object_in_inventory(player, mock_writer):
|
|
||||||
"""use should find objects in inventory"""
|
|
||||||
|
|
||||||
class Potion(Thing):
|
|
||||||
@verb("use")
|
|
||||||
async def use_verb(self, player, args):
|
|
||||||
await player.send("You drink the potion.\r\n")
|
|
||||||
|
|
||||||
_potion = Potion(
|
|
||||||
name="potion", description="a healing potion", portable=True, location=player
|
|
||||||
)
|
|
||||||
|
|
||||||
await cmd_use(player, "potion")
|
|
||||||
output = get_output(mock_writer)
|
|
||||||
assert "You drink the potion." in output
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_use_passes_correct_args(player, mock_writer, test_zone):
|
|
||||||
"""verify verb handler receives correct args string"""
|
|
||||||
received_args = None
|
|
||||||
|
|
||||||
class Tool(Thing):
|
|
||||||
@verb("use")
|
|
||||||
async def use_verb(self, player, args):
|
|
||||||
nonlocal received_args
|
|
||||||
received_args = args
|
|
||||||
await player.send(f"Used with args: '{args}'\r\n")
|
|
||||||
|
|
||||||
_tool = Tool(
|
|
||||||
name="tool", description="a multi-tool", portable=True, location=test_zone
|
|
||||||
)
|
|
||||||
_tool.x = 5
|
|
||||||
_tool.y = 5
|
|
||||||
|
|
||||||
# Test with no target
|
|
||||||
await cmd_use(player, "tool")
|
|
||||||
assert received_args == ""
|
|
||||||
|
|
||||||
# Reset writer
|
|
||||||
mock_writer.write.reset_mock()
|
|
||||||
received_args = None
|
|
||||||
|
|
||||||
# Test with target
|
|
||||||
await cmd_use(player, "tool on something")
|
|
||||||
assert received_args == "something"
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
"""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")
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
"""Tests for the verb system on Object."""
|
|
||||||
|
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from mudlib.object import Object
|
|
||||||
from mudlib.player import Player
|
|
||||||
from mudlib.thing import Thing
|
|
||||||
from mudlib.verbs import find_object, 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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_object_starts_with_empty_verbs():
|
|
||||||
"""Object starts with empty _verbs dict."""
|
|
||||||
obj = Object(name="rock")
|
|
||||||
assert obj._verbs == {}
|
|
||||||
|
|
||||||
|
|
||||||
def test_register_verb():
|
|
||||||
"""register_verb adds a verb to the _verbs dict."""
|
|
||||||
|
|
||||||
async def test_handler(obj, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
obj = Object(name="fountain")
|
|
||||||
obj.register_verb("drink", test_handler)
|
|
||||||
assert "drink" in obj._verbs
|
|
||||||
assert obj._verbs["drink"] is test_handler
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_verb_returns_handler():
|
|
||||||
"""get_verb returns the registered handler."""
|
|
||||||
|
|
||||||
async def test_handler(obj, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
obj = Object(name="fountain")
|
|
||||||
obj.register_verb("drink", test_handler)
|
|
||||||
assert obj.get_verb("drink") is test_handler
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_verb_returns_none_for_unknown():
|
|
||||||
"""get_verb returns None for unknown verb."""
|
|
||||||
obj = Object(name="rock")
|
|
||||||
assert obj.get_verb("nonexistent") is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_has_verb_returns_true_when_present():
|
|
||||||
"""has_verb returns True when verb is registered."""
|
|
||||||
|
|
||||||
async def test_handler(obj, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
obj = Object(name="fountain")
|
|
||||||
obj.register_verb("drink", test_handler)
|
|
||||||
assert obj.has_verb("drink") is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_has_verb_returns_false_when_absent():
|
|
||||||
"""has_verb returns False when verb is not registered."""
|
|
||||||
obj = Object(name="rock")
|
|
||||||
assert obj.has_verb("drink") is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_verb_decorator_marks_method():
|
|
||||||
"""@verb decorator marks method with _verb_name attribute."""
|
|
||||||
|
|
||||||
class TestObject(Object):
|
|
||||||
@verb("test")
|
|
||||||
async def test_verb(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert hasattr(TestObject.test_verb, "_verb_name")
|
|
||||||
assert TestObject.test_verb._verb_name == "test"
|
|
||||||
|
|
||||||
|
|
||||||
def test_verb_decorator_auto_registers_on_instantiation():
|
|
||||||
"""Class with @verb methods auto-registers verbs on instantiation."""
|
|
||||||
|
|
||||||
class Fountain(Thing):
|
|
||||||
@verb("drink")
|
|
||||||
async def drink(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
fountain = Fountain(name="fountain")
|
|
||||||
assert fountain.has_verb("drink")
|
|
||||||
assert fountain.get_verb("drink") is not None
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiple_verbs_on_same_class():
|
|
||||||
"""Multiple verbs on same class all register."""
|
|
||||||
|
|
||||||
class Fountain(Thing):
|
|
||||||
@verb("drink")
|
|
||||||
async def drink(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@verb("splash")
|
|
||||||
async def splash(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
fountain = Fountain(name="fountain")
|
|
||||||
assert fountain.has_verb("drink")
|
|
||||||
assert fountain.has_verb("splash")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_verb_handler_can_be_called(player):
|
|
||||||
"""Verb handler can be called as an async function."""
|
|
||||||
called = False
|
|
||||||
received_args = None
|
|
||||||
|
|
||||||
class Fountain(Thing):
|
|
||||||
@verb("drink")
|
|
||||||
async def drink(self, player, args):
|
|
||||||
nonlocal called, received_args
|
|
||||||
called = True
|
|
||||||
received_args = args
|
|
||||||
|
|
||||||
fountain = Fountain(name="fountain")
|
|
||||||
handler = fountain.get_verb("drink")
|
|
||||||
assert handler is not None
|
|
||||||
# handler is a bound method, so we don't pass self/fountain again
|
|
||||||
await handler(player, "from the left side")
|
|
||||||
|
|
||||||
assert called
|
|
||||||
assert received_args == "from the left side"
|
|
||||||
|
|
||||||
|
|
||||||
def test_verb_registered_on_parent_class_propagates_to_subclass():
|
|
||||||
"""Verb registered on parent class propagates to subclass instances."""
|
|
||||||
|
|
||||||
class BaseFountain(Thing):
|
|
||||||
@verb("drink")
|
|
||||||
async def drink(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class FancyFountain(BaseFountain):
|
|
||||||
@verb("splash")
|
|
||||||
async def splash(self, player, args):
|
|
||||||
pass
|
|
||||||
|
|
||||||
fancy = FancyFountain(name="fancy fountain")
|
|
||||||
assert fancy.has_verb("drink")
|
|
||||||
assert fancy.has_verb("splash")
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_by_name_in_inventory(player):
|
|
||||||
"""find_object finds object by name in inventory."""
|
|
||||||
sword = Thing(name="sword", location=player)
|
|
||||||
found = find_object("sword", player)
|
|
||||||
assert found is sword
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_by_alias_in_inventory(player):
|
|
||||||
"""find_object finds object by alias in inventory."""
|
|
||||||
can = Thing(name="pepsi can", aliases=["can", "pepsi"], location=player)
|
|
||||||
found = find_object("can", player)
|
|
||||||
assert found is can
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_by_name_on_ground(player, test_zone):
|
|
||||||
"""find_object finds object by name on ground."""
|
|
||||||
rock = Thing(name="rock", location=test_zone, x=5, y=5)
|
|
||||||
found = find_object("rock", player)
|
|
||||||
assert found is rock
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_by_alias_on_ground(player, test_zone):
|
|
||||||
"""find_object finds object by alias on ground."""
|
|
||||||
can = Thing(
|
|
||||||
name="pepsi can", aliases=["can", "pepsi"], location=test_zone, x=5, y=5
|
|
||||||
)
|
|
||||||
found = find_object("pepsi", player)
|
|
||||||
assert found is can
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_prefers_inventory_over_ground(player, test_zone):
|
|
||||||
"""find_object prefers inventory over ground when matching object in both."""
|
|
||||||
Thing(name="sword", location=test_zone, x=5, y=5)
|
|
||||||
inv_sword = Thing(name="sword", location=player)
|
|
||||||
found = find_object("sword", player)
|
|
||||||
assert found is inv_sword
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_returns_none_when_nothing_matches(player):
|
|
||||||
"""find_object returns None when nothing matches."""
|
|
||||||
found = find_object("nonexistent", player)
|
|
||||||
assert found is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_searches_all_objects_not_just_things(player, test_zone):
|
|
||||||
"""find_object searches all Objects, not just Things."""
|
|
||||||
|
|
||||||
class CustomObject(Object):
|
|
||||||
def __init__(self, name, aliases=None, **kwargs):
|
|
||||||
super().__init__(name=name, **kwargs)
|
|
||||||
self.aliases = aliases or []
|
|
||||||
|
|
||||||
custom = CustomObject(name="widget", aliases=["gadget"], location=player)
|
|
||||||
found = find_object("widget", player)
|
|
||||||
assert found is custom
|
|
||||||
found_by_alias = find_object("gadget", player)
|
|
||||||
assert found_by_alias is custom
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_object_skips_self(player, test_zone):
|
|
||||||
"""find_object should not return the querying player."""
|
|
||||||
player.move_to(test_zone, x=0, y=0)
|
|
||||||
result = find_object(player.name, player)
|
|
||||||
assert result is None
|
|
||||||
Loading…
Reference in a new issue