diff --git a/src/mudlib/commands/home.py b/src/mudlib/commands/home.py new file mode 100644 index 0000000..390e272 --- /dev/null +++ b/src/mudlib/commands/home.py @@ -0,0 +1,108 @@ +"""Home command — teleport to personal zone.""" + +from mudlib.commands import CommandDefinition, register +from mudlib.commands.movement import send_nearby_message +from mudlib.housing import get_or_create_home +from mudlib.player import Player +from mudlib.store import save_player_home_zone +from mudlib.zone import Zone +from mudlib.zones import get_zone + + +async def cmd_home(player: Player, args: str) -> None: + """Teleport to your personal zone, or return from it. + + Usage: + home — go to your home zone + home return — return to where you were before going home + """ + from mudlib.commands.look import cmd_look + + arg = args.strip().lower() + zone = player.location + + if arg == "return": + # Return to previous location + if player.return_location is None: + await player.send("You have nowhere to return to.\r\n") + return + + target_zone_name, target_x, target_y = player.return_location + target_zone = get_zone(target_zone_name) + if target_zone is None: + await player.send("Your return destination no longer exists.\r\n") + player.return_location = None + return + + # Departure message + if isinstance(zone, Zone): + await send_nearby_message( + player, + player.x, + player.y, + f"{player.name} vanishes in a flash.\r\n", + ) + + # Move + player.move_to(target_zone, x=target_x, y=target_y) + player.return_location = None + + # Arrival message + await send_nearby_message( + player, + player.x, + player.y, + f"{player.name} appears in a flash.\r\n", + ) + + await player.send("You return to where you were.\r\n") + await cmd_look(player, "") + return + + if arg: + await player.send("Usage: home | home return\r\n") + return + + # Go home + home = get_or_create_home(player.name) + + # Save current location for return trip (only if not already at home) + home_zone_name = f"home:{player.name.lower()}" + if isinstance(zone, Zone) and zone.name != home_zone_name: + player.return_location = (zone.name, player.x, player.y) + + # Departure message + if isinstance(zone, Zone): + await send_nearby_message( + player, + player.x, + player.y, + f"{player.name} vanishes in a flash.\r\n", + ) + + # Move to home spawn point + player.move_to(home, x=home.spawn_x, y=home.spawn_y) + + # Update home_zone on player + player.home_zone = home.name + save_player_home_zone(player.name, home.name) + + # Arrival message (usually nobody else is in your home, but just in case) + await send_nearby_message( + player, + player.x, + player.y, + f"{player.name} appears in a flash.\r\n", + ) + + await player.send("You arrive at your home.\r\n") + await cmd_look(player, "") + + +register( + CommandDefinition( + "home", + cmd_home, + help="Teleport to your personal zone. 'home return' to go back.", + ) +) diff --git a/tests/test_command_home.py b/tests/test_command_home.py new file mode 100644 index 0000000..221c84a --- /dev/null +++ b/tests/test_command_home.py @@ -0,0 +1,162 @@ +"""Tests for the home command.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.commands.home import cmd_home +from mudlib.housing import init_housing +from mudlib.player import Player +from mudlib.zone import Zone +from mudlib.zones import get_zone, register_zone, zone_registry + + +@pytest.fixture(autouse=True) +def _clean_zone_registry(): + saved = dict(zone_registry) + 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", "diane", "eve", "frank", "grace"]: + create_account(name, "testpass") + + +def _make_zone(name="overworld", width=20, height=20): + 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): + 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_home_creates_zone(_init_housing): + """Player with no home calls home and gets teleported to new zone.""" + overworld = _make_zone("overworld") + player = _make_player("alice", zone=overworld) + + # Initially no home zone exists + assert get_zone("home:alice") is None + + await cmd_home(player, "") + + # Now home zone exists and player is in it + home = get_zone("home:alice") + assert home is not None + assert player.location is home + assert player.home_zone == "home:alice" + assert player.return_location == ("overworld", 5, 5) + + +@pytest.mark.asyncio +async def test_home_return(_init_housing): + """Player goes home then home return, ends up back where they were.""" + overworld = _make_zone("overworld") + player = _make_player("bob", zone=overworld, x=10, y=15) + + # Go home + await cmd_home(player, "") + home = get_zone("home:bob") + assert player.location is home + + # Return + await cmd_home(player, "return") + assert player.location is overworld + assert player.x == 10 + assert player.y == 15 + assert player.return_location is None + + +@pytest.mark.asyncio +async def test_home_return_without_location(_init_housing): + """home return with no saved location shows error.""" + overworld = _make_zone("overworld") + player = _make_player("charlie", zone=overworld) + + # No return location set + assert player.return_location is None + + await cmd_home(player, "return") + + # Should get error message + assert player.writer.write.called + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + assert "nowhere" in output.lower() + + +@pytest.mark.asyncio +async def test_home_already_at_home(_init_housing): + """Calling home while already at home doesn't overwrite return_location.""" + overworld = _make_zone("overworld") + player = _make_player("diane", zone=overworld, x=7, y=8) + + # Go home first time + await cmd_home(player, "") + assert player.return_location == ("overworld", 7, 8) + + # Call home again while at home + await cmd_home(player, "") + + # return_location should still point to overworld, not home + assert player.return_location == ("overworld", 7, 8) + + +@pytest.mark.asyncio +async def test_home_invalid_args(_init_housing): + """home foo shows usage.""" + overworld = _make_zone("overworld") + player = _make_player("eve", zone=overworld) + + await cmd_home(player, "foo") + + assert player.writer.write.called + output = "".join(c[0][0] for c in player.writer.write.call_args_list) + assert "usage" in output.lower() + + +@pytest.mark.asyncio +async def test_home_departure_arrival_messages(_init_housing): + """Check nearby messages are sent.""" + from mudlib.player import players + + players.clear() + + overworld = _make_zone("overworld") + player = _make_player("frank", zone=overworld, x=10, y=10) + other = _make_player("grace", zone=overworld, x=10, y=10) + + players["frank"] = player + players["grace"] = other + + # Go home + await cmd_home(player, "") + + # Other player should have seen departure message + assert other.writer.write.called + output = "".join(c[0][0] for c in other.writer.write.call_args_list) + assert "frank" in output.lower() + assert "vanishes" in output.lower() + + players.clear()