Removed identical local copies from 45 test files. These fixtures are already defined in conftest.py.
428 lines
13 KiB
Python
428 lines
13 KiB
Python
"""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 <thing> 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
|