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:
parent
d7d4fff701
commit
525b2fd812
4 changed files with 260 additions and 10 deletions
|
|
@ -1,11 +1,19 @@
|
||||||
"""Look command for viewing the world."""
|
"""Look command for viewing the world."""
|
||||||
|
|
||||||
from mudlib.commands import CommandDefinition, register
|
from mudlib.commands import CommandDefinition, register
|
||||||
|
from mudlib.commands.examine import cmd_examine
|
||||||
from mudlib.commands.things import _format_thing_name
|
from mudlib.commands.things import _format_thing_name
|
||||||
from mudlib.effects import get_effects_at
|
from mudlib.effects import get_effects_at
|
||||||
from mudlib.entity import Entity
|
from mudlib.entity import Entity
|
||||||
from mudlib.player import Player
|
from mudlib.player import Player
|
||||||
from mudlib.render.ansi import RESET, colorize_terrain
|
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.thing import Thing
|
||||||
from mudlib.zone import Zone
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
|
|
@ -19,8 +27,13 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
player: The player executing the command
|
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
|
zone = player.location
|
||||||
if zone is None or not isinstance(zone, Zone):
|
if zone is None or not isinstance(zone, Zone):
|
||||||
player.writer.write("You are nowhere.\r\n")
|
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))
|
line.append(colorize_terrain(tile, player.color_depth))
|
||||||
output_lines.append("".join(line))
|
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
|
# 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
|
# Show entities (mobs, other players) at the player's position
|
||||||
entities_here = [
|
entities_here = [
|
||||||
|
|
@ -111,8 +154,9 @@ async def cmd_look(player: Player, args: str) -> None:
|
||||||
and (not hasattr(obj, "alive") or obj.alive)
|
and (not hasattr(obj, "alive") or obj.alive)
|
||||||
]
|
]
|
||||||
if entities_here:
|
if entities_here:
|
||||||
names = ", ".join(e.name for e in entities_here)
|
entity_lines = render_entity_lines(entities_here, player)
|
||||||
player.writer.write(f"Here: {names}\r\n")
|
# 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
|
# Show items on the ground at player's position
|
||||||
from mudlib.portal import Portal
|
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")
|
player.writer.write(f"On the ground: {names}\r\n")
|
||||||
|
|
||||||
if portals:
|
if portals:
|
||||||
names = ", ".join(p.name for p in portals)
|
for portal in portals:
|
||||||
player.writer.write(f"Portals: {names}\r\n")
|
player.writer.write(f"You see {portal.name}.\r\n")
|
||||||
|
|
||||||
await player.writer.drain()
|
await player.writer.drain()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -495,9 +495,9 @@ async def test_look_shows_entities_here(player, test_zone):
|
||||||
await look.cmd_look(player, "")
|
await look.cmd_look(player, "")
|
||||||
|
|
||||||
output = "".join(c[0][0] for c in player.writer.write.call_args_list)
|
output = "".join(c[0][0] for c in player.writer.write.call_args_list)
|
||||||
assert "Here: " in output
|
# New format shows individual entity lines, not "Here: "
|
||||||
assert "goblin" in output
|
assert "goblin is standing here." in output
|
||||||
assert "Ally" in output
|
assert "Ally is standing here." in output
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
205
tests/test_look_command.py
Normal file
205
tests/test_look_command.py
Normal 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
|
||||||
|
|
@ -64,7 +64,8 @@ async def test_look_shows_portal_at_position(player, test_zone, mock_writer):
|
||||||
|
|
||||||
await cmd_look(player, "")
|
await cmd_look(player, "")
|
||||||
output = "".join([call[0][0] for call in mock_writer.write.call_args_list])
|
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
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue