diff --git a/src/mudlib/commands/build.py b/src/mudlib/commands/build.py index 6adc300..d31ae8d 100644 --- a/src/mudlib/commands/build.py +++ b/src/mudlib/commands/build.py @@ -8,7 +8,7 @@ from mudlib.player import Player, players from mudlib.store import account_exists, load_player_data, set_admin from mudlib.things import spawn_thing, thing_templates from mudlib.zone import Zone -from mudlib.zones import get_zone, register_zone +from mudlib.zones import get_zone, register_zone, zone_registry # Content directory, set during server startup _content_dir: Path | None = None @@ -180,6 +180,24 @@ async def cmd_demote(player: Player, args: str) -> None: await player.send(f"{target_name} is no longer an admin.\r\n") +async def cmd_zones(player: Player, args: str) -> None: + """List all registered zones.""" + if not zone_registry: + await player.send("No zones registered.\r\n") + return + + await player.send("zones:\r\n") + current_zone_name = ( + player.location.name if isinstance(player.location, Zone) else None + ) + + for name in sorted(zone_registry.keys()): + zone = zone_registry[name] + dimensions = f"{zone.width}x{zone.height}" + marker = " [here]" if name == current_zone_name else "" + await player.send(f" {name:<15} {dimensions:>6}{marker}\r\n") + + register(CommandDefinition("@goto", cmd_goto, admin=True, help="Teleport to a zone")) register(CommandDefinition("@dig", cmd_dig, admin=True, help="Create a new zone")) register(CommandDefinition("@save", cmd_save, admin=True, help="Save current zone")) @@ -190,3 +208,4 @@ register( register( CommandDefinition("@demote", cmd_demote, admin=True, help="Revoke admin status") ) +register(CommandDefinition("@zones", cmd_zones, admin=True, help="List all zones")) diff --git a/tests/test_build_commands.py b/tests/test_build_commands.py index 91b751c..b25bfde 100644 --- a/tests/test_build_commands.py +++ b/tests/test_build_commands.py @@ -325,3 +325,132 @@ async def test_builder_commands_require_admin(zone, mock_writer, mock_reader): mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower() + + +# --- @zones --- + + +@pytest.mark.asyncio +async def test_zones_lists_registered_zones(player): + """@zones lists all registered zones.""" + from mudlib.commands.build import cmd_zones + + # Register additional zones + forest = Zone( + name="forest", + width=15, + height=12, + terrain=[["." for _ in range(15)] for _ in range(12)], + ) + register_zone("forest", forest) + + tavern = Zone( + name="tavern", + width=8, + height=6, + terrain=[["." for _ in range(8)] for _ in range(6)], + ) + register_zone("tavern", tavern) + + await cmd_zones(player, "") + + # Check all zones are listed + all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) + assert "hub" in all_output + assert "forest" in all_output + assert "tavern" in all_output + + +@pytest.mark.asyncio +async def test_zones_shows_dimensions(player): + """@zones shows width x height for each zone.""" + from mudlib.commands.build import cmd_zones + + forest = Zone( + name="forest", + width=20, + height=15, + terrain=[["." for _ in range(20)] for _ in range(15)], + ) + register_zone("forest", forest) + + await cmd_zones(player, "") + + all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) + assert "10x10" in all_output # hub from fixture + assert "20x15" in all_output # forest + + +@pytest.mark.asyncio +async def test_zones_highlights_current_zone(player): + """@zones marks the player's current zone.""" + from mudlib.commands.build import cmd_zones + + forest = Zone( + name="forest", + width=15, + height=12, + terrain=[["." for _ in range(15)] for _ in range(12)], + ) + register_zone("forest", forest) + + await cmd_zones(player, "") + + all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) + # hub should be marked as current (player is in hub via fixture) + assert "[here]" in all_output + # [here] should be on the same line as hub + hub_line_idx = all_output.find("hub") + here_idx = all_output.find("[here]") + assert hub_line_idx < here_idx < hub_line_idx + 50 + + +@pytest.mark.asyncio +async def test_zones_empty_registry(zone, mock_writer, mock_reader): + """@zones with no zones shows appropriate message.""" + from mudlib.commands.build import cmd_zones + + # Create player not in a zone fixture + zone_registry.clear() + temp_zone = Zone( + name="temp", + width=5, + height=5, + terrain=[["." for _ in range(5)] for _ in range(5)], + ) + p = Player( + name="builder", + x=0, + y=0, + writer=mock_writer, + reader=mock_reader, + location=temp_zone, + is_admin=True, + ) + + await cmd_zones(p, "") + + mock_writer.write.assert_called() + written = mock_writer.write.call_args_list[0][0][0] + assert "no zones" in written.lower() + + +@pytest.mark.asyncio +async def test_zones_requires_admin(zone, mock_writer, mock_reader): + """Non-admin players cannot use @zones.""" + from mudlib.commands import dispatch + + non_admin = Player( + name="player", + x=5, + y=5, + writer=mock_writer, + reader=mock_reader, + location=zone, + is_admin=False, + ) + + await dispatch(non_admin, "@zones") + mock_writer.write.assert_called() + written = mock_writer.write.call_args_list[-1][0][0] + assert "permission" in written.lower()