mud/tests/test_terrain_edit.py

209 lines
5.9 KiB
Python

"""Tests for terrain editing command."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from mudlib.commands.terrain import cmd_terrain
from mudlib.housing import init_housing
from mudlib.player import Player
from mudlib.zone import Zone
from mudlib.zones import register_zone, zone_registry
@pytest.fixture(autouse=True)
def _clean_registries():
"""Clear zone registry between tests."""
saved = dict(zone_registry)
zone_registry.clear()
yield
zone_registry.clear()
zone_registry.update(saved)
def _make_home_zone(player_name="alice"):
"""Create a home zone matching housing.py format."""
name = f"home:{player_name}"
terrain = []
for y in range(9):
row = []
for x in range(9):
if x == 0 or x == 8 or y == 0 or y == 8:
row.append("#")
else:
row.append(".")
terrain.append(row)
zone = Zone(
name=name,
description=f"{player_name}'s home",
width=9,
height=9,
terrain=terrain,
toroidal=False,
impassable={"#", "^", "~"},
spawn_x=4,
spawn_y=4,
safe=True,
)
register_zone(name, zone)
return zone
def _make_player(name="alice", zone=None, x=4, y=4):
"""Create a test player with mock writer."""
mock_writer = MagicMock()
mock_writer.write = MagicMock()
mock_writer.drain = AsyncMock()
p = Player(name=name, location=zone, x=x, y=y, writer=mock_writer)
p.home_zone = f"home:{name}"
return p
@pytest.mark.asyncio
async def test_terrain_paint_tile(tmp_path):
"""terrain <tile> changes terrain at current position."""
init_housing(tmp_path)
zone = _make_home_zone("alice")
player = _make_player("alice", zone=zone, x=4, y=4)
# Verify starting terrain
assert zone.terrain[4][4] == "."
# Paint a new tile
await cmd_terrain(player, "~")
# Verify terrain changed
assert zone.terrain[4][4] == "~"
# Verify success message
player.writer.write.assert_called()
messages = "".join(call[0][0] for call in player.writer.write.call_args_list)
assert "paint" in messages.lower()
@pytest.mark.asyncio
async def test_terrain_not_in_home_zone():
"""terrain command fails when not in home zone."""
# Create home zone but place player in different zone
_make_home_zone("alice")
overworld = Zone(
name="overworld",
description="The overworld",
width=50,
height=50,
terrain=[["."] * 50 for _ in range(50)],
toroidal=True,
impassable=set(),
spawn_x=25,
spawn_y=25,
safe=False,
)
register_zone("overworld", overworld)
player = _make_player("alice", zone=overworld, x=25, y=25)
await cmd_terrain(player, "~")
# Verify error message
player.writer.write.assert_called()
messages = "".join(call[0][0] for call in player.writer.write.call_args_list)
assert "home" in messages.lower() or "can't" in messages.lower()
@pytest.mark.asyncio
async def test_terrain_cannot_edit_border():
"""terrain command prevents editing border tiles."""
zone = _make_home_zone("alice")
# Test all border positions
border_positions = [
(0, 0), # top-left corner
(4, 0), # top edge
(8, 0), # top-right corner
(0, 4), # left edge
(8, 4), # right edge
(0, 8), # bottom-left corner
(4, 8), # bottom edge
(8, 8), # bottom-right corner
]
for x, y in border_positions:
player = _make_player("alice", zone=zone, x=x, y=y)
await cmd_terrain(player, ".")
# Verify error message
player.writer.write.assert_called()
messages = "".join(call[0][0] for call in player.writer.write.call_args_list)
assert "border" in messages.lower() or "wall" in messages.lower()
# Verify terrain unchanged
assert zone.terrain[y][x] == "#"
@pytest.mark.asyncio
async def test_terrain_no_args():
"""terrain with no args shows usage."""
zone = _make_home_zone("alice")
player = _make_player("alice", zone=zone, x=4, y=4)
await cmd_terrain(player, "")
# Verify usage message
player.writer.write.assert_called()
messages = "".join(call[0][0] for call in player.writer.write.call_args_list)
assert "usage" in messages.lower() or "terrain <" in messages.lower()
@pytest.mark.asyncio
async def test_terrain_only_single_char():
"""terrain rejects multi-character arguments."""
zone = _make_home_zone("alice")
player = _make_player("alice", zone=zone, x=4, y=4)
await cmd_terrain(player, "~~")
# Verify error message
player.writer.write.assert_called()
messages = "".join(call[0][0] for call in player.writer.write.call_args_list)
assert "single" in messages.lower() or "one character" in messages.lower()
# Verify terrain unchanged
assert zone.terrain[4][4] == "."
@pytest.mark.asyncio
async def test_terrain_saves_zone(tmp_path):
"""terrain command saves zone to TOML after edit."""
import tomllib
init_housing(tmp_path)
zone = _make_home_zone("alice")
player = _make_player("alice", zone=zone, x=4, y=4)
# Paint a tile
await cmd_terrain(player, "~")
# Verify TOML file was updated
zone_file = tmp_path / "alice.toml"
assert zone_file.exists()
with open(zone_file, "rb") as f:
data = tomllib.load(f)
# Check that row 4 contains the water tile at position 4
rows = data["terrain"]["rows"]
assert rows[4][4] == "~"
@pytest.mark.asyncio
async def test_terrain_various_tiles(tmp_path):
"""terrain accepts various tile characters."""
init_housing(tmp_path)
zone = _make_home_zone("alice")
test_tiles = [".", "~", "^", "T", ",", '"', "*", "+", "="]
for tile in test_tiles:
player = _make_player("alice", zone=zone, x=4, y=4)
await cmd_terrain(player, tile)
assert zone.terrain[4][4] == tile