"""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