From 5b6c808050f92ac145e9b3f8820ffffeeee0fda6 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 14 Feb 2026 17:41:43 -0500 Subject: [PATCH] Add describe command for home zone descriptions Allows players to set custom descriptions for their home zones. Only works in the player's own home zone. Saves to TOML file. --- src/mudlib/commands/describe.py | 50 +++++++++++ src/mudlib/server.py | 3 + tests/test_command_describe.py | 151 ++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 src/mudlib/commands/describe.py create mode 100644 tests/test_command_describe.py diff --git a/src/mudlib/commands/describe.py b/src/mudlib/commands/describe.py new file mode 100644 index 0000000..8409249 --- /dev/null +++ b/src/mudlib/commands/describe.py @@ -0,0 +1,50 @@ +"""Describe command — set home zone description.""" + +from mudlib.commands import CommandDefinition, register +from mudlib.housing import save_home_zone +from mudlib.player import Player +from mudlib.zone import Zone + + +async def cmd_describe(player: Player, args: str) -> None: + """Set the description for your home zone. + + Usage: + describe — set your home zone description + describe — show current description + """ + zone = player.location + + # Must be in a zone + if not isinstance(zone, Zone): + await player.send("You aren't anywhere.\r\n") + return + + # Must be in own home zone + if zone.name != player.home_zone: + await player.send("You can only describe your own home zone.\r\n") + return + + # No args — show current description + if not args.strip(): + await player.send(f"Current description: {zone.description}\r\n") + return + + # Set new description + description = args.strip() + if len(description) > 500: + await player.send("Description too long (max 500 characters).\r\n") + return + zone.description = description + save_home_zone(player.name, zone) + + await player.send(f"Home zone description set to: {zone.description}\r\n") + + +register( + CommandDefinition( + "describe", + cmd_describe, + help="Set your home zone description.", + ) +) diff --git a/src/mudlib/server.py b/src/mudlib/server.py index 1d6c99d..eb9a086 100644 --- a/src/mudlib/server.py +++ b/src/mudlib/server.py @@ -16,6 +16,8 @@ from telnetlib3.server_shell import readline2 import mudlib.combat.commands import mudlib.commands import mudlib.commands.containers +import mudlib.commands.crafting +import mudlib.commands.describe import mudlib.commands.edit import mudlib.commands.examine import mudlib.commands.fly @@ -31,6 +33,7 @@ import mudlib.commands.reload import mudlib.commands.snapneck import mudlib.commands.spawn import mudlib.commands.talk +import mudlib.commands.terrain import mudlib.commands.things import mudlib.commands.use from mudlib.caps import parse_mtts diff --git a/tests/test_command_describe.py b/tests/test_command_describe.py new file mode 100644 index 0000000..9789abf --- /dev/null +++ b/tests/test_command_describe.py @@ -0,0 +1,151 @@ +"""Tests for the describe command.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.commands.describe import cmd_describe +from mudlib.housing import init_housing +from mudlib.player import Player +from mudlib.zone import Zone +from mudlib.zones import register_zone, zone_registry + + +@pytest.fixture(autouse=True) +def _clean_registries(): + saved = dict(zone_registry) + zone_registry.clear() + yield + zone_registry.clear() + zone_registry.update(saved) + + +@pytest.fixture +def _init_housing(tmp_path): + from mudlib.store import create_account, init_db + + init_housing(tmp_path / "player_zones") + init_db(tmp_path / "test.db") + # Create accounts for test players + for name in ["alice", "bob", "charlie"]: + create_account(name, "testpass") + + +def _make_home_zone(player_name="alice"): + name = f"home:{player_name}" + terrain = [] + for y in range(9): + row = [] + for x in range(9): + if x == 0 or x == 8 or y == 0 or y == 8: + row.append("#") + else: + row.append(".") + terrain.append(row) + zone = Zone( + name=name, + description=f"{player_name}'s home", + width=9, + height=9, + terrain=terrain, + toroidal=False, + impassable={"#", "^", "~"}, + spawn_x=4, + spawn_y=4, + safe=True, + ) + register_zone(name, zone) + return zone + + +def _make_player(name="alice", zone=None, x=4, y=4): + mock_writer = MagicMock() + mock_writer.write = MagicMock() + mock_writer.drain = AsyncMock() + p = Player(name=name, location=zone, x=x, y=y, writer=mock_writer) + p.home_zone = f"home:{name}" + return p + + +@pytest.mark.asyncio +async def test_describe_sets_description(_init_housing): + """describe sets zone.description.""" + home = _make_home_zone("alice") + player = _make_player("alice", zone=home) + + await cmd_describe(player, "a cozy cottage by the sea") + + assert home.description == "a cozy cottage by the sea" + + +@pytest.mark.asyncio +async def test_describe_not_in_home_zone(_init_housing): + """describe fails if not in player's own home zone.""" + _make_home_zone("alice") + other_zone = Zone( + name="overworld", + description="The world", + width=20, + height=20, + terrain=[["." for _ in range(20)] for _ in range(20)], + toroidal=True, + ) + register_zone("overworld", other_zone) + player = _make_player("alice", zone=other_zone) + + await cmd_describe(player, "trying to describe the overworld") + + # Should show error message + assert player.writer.write.called + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + assert "only" in output.lower() and "home" in output.lower() + + +@pytest.mark.asyncio +async def test_describe_no_args_shows_current(_init_housing): + """describe with no args shows current description.""" + home = _make_home_zone("alice") + home.description = "a warm and welcoming place" + player = _make_player("alice", zone=home) + + await cmd_describe(player, "") + + assert player.writer.write.called + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + assert "a warm and welcoming place" in output + + +@pytest.mark.asyncio +async def test_describe_saves_zone(_init_housing): + """describe saves the zone to disk.""" + import tomllib + + from mudlib.housing import _zone_path + + home = _make_home_zone("alice") + player = _make_player("alice", zone=home) + + await cmd_describe(player, "a newly described home") + + # Check TOML file was saved + zone_path = _zone_path("alice") + assert zone_path.exists() + + with open(zone_path, "rb") as f: + data = tomllib.load(f) + + assert data["description"] == "a newly described home" + + +@pytest.mark.asyncio +async def test_describe_multiword(_init_housing): + """describe handles multiword descriptions.""" + home = _make_home_zone("bob") + player = _make_player("bob", zone=home) + + await cmd_describe(player, "a cozy cottage with warm lighting") + + assert home.description == "a cozy cottage with warm lighting" + assert player.writer.write.called + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + assert "description" in output.lower() or "set" in output.lower()