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.
418 lines
12 KiB
Python
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
|