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