Refactor look command to use structured room display

- Add Where: header with zone description
- Add Location: line with quadrant and coordinates
- Add Nearby: line showing entities in viewport (not on player's tile)
- Add Exits: line showing available cardinal directions
- Replace 'Here:' with individual entity lines showing posture
- Replace 'Portals:' with individual 'You see {name}.' lines
- Add look <thing> routing to examine command
- Add comprehensive tests for new structured output
- Update existing tests to match new output format
This commit is contained in:
Jared Miller 2026-02-13 22:15:32 -05:00
parent d7d4fff701
commit 525b2fd812
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 260 additions and 10 deletions

View file

@ -1,11 +1,19 @@
"""Look command for viewing the world."""
from mudlib.commands import CommandDefinition, register
from mudlib.commands.examine import cmd_examine
from mudlib.commands.things import _format_thing_name
from mudlib.effects import get_effects_at
from mudlib.entity import Entity
from mudlib.player import Player
from mudlib.render.ansi import RESET, colorize_terrain
from mudlib.render.room import (
render_entity_lines,
render_exits,
render_location,
render_nearby,
render_where,
)
from mudlib.thing import Thing
from mudlib.zone import Zone
@ -19,8 +27,13 @@ async def cmd_look(player: Player, args: str) -> None:
Args:
player: The player executing the command
args: Command arguments (unused for now)
args: Command arguments (if provided, route to examine)
"""
# If args provided, route to examine
if args.strip():
await cmd_examine(player, args)
return
zone = player.location
if zone is None or not isinstance(zone, Zone):
player.writer.write("You are nowhere.\r\n")
@ -99,8 +112,38 @@ async def cmd_look(player: Player, args: str) -> None:
line.append(colorize_terrain(tile, player.color_depth))
output_lines.append("".join(line))
# Build structured output
output = []
# Where header
output.append(render_where(zone.description))
# Viewport
output.append("\r\n".join(output_lines))
# Location line
output.append(render_location(zone, player.x, player.y))
# Collect nearby entities (in viewport but not on player's tile)
nearby_entities = [
obj
for obj in nearby
if isinstance(obj, Entity)
and obj is not player
and (not hasattr(obj, "alive") or obj.alive)
and not (obj.x == player.x and obj.y == player.y)
]
if nearby_entities:
output.append(render_nearby(nearby_entities, player))
# Exits line
output.append(render_exits(zone, player.x, player.y))
# Send to player
player.writer.write("\r\n".join(output_lines) + "\r\n")
player.writer.write("\r\n".join(output) + "\r\n")
# Blank line before entity/item details
player.writer.write("\r\n")
# Show entities (mobs, other players) at the player's position
entities_here = [
@ -111,8 +154,9 @@ async def cmd_look(player: Player, args: str) -> None:
and (not hasattr(obj, "alive") or obj.alive)
]
if entities_here:
names = ", ".join(e.name for e in entities_here)
player.writer.write(f"Here: {names}\r\n")
entity_lines = render_entity_lines(entities_here, player)
# Convert \n to \r\n for telnet
player.writer.write(entity_lines.replace("\n", "\r\n") + "\r\n")
# Show items on the ground at player's position
from mudlib.portal import Portal
@ -130,8 +174,8 @@ async def cmd_look(player: Player, args: str) -> None:
player.writer.write(f"On the ground: {names}\r\n")
if portals:
names = ", ".join(p.name for p in portals)
player.writer.write(f"Portals: {names}\r\n")
for portal in portals:
player.writer.write(f"You see {portal.name}.\r\n")
await player.writer.drain()

View file

@ -495,9 +495,9 @@ async def test_look_shows_entities_here(player, test_zone):
await look.cmd_look(player, "")
output = "".join(c[0][0] for c in player.writer.write.call_args_list)
assert "Here: " in output
assert "goblin" in output
assert "Ally" in output
# New format shows individual entity lines, not "Here: "
assert "goblin is standing here." in output
assert "Ally is standing here." in output
@pytest.mark.asyncio

205
tests/test_look_command.py Normal file
View file

@ -0,0 +1,205 @@
"""Tests for the look command with structured room display."""
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.player import Player
from mudlib.portal import Portal
from mudlib.thing import Thing
from mudlib.zone import Zone
@pytest.fixture
def mock_writer():
writer = MagicMock()
writer.write = MagicMock()
writer.drain = AsyncMock()
return writer
@pytest.fixture
def mock_reader():
return MagicMock()
@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" 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."""
# Add an item to examine (location param auto-adds to zone)
Thing(name="sword", x=25, y=25, description="A sharp blade.", location=test_zone)
await cmd_look(player, "sword")
output = get_output(player)
# Should see the item's description (examine behavior)
assert "A sharp blade." 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

View file

@ -64,7 +64,8 @@ async def test_look_shows_portal_at_position(player, test_zone, mock_writer):
await cmd_look(player, "")
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
assert "portal" in output.lower() and "shimmering doorway" in output.lower()
# New format: "You see {portal.name}."
assert "you see shimmering doorway." in output.lower()
@pytest.mark.asyncio