Add auto-trigger portal on movement

This commit is contained in:
Jared Miller 2026-02-11 22:06:40 -05:00
parent b3801f780f
commit d6920834c8
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
2 changed files with 205 additions and 0 deletions

View file

@ -3,7 +3,9 @@
from mudlib.commands import CommandDefinition, register from mudlib.commands import CommandDefinition, register
from mudlib.entity import Entity from mudlib.entity import Entity
from mudlib.player import Player from mudlib.player import Player
from mudlib.portal import Portal
from mudlib.zone import Zone from mudlib.zone import Zone
from mudlib.zones import get_zone
# Direction mappings: command -> (dx, dy) # Direction mappings: command -> (dx, dy)
DIRECTIONS: dict[str, tuple[int, int]] = { DIRECTIONS: dict[str, tuple[int, int]] = {
@ -81,6 +83,34 @@ async def move_player(player: Player, dx: int, dy: int, direction_name: str) ->
player.x = target_x player.x = target_x
player.y = target_y player.y = target_y
# Check for auto-trigger portals at new position
portals_here = [
obj for obj in zone.contents_at(target_x, target_y) if isinstance(obj, Portal)
]
if portals_here:
portal = portals_here[0] # Take first portal
target_zone = get_zone(portal.target_zone)
if target_zone:
await player.send(f"You enter {portal.name}.\r\n")
await send_nearby_message(
player, player.x, player.y, f"{player.name} enters {portal.name}.\r\n"
)
player.move_to(target_zone, x=portal.target_x, y=portal.target_y)
await send_nearby_message(
player, player.x, player.y, f"{player.name} arrives.\r\n"
)
from mudlib.commands.look import cmd_look
await cmd_look(player, "")
return # Don't do normal arrival+look
else:
await player.send("The portal doesn't lead anywhere.\r\n")
# Stay at portal tile but show normal look
from mudlib.commands.look import cmd_look
await cmd_look(player, "")
return
# Send arrival message to players in the new area # Send arrival message to players in the new area
await send_nearby_message( await send_nearby_message(
player, player.x, player.y, f"{player.name} arrives from the {opposite}.\r\n" player, player.x, player.y, f"{player.name} arrives from the {opposite}.\r\n"

View file

@ -0,0 +1,175 @@
"""Tests for auto-triggering portals on movement."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.player import Player
from mudlib.portal import Portal
from mudlib.zone import Zone
from mudlib.zones import register_zone, zone_registry
@pytest.fixture
def mock_writer():
writer = MagicMock()
writer.write = MagicMock()
writer.drain = AsyncMock()
return writer
@pytest.fixture
def mock_reader():
return MagicMock()
@pytest.fixture
def test_zone():
terrain = [["." for _ in range(10)] for _ in range(10)]
return Zone(
name="testzone",
width=10,
height=10,
toroidal=True,
terrain=terrain,
)
@pytest.fixture
def target_zone():
terrain = [["." for _ in range(10)] for _ in range(10)]
return Zone(
name="targetzone",
width=10,
height=10,
toroidal=True,
terrain=terrain,
)
@pytest.fixture
def player(mock_reader, mock_writer, test_zone):
p = Player(
name="TestPlayer",
x=5,
y=5,
reader=mock_reader,
writer=mock_writer,
location=test_zone,
)
return p
@pytest.fixture(autouse=True)
def clear_zones():
"""Clear zone registry before and after each test."""
zone_registry.clear()
yield
zone_registry.clear()
@pytest.mark.asyncio
async def test_move_onto_portal_triggers_zone_transition(
player, test_zone, target_zone
):
"""Walking onto a portal tile auto-triggers zone transition."""
from mudlib.commands.movement import move_player
register_zone("targetzone", target_zone)
# Place portal at (6, 5), one tile east of player
Portal(
name="shimmering doorway",
location=test_zone,
x=6,
y=5,
target_zone="targetzone",
target_x=3,
target_y=7,
)
# Move east onto the portal
await move_player(player, 1, 0, "east")
# Player should end up in target zone at portal's target coords
assert player.location is target_zone
assert player.x == 3
assert player.y == 7
assert player in target_zone.contents
assert player not in test_zone.contents
@pytest.mark.asyncio
async def test_move_onto_portal_sends_transition_message(
player, test_zone, target_zone, mock_writer
):
"""Auto-triggered portal shows transition message to player."""
from mudlib.commands.movement import move_player
register_zone("targetzone", target_zone)
Portal(
name="mystic portal",
location=test_zone,
x=6,
y=5,
target_zone="targetzone",
target_x=2,
target_y=3,
)
await move_player(player, 1, 0, "east")
# Check that player got a transition message
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
assert "enter" in output.lower() or "portal" in output.lower()
@pytest.mark.asyncio
async def test_move_onto_tile_without_portal_normal_movement(
player, test_zone, mock_writer
):
"""Normal movement still works when no portal present."""
from mudlib.commands.movement import move_player
initial_x = player.x
initial_y = player.y
# Move east to empty tile
await move_player(player, 1, 0, "east")
# Player should still be in test zone with updated position
assert player.location is test_zone
assert player.x == initial_x + 1
assert player.y == initial_y
assert player in test_zone.contents
# Should see look output (viewport with @ symbol)
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
assert "@" in output # Player's position marker in viewport
@pytest.mark.asyncio
async def test_portal_autotrigger_target_zone_not_found(player, test_zone, mock_writer):
"""If target zone not registered, player stays and gets error."""
from mudlib.commands.movement import move_player
# Create portal with invalid target zone (not registered)
Portal(
name="broken portal",
location=test_zone,
x=6,
y=5,
target_zone="nonexistent",
target_x=3,
target_y=7,
)
await move_player(player, 1, 0, "east")
# Player should stay in original zone at portal tile
assert player.location is test_zone
assert player.x == 6
assert player.y == 5
# Should see error message
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
assert "doesn't lead anywhere" in output.lower() or "nowhere" in output.lower()