Add furnish and unfurnish commands

This commit is contained in:
Jared Miller 2026-02-14 17:42:52 -05:00
parent 5b6c808050
commit 7342a70ba2
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
3 changed files with 356 additions and 0 deletions

View file

@ -0,0 +1,92 @@
"""Furnish and unfurnish commands — place/remove furniture in home zones."""
from mudlib.commands import CommandDefinition, register
from mudlib.housing import save_home_zone
from mudlib.player import Player
from mudlib.targeting import find_in_inventory, find_thing_on_tile
from mudlib.zone import Zone
async def cmd_furnish(player: Player, args: str) -> None:
"""Place an item from inventory as furniture in your home zone.
Usage:
furnish <item>
"""
# Validate arguments
if not args.strip():
await player.send("Usage: furnish <item>\r\n")
return
# Check that player is in their home zone
zone = player.location
if not isinstance(zone, Zone) or zone.name != player.home_zone:
await player.send("You can only furnish items in your home zone.\r\n")
return
# Find item in inventory
item_name = args.strip()
thing = find_in_inventory(item_name, player)
if thing is None:
await player.send(f"You don't have '{item_name}'.\r\n")
return
# Place item at player's position
thing.move_to(zone, x=player.x, y=player.y)
# Save the zone
save_home_zone(player.name, zone)
await player.send(f"You place the {thing.name} here.\r\n")
async def cmd_unfurnish(player: Player, args: str) -> None:
"""Pick up furniture from your home zone into inventory.
Usage:
unfurnish <item>
"""
# Validate arguments
if not args.strip():
await player.send("Usage: unfurnish <item>\r\n")
return
# Check that player is in their home zone
zone = player.location
if not isinstance(zone, Zone) or zone.name != player.home_zone:
await player.send("You can only unfurnish items in your home zone.\r\n")
return
# Find furniture at player's position
item_name = args.strip()
thing = find_thing_on_tile(item_name, zone, player.x, player.y)
if thing is None:
await player.send(f"You don't see '{item_name}' here.\r\n")
return
# Pick up the item
thing.move_to(player)
# Save the zone
save_home_zone(player.name, zone)
await player.send(f"You pick up the {thing.name}.\r\n")
register(
CommandDefinition(
"furnish",
cmd_furnish,
help="Place an item from inventory as furniture in your home zone.",
)
)
register(
CommandDefinition(
"unfurnish",
cmd_unfurnish,
help="Pick up furniture from your home zone into inventory.",
)
)

View file

@ -21,6 +21,7 @@ import mudlib.commands.describe
import mudlib.commands.edit
import mudlib.commands.examine
import mudlib.commands.fly
import mudlib.commands.furnish
import mudlib.commands.help
import mudlib.commands.home
import mudlib.commands.look

263
tests/test_furnish.py Normal file
View file

@ -0,0 +1,263 @@
"""Tests for furnish and unfurnish commands."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.housing import init_housing
from mudlib.player import Player
from mudlib.thing import Thing
from mudlib.zone import Zone
from mudlib.zones import register_zone, zone_registry
@pytest.fixture(autouse=True)
def _clean_registries():
"""Clear zone registry between tests."""
saved = dict(zone_registry)
zone_registry.clear()
yield
zone_registry.clear()
zone_registry.update(saved)
def _make_zone(name="overworld", width=20, height=20):
"""Create a zone for testing."""
terrain = [["." for _ in range(width)] for _ in range(height)]
zone = Zone(
name=name,
description=name,
width=width,
height=height,
terrain=terrain,
toroidal=True,
)
register_zone(name, zone)
return zone
def _make_player(name="tester", zone=None, x=5, y=5):
"""Create a player with mock writer."""
mock_writer = MagicMock()
mock_writer.write = MagicMock()
mock_writer.drain = AsyncMock()
return Player(name=name, location=zone, x=x, y=y, writer=mock_writer)
@pytest.mark.asyncio
async def test_furnish_places_item(tmp_path):
"""furnish moves an item from inventory to the zone at player position."""
from mudlib.commands.furnish import cmd_furnish
init_housing(tmp_path)
# Create home zone and player
home_zone = _make_zone("home:alice", width=9, height=9)
player = _make_player("alice", zone=home_zone, x=4, y=4)
player.home_zone = "home:alice"
# Give player a chair
chair = Thing(name="chair", description="A wooden chair")
chair.move_to(player)
# Furnish the chair
await cmd_furnish(player, "chair")
# Chair should be in the zone at player position
assert chair.location is home_zone
assert chair.x == 4
assert chair.y == 4
assert chair not in player.contents
# Player should get feedback
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "chair" in output.lower()
@pytest.mark.asyncio
async def test_furnish_not_in_home_zone():
"""furnish fails if player is not in their home zone."""
from mudlib.commands.furnish import cmd_furnish
overworld = _make_zone("overworld")
player = _make_player("alice", zone=overworld)
player.home_zone = "home:alice"
chair = Thing(name="chair")
chair.move_to(player)
await cmd_furnish(player, "chair")
# Chair should still be in inventory
assert chair.location is player
assert chair in player.contents
# Player should get error message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "home zone" in output.lower()
@pytest.mark.asyncio
async def test_furnish_item_not_in_inventory():
"""furnish fails if item is not in player inventory."""
from mudlib.commands.furnish import cmd_furnish
home_zone = _make_zone("home:alice", width=9, height=9)
player = _make_player("alice", zone=home_zone)
player.home_zone = "home:alice"
await cmd_furnish(player, "chair")
# Player should get error message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "don't have" in output.lower() or "not carrying" in output.lower()
@pytest.mark.asyncio
async def test_furnish_no_args():
"""furnish fails with usage message if no args provided."""
from mudlib.commands.furnish import cmd_furnish
home_zone = _make_zone("home:alice", width=9, height=9)
player = _make_player("alice", zone=home_zone)
player.home_zone = "home:alice"
await cmd_furnish(player, "")
# Player should get usage message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "usage" in output.lower() or "furnish" in output.lower()
@pytest.mark.asyncio
async def test_unfurnish_picks_up_item(tmp_path):
"""unfurnish moves furniture from zone to player inventory."""
from mudlib.commands.furnish import cmd_unfurnish
init_housing(tmp_path)
# Create home zone with furniture
home_zone = _make_zone("home:bob", width=9, height=9)
player = _make_player("bob", zone=home_zone, x=4, y=4)
player.home_zone = "home:bob"
# Place a table at player position
table = Thing(name="table", description="A wooden table")
table.move_to(home_zone, x=4, y=4)
# Unfurnish the table
await cmd_unfurnish(player, "table")
# Table should be in inventory
assert table.location is player
assert table in player.contents
assert table not in home_zone._contents
# Player should get feedback
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "table" in output.lower()
@pytest.mark.asyncio
async def test_unfurnish_not_in_home_zone():
"""unfurnish fails if player is not in their home zone."""
from mudlib.commands.furnish import cmd_unfurnish
overworld = _make_zone("overworld")
player = _make_player("bob", zone=overworld)
player.home_zone = "home:bob"
# Place a table
table = Thing(name="table")
table.move_to(overworld, x=5, y=5)
await cmd_unfurnish(player, "table")
# Table should still be on ground
assert table.location is overworld
assert table not in player.contents
# Player should get error message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "home zone" in output.lower()
@pytest.mark.asyncio
async def test_unfurnish_nothing_at_position():
"""unfurnish fails if no matching furniture at player position."""
from mudlib.commands.furnish import cmd_unfurnish
home_zone = _make_zone("home:bob", width=9, height=9)
player = _make_player("bob", zone=home_zone, x=4, y=4)
player.home_zone = "home:bob"
# No furniture at position
await cmd_unfurnish(player, "table")
# Player should get error message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert (
"no" in output.lower()
or "don't see" in output.lower()
or "can't find" in output.lower()
)
@pytest.mark.asyncio
async def test_unfurnish_no_args():
"""unfurnish fails with usage message if no args provided."""
from mudlib.commands.furnish import cmd_unfurnish
home_zone = _make_zone("home:bob", width=9, height=9)
player = _make_player("bob", zone=home_zone)
player.home_zone = "home:bob"
await cmd_unfurnish(player, "")
# Player should get usage message
player.writer.write.assert_called()
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
assert "usage" in output.lower() or "unfurnish" in output.lower()
@pytest.mark.asyncio
async def test_furnish_saves_zone(tmp_path):
"""furnish persists furniture to the zone TOML file."""
import tomllib
from mudlib.commands.furnish import cmd_furnish
init_housing(tmp_path)
# Create home zone and player
home_zone = _make_zone("home:charlie", width=9, height=9)
player = _make_player("charlie", zone=home_zone, x=4, y=4)
player.home_zone = "home:charlie"
# Give player a lamp
lamp = Thing(name="lamp", description="A brass lamp")
lamp.move_to(player)
# Furnish it
await cmd_furnish(player, "lamp")
# Check TOML file
zone_file = tmp_path / "charlie.toml"
assert zone_file.exists()
with open(zone_file, "rb") as f:
data = tomllib.load(f)
# Should have furniture entry
furniture = data.get("furniture", [])
assert len(furniture) == 1
assert furniture[0]["template"] == "lamp"
assert furniture[0]["x"] == 4
assert furniture[0]["y"] == 4