Add home command for personal zone teleportation
This commit is contained in:
parent
9fac18ad2b
commit
6229c87945
2 changed files with 270 additions and 0 deletions
108
src/mudlib/commands/home.py
Normal file
108
src/mudlib/commands/home.py
Normal file
|
|
@ -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.",
|
||||||
|
)
|
||||||
|
)
|
||||||
162
tests/test_command_home.py
Normal file
162
tests/test_command_home.py
Normal file
|
|
@ -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()
|
||||||
Loading…
Reference in a new issue