Add render/room.py with structured room display functions
This commit is contained in:
parent
1f7db3a205
commit
d7d4fff701
2 changed files with 310 additions and 0 deletions
139
src/mudlib/render/room.py
Normal file
139
src/mudlib/render/room.py
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
"""Pure functions for structured room display."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def render_where(zone_name: str) -> str:
|
||||||
|
"""Render the zone description line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zone_name: The zone's description/name to display
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted string like "Where: The Overworld"
|
||||||
|
"""
|
||||||
|
return f"Where: {zone_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_location(zone, x: int, y: int) -> str:
|
||||||
|
"""Render location with quadrant and coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zone: The zone object (needs width and height attributes)
|
||||||
|
x: X coordinate
|
||||||
|
y: Y coordinate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted string like "Location: northeast 47, 53"
|
||||||
|
"""
|
||||||
|
# Calculate quadrant based on zone thirds
|
||||||
|
third_w = zone.width / 3
|
||||||
|
third_h = zone.height / 3
|
||||||
|
|
||||||
|
# Determine horizontal component (west/center/east)
|
||||||
|
if x < third_w:
|
||||||
|
h_part = "west"
|
||||||
|
elif x < 2 * third_w:
|
||||||
|
h_part = "center"
|
||||||
|
else:
|
||||||
|
h_part = "east"
|
||||||
|
|
||||||
|
# Determine vertical component (north/center/south)
|
||||||
|
if y < third_h:
|
||||||
|
v_part = "north"
|
||||||
|
elif y < 2 * third_h:
|
||||||
|
v_part = "center"
|
||||||
|
else:
|
||||||
|
v_part = "south"
|
||||||
|
|
||||||
|
# Combine into quadrant name
|
||||||
|
if h_part == "center" and v_part == "center":
|
||||||
|
quadrant = "center"
|
||||||
|
elif h_part == "center":
|
||||||
|
quadrant = v_part
|
||||||
|
elif v_part == "center":
|
||||||
|
quadrant = h_part
|
||||||
|
else:
|
||||||
|
quadrant = f"{v_part}{h_part}"
|
||||||
|
|
||||||
|
return f"Location: {quadrant} {x}, {y}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_nearby(entities: list, viewer) -> str:
|
||||||
|
"""Render nearby entities line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entities: List of entities in viewport but not on viewer's tile
|
||||||
|
viewer: The viewing entity (unused for now)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted string like "Nearby: (3) saibaman / Master Roshi / Goku"
|
||||||
|
or empty string if no nearby entities
|
||||||
|
"""
|
||||||
|
if not entities:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
count = len(entities)
|
||||||
|
names = " / ".join(entity.name for entity in entities)
|
||||||
|
return f"Nearby: ({count}) {names}"
|
||||||
|
|
||||||
|
|
||||||
|
def render_exits(zone, x: int, y: int) -> str:
|
||||||
|
"""Render available exits from current position.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zone: The zone object (needs is_passable method)
|
||||||
|
x: Current X coordinate
|
||||||
|
y: Current Y coordinate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Formatted string like "Exits: north south east west"
|
||||||
|
"""
|
||||||
|
exits = []
|
||||||
|
|
||||||
|
# Check cardinal directions
|
||||||
|
# NOTE: up/down exits deferred until z-axis movement is implemented
|
||||||
|
if zone.is_passable(x, y - 1): # north (y decreases going up)
|
||||||
|
exits.append("north")
|
||||||
|
if zone.is_passable(x, y + 1): # south
|
||||||
|
exits.append("south")
|
||||||
|
if zone.is_passable(x + 1, y): # east
|
||||||
|
exits.append("east")
|
||||||
|
if zone.is_passable(x - 1, y): # west
|
||||||
|
exits.append("west")
|
||||||
|
|
||||||
|
if exits:
|
||||||
|
return f"Exits: {' '.join(exits)}"
|
||||||
|
return "Exits:"
|
||||||
|
|
||||||
|
|
||||||
|
_POSTURE_MESSAGES = {
|
||||||
|
"standing": "is standing here.",
|
||||||
|
"resting": "is resting here.",
|
||||||
|
"flying": "is flying above.",
|
||||||
|
"fighting": "is fighting here.",
|
||||||
|
"unconscious": "is unconscious.",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render_entity_lines(entities: list, viewer) -> str:
|
||||||
|
"""Render entity descriptions based on posture.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entities: List of entities on viewer's tile (excluding viewer)
|
||||||
|
viewer: The viewing entity (unused for now)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Newline-separated entity descriptions, or empty string if none
|
||||||
|
"""
|
||||||
|
if not entities:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
for entity in entities:
|
||||||
|
# Get posture with fallback to "standing"
|
||||||
|
posture = getattr(entity, "posture", "standing")
|
||||||
|
msg = _POSTURE_MESSAGES.get(posture, "is standing here.")
|
||||||
|
lines.append(f"{entity.name} {msg}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
171
tests/test_room_render.py
Normal file
171
tests/test_room_render.py
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
"""Tests for room rendering functions."""
|
||||||
|
|
||||||
|
from mudlib.entity import Mob
|
||||||
|
from mudlib.render.room import (
|
||||||
|
render_entity_lines,
|
||||||
|
render_exits,
|
||||||
|
render_location,
|
||||||
|
render_nearby,
|
||||||
|
render_where,
|
||||||
|
)
|
||||||
|
from mudlib.zone import Zone
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_where():
|
||||||
|
"""render_where returns 'Where: {zone description}'."""
|
||||||
|
zone = Zone(name="overworld", description="The Overworld")
|
||||||
|
assert render_where(zone.description) == "Where: The Overworld"
|
||||||
|
|
||||||
|
zone2 = Zone(name="cave", description="Dark Cave")
|
||||||
|
assert render_where(zone2.description) == "Where: Dark Cave"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_location_center():
|
||||||
|
"""render_location shows 'center' for positions near zone center."""
|
||||||
|
zone = Zone(name="test", width=50, height=50, terrain=[])
|
||||||
|
# Center should be around 25, 25 (middle third of both axes)
|
||||||
|
assert render_location(zone, 25, 25) == "Location: center 25, 25"
|
||||||
|
assert render_location(zone, 20, 20) == "Location: center 20, 20"
|
||||||
|
assert render_location(zone, 30, 30) == "Location: center 30, 30"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_location_quadrants():
|
||||||
|
"""render_location shows correct quadrants based on position."""
|
||||||
|
zone = Zone(name="test", width=60, height=60, terrain=[])
|
||||||
|
# Thirds: 0-19 west/north, 20-39 center, 40-59 east/south
|
||||||
|
|
||||||
|
# Northwest
|
||||||
|
assert render_location(zone, 5, 5) == "Location: northwest 5, 5"
|
||||||
|
assert render_location(zone, 10, 10) == "Location: northwest 10, 10"
|
||||||
|
|
||||||
|
# North (center horizontally, north vertically)
|
||||||
|
assert render_location(zone, 30, 5) == "Location: north 30, 5"
|
||||||
|
|
||||||
|
# Northeast
|
||||||
|
assert render_location(zone, 50, 5) == "Location: northeast 50, 5"
|
||||||
|
|
||||||
|
# West (west horizontally, center vertically)
|
||||||
|
assert render_location(zone, 5, 30) == "Location: west 5, 30"
|
||||||
|
|
||||||
|
# East
|
||||||
|
assert render_location(zone, 50, 30) == "Location: east 50, 30"
|
||||||
|
|
||||||
|
# Southwest
|
||||||
|
assert render_location(zone, 5, 50) == "Location: southwest 5, 50"
|
||||||
|
|
||||||
|
# South
|
||||||
|
assert render_location(zone, 30, 50) == "Location: south 30, 50"
|
||||||
|
|
||||||
|
# Southeast
|
||||||
|
assert render_location(zone, 50, 50) == "Location: southeast 50, 50"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_nearby_empty():
|
||||||
|
"""render_nearby returns empty string when no nearby entities."""
|
||||||
|
assert render_nearby([], None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_nearby_single():
|
||||||
|
"""render_nearby shows count and single entity name."""
|
||||||
|
mob = Mob(name="saibaman", x=10, y=10)
|
||||||
|
result = render_nearby([mob], None)
|
||||||
|
assert result == "Nearby: (1) saibaman"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_nearby_multiple():
|
||||||
|
"""render_nearby shows count and names separated by '/'."""
|
||||||
|
mobs = [
|
||||||
|
Mob(name="saibaman", x=10, y=10),
|
||||||
|
Mob(name="Master Roshi", x=11, y=10),
|
||||||
|
Mob(name="Goku", x=10, y=11),
|
||||||
|
]
|
||||||
|
result = render_nearby(mobs, None)
|
||||||
|
assert result == "Nearby: (3) saibaman / Master Roshi / Goku"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_exits_all_directions():
|
||||||
|
"""render_exits shows all passable cardinal directions."""
|
||||||
|
# Create zone with all passable terrain (grass)
|
||||||
|
terrain = [["." for _ in range(10)] for _ in range(10)]
|
||||||
|
zone = Zone(
|
||||||
|
name="test", width=10, height=10, terrain=terrain, impassable={"^", "~"}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = render_exits(zone, 5, 5)
|
||||||
|
assert result == "Exits: north south east west"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_exits_blocked_directions():
|
||||||
|
"""render_exits only shows passable directions."""
|
||||||
|
# Create zone with mountains (^) blocking some directions
|
||||||
|
terrain = [
|
||||||
|
[".", ".", ".", ".", "."],
|
||||||
|
[".", ".", "^", ".", "."], # mountain north
|
||||||
|
[".", "^", ".", "^", "."], # mountains west and east
|
||||||
|
[".", ".", "^", ".", "."], # mountain south
|
||||||
|
[".", ".", ".", ".", "."],
|
||||||
|
]
|
||||||
|
zone = Zone(name="test", width=5, height=5, terrain=terrain, impassable={"^", "~"})
|
||||||
|
|
||||||
|
# Position at center (2, 2) — surrounded by mountains
|
||||||
|
result = render_exits(zone, 2, 2)
|
||||||
|
assert result == "Exits:" # no passable exits
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_exits_partial():
|
||||||
|
"""render_exits shows only available exits."""
|
||||||
|
terrain = [
|
||||||
|
[".", ".", "."],
|
||||||
|
["^", ".", "."], # mountain to west
|
||||||
|
[".", "^", "."], # mountain to south
|
||||||
|
]
|
||||||
|
zone = Zone(name="test", width=3, height=3, terrain=terrain, impassable={"^"})
|
||||||
|
|
||||||
|
# Position at (1, 1) — north and east are passable
|
||||||
|
result = render_exits(zone, 1, 1)
|
||||||
|
assert result == "Exits: north east"
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_entity_lines_empty():
|
||||||
|
"""render_entity_lines returns empty string when no entities."""
|
||||||
|
assert render_entity_lines([], None) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_entity_lines_single():
|
||||||
|
"""render_entity_lines shows single entity with posture."""
|
||||||
|
mob = Mob(name="saibaman", x=10, y=10)
|
||||||
|
# Default posture is "standing" (fallback)
|
||||||
|
result = render_entity_lines([mob], None)
|
||||||
|
assert result == "saibaman is standing here."
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_entity_lines_multiple():
|
||||||
|
"""render_entity_lines shows multiple entities, each on own line."""
|
||||||
|
# Create entities with different states that affect posture
|
||||||
|
mob1 = Mob(name="saibaman", x=10, y=10) # standing by default
|
||||||
|
mob2 = Mob(name="Master Roshi", x=10, y=10, resting=True) # resting
|
||||||
|
|
||||||
|
result = render_entity_lines([mob1, mob2], None)
|
||||||
|
lines = result.split("\n")
|
||||||
|
assert len(lines) == 2
|
||||||
|
assert lines[0] == "saibaman is standing here."
|
||||||
|
assert lines[1] == "Master Roshi is resting here."
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_entity_lines_postures():
|
||||||
|
"""render_entity_lines handles different posture types based on entity state."""
|
||||||
|
# Standing (default state)
|
||||||
|
mob = Mob(name="Goku", x=10, y=10)
|
||||||
|
assert render_entity_lines([mob], None) == "Goku is standing here."
|
||||||
|
|
||||||
|
# Resting (resting=True)
|
||||||
|
mob_resting = Mob(name="Goku", x=10, y=10, resting=True)
|
||||||
|
assert render_entity_lines([mob_resting], None) == "Goku is resting here."
|
||||||
|
|
||||||
|
# Unconscious (pl <= 0)
|
||||||
|
mob_unconscious = Mob(name="Goku", x=10, y=10, pl=0)
|
||||||
|
assert render_entity_lines([mob_unconscious], None) == "Goku is unconscious."
|
||||||
|
|
||||||
|
# Unconscious (stamina <= 0)
|
||||||
|
mob_exhausted = Mob(name="Goku", x=10, y=10, stamina=0)
|
||||||
|
assert render_entity_lines([mob_exhausted], None) == "Goku is unconscious."
|
||||||
Loading…
Reference in a new issue