mud/tests/test_boundaries.py
Jared Miller b5c5542792
Add boundary region data model with TOML parsing and export
Boundaries are rectangular regions within zones that can trigger effects
when players enter or exit. Added BoundaryRegion dataclass with contains()
method, TOML parsing in load_zone(), and export support. Tests verify
parsing, export, and round-trip behavior.
2026-02-14 12:39:48 -05:00

418 lines
12 KiB
Python

"""Tests for zone boundary regions."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.export import export_zone
from mudlib.player import Player
from mudlib.thing import Thing
from mudlib.zone import BoundaryRegion, Zone
from mudlib.zones import load_zone
def create_mock_writer():
"""Create a mock writer for testing."""
writer = MagicMock()
writer.write = MagicMock()
writer.drain = AsyncMock()
return writer
def test_boundary_contains():
"""Test BoundaryRegion.contains() method."""
boundary = BoundaryRegion(
name="test_area",
x_min=10,
x_max=15,
y_min=8,
y_max=12,
)
# Inside the boundary
assert boundary.contains(10, 8)
assert boundary.contains(15, 12)
assert boundary.contains(12, 10)
# Outside the boundary
assert not boundary.contains(9, 10)
assert not boundary.contains(16, 10)
assert not boundary.contains(12, 7)
assert not boundary.contains(12, 13)
def test_boundary_parsing_from_toml(tmp_path):
"""Test loading boundaries from TOML."""
toml_content = """
name = "test_zone"
width = 20
height = 20
[terrain]
rows = [
".....................",
".....................",
".....................",
]
[terrain.impassable]
tiles = ["^"]
[[boundaries]]
name = "vault"
x = [10, 15]
y = [8, 12]
on_enter_message = "You enter the vault."
on_exit_message = "You leave the vault."
on_exit_check = "carrying:treasure"
on_exit_fail = "Guards block your path!"
[[boundaries]]
name = "simple"
x = [0, 5]
y = [0, 5]
"""
zone_file = tmp_path / "test.toml"
zone_file.write_text(toml_content)
zone = load_zone(zone_file)
assert len(zone.boundaries) == 2
vault = zone.boundaries[0]
assert vault.name == "vault"
assert vault.x_min == 10
assert vault.x_max == 15
assert vault.y_min == 8
assert vault.y_max == 12
assert vault.on_enter_message == "You enter the vault."
assert vault.on_exit_message == "You leave the vault."
assert vault.on_exit_check == "carrying:treasure"
assert vault.on_exit_fail == "Guards block your path!"
simple = zone.boundaries[1]
assert simple.name == "simple"
assert simple.x_min == 0
assert simple.x_max == 5
assert simple.y_min == 0
assert simple.y_max == 5
assert simple.on_enter_message is None
assert simple.on_exit_message is None
assert simple.on_exit_check is None
assert simple.on_exit_fail is None
def test_boundary_export_to_toml():
"""Test exporting boundaries to TOML."""
zone = Zone(
name="test_zone",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=8,
y_max=12,
on_enter_message="You enter the vault.",
on_exit_message="You leave the vault.",
on_exit_check="carrying:treasure",
on_exit_fail="Guards block your path!",
),
],
)
toml_str = export_zone(zone)
assert "[[boundaries]]" in toml_str
assert 'name = "vault"' in toml_str
assert "x = [10, 15]" in toml_str
assert "y = [8, 12]" in toml_str
assert 'on_enter_message = "You enter the vault."' in toml_str
assert 'on_exit_message = "You leave the vault."' in toml_str
assert 'on_exit_check = "carrying:treasure"' in toml_str
assert 'on_exit_fail = "Guards block your path!"' in toml_str
def test_boundary_roundtrip(tmp_path):
"""Test TOML -> load -> export -> load round-trip."""
original = Zone(
name="test_zone",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="area1",
x_min=5,
x_max=10,
y_min=5,
y_max=10,
on_enter_message="Enter area1",
),
],
)
# Export to file
zone_file = tmp_path / "test.toml"
toml_str = export_zone(original)
zone_file.write_text(toml_str)
# Load it back
loaded = load_zone(zone_file)
# Verify boundaries survived the round-trip
assert len(loaded.boundaries) == 1
assert loaded.boundaries[0].name == "area1"
assert loaded.boundaries[0].x_min == 5
assert loaded.boundaries[0].x_max == 10
assert loaded.boundaries[0].on_enter_message == "Enter area1"
@pytest.mark.asyncio
async def test_boundary_enter_message():
"""Test on_enter_message sent when entering boundary."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=10,
y_max=15,
on_enter_message="You step into the vault.",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=9, y=10, writer=writer)
# Move east into the boundary
await move_player(player, 1, 0, "east")
# Check that the message was written
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "You step into the vault." in written_text
@pytest.mark.asyncio
async def test_boundary_exit_message():
"""Test on_exit_message sent when leaving boundary."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=10,
y_max=15,
on_exit_message="You leave the vault.",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=15, y=10, writer=writer)
# Move east out of the boundary
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "You leave the vault." in written_text
@pytest.mark.asyncio
async def test_carrying_check_blocks_exit():
"""Test carrying check blocks movement when holding matching item."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=10,
y_max=15,
on_exit_check="carrying:treasure",
on_exit_fail="Guards block your path!",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=15, y=10, writer=writer)
# Give player an item named "treasure"
Thing(name="treasure", location=player)
# Try to move east out of boundary
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "Guards block your path!" in written_text
# Player should still be at original position
assert player.x == 15
assert player.y == 10
@pytest.mark.asyncio
async def test_carrying_check_allows_exit_without_item():
"""Test carrying check allows movement without matching item."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=10,
y_max=15,
on_exit_check="carrying:treasure",
on_exit_fail="Guards block your path!",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=15, y=10, writer=writer)
# Move east out of boundary (no treasure in inventory)
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "Guards block your path!" not in written_text
# Player should have moved
assert player.x == 16
assert player.y == 10
@pytest.mark.asyncio
async def test_carrying_check_matches_by_tag():
"""Test carrying check matches items by tag."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[
BoundaryRegion(
name="vault",
x_min=10,
x_max=15,
y_min=10,
y_max=15,
on_exit_check="carrying:valuable",
on_exit_fail="Guards block your path!",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=15, y=10, writer=writer)
# Give player an item with "valuable" tag
Thing(name="gem", location=player, tags=["valuable", "shiny"])
# Try to move east out of boundary
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "Guards block your path!" in written_text
assert player.x == 15
@pytest.mark.asyncio
async def test_no_boundaries_no_effect():
"""Test that zones with no boundaries work normally."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=20,
height=20,
terrain=[["." for _ in range(20)] for _ in range(20)],
boundaries=[], # No boundaries
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=10, y=10, writer=writer)
# Movement should work normally
await move_player(player, 1, 0, "east")
assert player.x == 11
@pytest.mark.asyncio
async def test_multiple_boundaries_in_zone():
"""Test multiple boundaries in the same zone."""
from mudlib.commands.movement import move_player
zone = Zone(
name="test",
width=30,
height=30,
terrain=[["." for _ in range(30)] for _ in range(30)],
boundaries=[
BoundaryRegion(
name="area1",
x_min=5,
x_max=10,
y_min=5,
y_max=10,
on_enter_message="Enter area1",
),
BoundaryRegion(
name="area2",
x_min=15,
x_max=20,
y_min=15,
y_max=20,
on_enter_message="Enter area2",
),
],
)
writer = create_mock_writer()
player = Player(name="TestPlayer", location=zone, x=4, y=5, writer=writer)
# Move into area1
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "Enter area1" in written_text
# Move to neutral zone
writer.write.reset_mock()
player.x, player.y = 14, 15
# Move into area2
await move_player(player, 1, 0, "east")
written_text = "".join(call[0][0] for call in writer.write.call_args_list)
assert "Enter area2" in written_text