"""Tests for the look command with structured room display.""" import time from unittest.mock import AsyncMock, MagicMock import pytest from mudlib.commands import look # noqa: F401 from mudlib.commands.look import cmd_look from mudlib.entity import Mob from mudlib.gametime import init_game_time from mudlib.player import Player from mudlib.portal import Portal from mudlib.thing import Thing from mudlib.weather import WeatherCondition, init_weather from mudlib.zone import Zone @pytest.fixture(autouse=True) def _reset_globals(): yield import mudlib.gametime import mudlib.weather mudlib.gametime._game_time = None mudlib.weather._current_weather = None @pytest.fixture def test_zone(): """Create a test zone with simple terrain.""" # 50x50 zone with grass everywhere terrain = [["." for _ in range(50)] for _ in range(50)] # Add some mountains terrain[10][10] = "^" terrain[10][11] = "^" return Zone( name="test_zone", description="The Test Zone", width=50, height=50, terrain=terrain, impassable={"^", "~"}, ) @pytest.fixture def player(mock_reader, mock_writer, test_zone): """Create a test player in the test zone.""" p = Player( name="TestPlayer", x=25, y=25, reader=mock_reader, writer=mock_writer, ) p.location = test_zone test_zone._contents.append(p) return p def get_output(player): """Get all output written to player's writer.""" return "".join([call[0][0] for call in player.writer.write.call_args_list]) @pytest.mark.asyncio async def test_look_includes_where_header(player): """Look output should include 'Where: {zone description}' line.""" await cmd_look(player, "") output = get_output(player) assert "Where: The Test Zone" in output @pytest.mark.asyncio async def test_look_includes_location_line(player): """Look output should include 'Location:' line with quadrant and coords.""" await cmd_look(player, "") output = get_output(player) assert "Location: center 25, 25" in output @pytest.mark.asyncio async def test_look_includes_exits_line(player): """Look output should include 'Exits:' line.""" await cmd_look(player, "") output = get_output(player) assert "Exits: north south east west up" in output @pytest.mark.asyncio async def test_look_shows_nearby_entities(player, test_zone): """Look should show nearby entities (not on player's tile) in viewport.""" # Add some mobs in viewport range (location param auto-adds to zone) Mob(name="Goku", x=26, y=25, location=test_zone) Mob(name="Vegeta", x=27, y=26, location=test_zone) await cmd_look(player, "") output = get_output(player) # Should see nearby line with count assert "Nearby: (2)" in output assert "Goku" in output assert "Vegeta" in output @pytest.mark.asyncio async def test_look_shows_entities_here(player, test_zone): """Look should show entities on player's tile with posture.""" # Add entities on player's tile (location param auto-adds to zone) Mob(name="Krillin", x=25, y=25, resting=True, location=test_zone) Mob(name="Piccolo", x=25, y=25, location=test_zone) await cmd_look(player, "") output = get_output(player) # Should see individual lines with postures assert "Krillin is resting here." in output assert "Piccolo is standing here." in output @pytest.mark.asyncio async def test_look_shows_ground_items(player, test_zone): """Look should show items on the ground.""" # Add an item on player's tile (location param auto-adds to zone) Thing(name="rusty sword", x=25, y=25, location=test_zone) await cmd_look(player, "") output = get_output(player) # Should see ground items assert "rusty sword" in output @pytest.mark.asyncio async def test_look_shows_portals(player, test_zone): """Look should show portals on player's tile.""" # Add a portal on player's tile (location param auto-adds to zone) Portal( name="wide dirt path", x=25, y=25, location=test_zone, target_zone="other", target_x=0, target_y=0, ) await cmd_look(player, "") output = get_output(player) # Should see portal with new format assert "You see wide dirt path." in output @pytest.mark.asyncio async def test_look_with_args_routes_to_examine(player, test_zone): """look should route to examine command logic.""" called = {} async def fake_examine(p, args, *, prefer_inventory=True): called["args"] = args called["prefer_inventory"] = prefer_inventory await p.send("examined\r\n") import mudlib.commands.examine original = mudlib.commands.examine.examine_target mudlib.commands.examine.examine_target = fake_examine # type: ignore[invalid-assignment] try: await cmd_look(player, "sword") finally: mudlib.commands.examine.examine_target = original output = get_output(player) assert called["args"] == "sword" assert called["prefer_inventory"] is False assert "examined" in output @pytest.mark.asyncio async def test_look_flying_shows_down_exit(player): """Flying players should see down as a vertical exit.""" player.flying = True await cmd_look(player, "") output = get_output(player) assert "Exits: north south east west down" in output @pytest.mark.asyncio async def test_look_structure_order(player, test_zone): """Look output should have sections in correct order.""" # Add entities and items (location param auto-adds to zone) Mob(name="Goku", x=25, y=25, location=test_zone) await cmd_look(player, "") output = get_output(player) # Find positions of key sections where_pos = output.find("Where:") location_pos = output.find("Location:") exits_pos = output.find("Exits:") # Verify order: Where comes first, then viewport (with terrain), # then Location, then Exits assert where_pos < location_pos assert location_pos < exits_pos @pytest.mark.asyncio async def test_look_nowhere(mock_reader, mock_writer): """Look should show 'You are nowhere.' when player has no location.""" # Create player without a location player = Player( name="NowherePlayer", x=0, y=0, reader=mock_reader, writer=mock_writer, ) player.location = None await cmd_look(player, "") output = get_output(player) assert "You are nowhere." in output @pytest.mark.asyncio async def test_look_includes_atmosphere_clear(): """Look should include atmosphere line with clear weather.""" # Initialize game time and weather init_game_time(epoch=time.time(), real_minutes_per_game_hour=1.0) init_weather(condition=WeatherCondition.clear, intensity=0.5) # Create player mock_reader = MagicMock() mock_writer = MagicMock() mock_writer.write = MagicMock() mock_writer.drain = AsyncMock() terrain = [["." for _ in range(50)] for _ in range(50)] zone = Zone( name="test_zone", description="The Test Zone", width=50, height=50, terrain=terrain, impassable={"^", "~"}, ) player = Player( name="TestPlayer", x=25, y=25, reader=mock_reader, writer=mock_writer ) player.location = zone zone._contents.append(player) await cmd_look(player, "") output = get_output(player) # Should see atmosphere line with period and season assert "[" in output and "]" in output # Should not have weather description for clear weather # (the format is: sky. [period, season]) seasons = ["spring]", "summer]", "autumn]", "winter]"] assert any(season in output for season in seasons) @pytest.mark.asyncio async def test_look_includes_atmosphere_with_rain(): """Look should include atmosphere line with rain.""" # Initialize game time and weather init_game_time(epoch=time.time(), real_minutes_per_game_hour=1.0) init_weather(condition=WeatherCondition.rain, intensity=0.5) # Create player mock_reader = MagicMock() mock_writer = MagicMock() mock_writer.write = MagicMock() mock_writer.drain = AsyncMock() terrain = [["." for _ in range(50)] for _ in range(50)] zone = Zone( name="test_zone", description="The Test Zone", width=50, height=50, terrain=terrain, impassable={"^", "~"}, ) player = Player( name="TestPlayer", x=25, y=25, reader=mock_reader, writer=mock_writer ) player.location = zone zone._contents.append(player) await cmd_look(player, "") output = get_output(player) # Should see atmosphere line with rain description assert "rain" in output.lower() # Should see season tag assert "[" in output and "]" in output @pytest.mark.asyncio async def test_look_atmosphere_between_where_and_viewport(): """Atmosphere line should appear between Where header and viewport.""" # Initialize game time and weather init_game_time(epoch=time.time(), real_minutes_per_game_hour=1.0) init_weather(condition=WeatherCondition.clear, intensity=0.5) # Create player mock_reader = MagicMock() mock_writer = MagicMock() mock_writer.write = MagicMock() mock_writer.drain = AsyncMock() terrain = [["." for _ in range(50)] for _ in range(50)] zone = Zone( name="test_zone", description="The Test Zone", width=50, height=50, terrain=terrain, impassable={"^", "~"}, ) player = Player( name="TestPlayer", x=25, y=25, reader=mock_reader, writer=mock_writer ) player.location = zone zone._contents.append(player) await cmd_look(player, "") output = get_output(player) # Find positions of key sections where_pos = output.find("Where:") location_pos = output.find("Location:") # Atmosphere should be between Where and Location # Look for a bracket tag (season indicator) bracket_pos = output.find("[") assert where_pos < bracket_pos < location_pos @pytest.mark.asyncio async def test_look_night_reduces_viewport(): """Look at night should show smaller viewport than day.""" # Set time to noon (hour 12) - epoch was 12 hours ago # elapsed_game_hours = (time.time() - epoch) / 60 / 1.0 = 12 # so epoch = time.time() - 12 * 60 init_game_time(epoch=time.time() - 12 * 60, real_minutes_per_game_hour=1.0) init_weather(condition=WeatherCondition.clear, intensity=0.5) mock_reader = MagicMock() mock_writer = MagicMock() mock_writer.write = MagicMock() mock_writer.drain = AsyncMock() terrain = [["." for _ in range(50)] for _ in range(50)] zone = Zone( name="test_zone", description="The Test Zone", width=50, height=50, terrain=terrain, impassable={"^", "~"}, ) player = Player( name="TestPlayer", x=25, y=25, reader=mock_reader, writer=mock_writer ) player.location = zone zone._contents.append(player) await cmd_look(player, "") day_output = get_output(player) # Find viewport section (between atmosphere and Location:) day_lines = day_output.split("\r\n") # Viewport is after atmosphere (containing '[') and before Location: viewport_start_idx = None viewport_end_idx = None for i, line in enumerate(day_lines): if "[" in line and viewport_start_idx is None: viewport_start_idx = i + 1 if line.startswith("Location:"): viewport_end_idx = i break assert viewport_start_idx is not None assert viewport_end_idx is not None day_viewport_height = viewport_end_idx - viewport_start_idx # Reset writer mock_writer.reset_mock() # Set time to night (hour 22) - epoch was 22 hours ago init_game_time(epoch=time.time() - 22 * 60, real_minutes_per_game_hour=1.0) await cmd_look(player, "") night_output = get_output(player) # Find viewport section night_lines = night_output.split("\r\n") viewport_start_idx = None viewport_end_idx = None for i, line in enumerate(night_lines): if "[" in line and viewport_start_idx is None: viewport_start_idx = i + 1 if line.startswith("Location:"): viewport_end_idx = i break assert viewport_start_idx is not None assert viewport_end_idx is not None night_viewport_height = viewport_end_idx - viewport_start_idx # Night should have fewer viewport lines (9 vs 11) assert night_viewport_height < day_viewport_height assert day_viewport_height == 11 # full viewport at noon assert night_viewport_height == 9 # reduced viewport at night