mud/src/mudlib/render/ansi.py
Jared Miller b0fcb080d3
Wire client capabilities into Player & terrain
Parse MTTS from telnetlib3 writer during connection and store capabilities
on Player.caps field. Add convenience property Player.color_depth that
delegates to caps.color_depth for easy access by rendering code.

Changes:
- Add caps field to Player with default 16-color ANSI capabilities
- Parse MTTS in server shell after Player creation using parse_mtts()
- Add Player.color_depth property for quick capability checks
- Add tests verifying Player caps integration and color_depth property
2026-02-07 22:44:45 -05:00

158 lines
4.4 KiB
Python

"""ANSI color codes for terminal rendering."""
# ANSI color codes
RESET = "\033[0m"
BOLD = "\033[1m"
# foreground colors
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
# bright foreground colors
BRIGHT_BLACK = "\033[90m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
def fg_256(n: int) -> str:
"""Generate 256-color foreground escape code.
Args:
n: Color index (0-255). Values outside range are clamped.
Returns:
ANSI escape sequence for 256-color foreground.
"""
clamped = max(0, min(255, n))
return f"\033[38;5;{clamped}m"
def bg_256(n: int) -> str:
"""Generate 256-color background escape code.
Args:
n: Color index (0-255). Values outside range are clamped.
Returns:
ANSI escape sequence for 256-color background.
"""
clamped = max(0, min(255, n))
return f"\033[48;5;{clamped}m"
def fg_rgb(r: int, g: int, b: int) -> str:
"""Generate truecolor foreground escape code.
Args:
r: Red component (0-255). Values outside range are clamped.
g: Green component (0-255). Values outside range are clamped.
b: Blue component (0-255). Values outside range are clamped.
Returns:
ANSI escape sequence for truecolor foreground.
"""
r_clamped = max(0, min(255, r))
g_clamped = max(0, min(255, g))
b_clamped = max(0, min(255, b))
return f"\033[38;2;{r_clamped};{g_clamped};{b_clamped}m"
def bg_rgb(r: int, g: int, b: int) -> str:
"""Generate truecolor background escape code.
Args:
r: Red component (0-255). Values outside range are clamped.
g: Green component (0-255). Values outside range are clamped.
b: Blue component (0-255). Values outside range are clamped.
Returns:
ANSI escape sequence for truecolor background.
"""
r_clamped = max(0, min(255, r))
g_clamped = max(0, min(255, g))
b_clamped = max(0, min(255, b))
return f"\033[48;2;{r_clamped};{g_clamped};{b_clamped}m"
# terrain color mapping (16-color baseline)
TERRAIN_COLORS = {
".": GREEN, # grass
"^": BRIGHT_BLACK, # mountain
"~": BLUE, # water
"T": GREEN, # forest (darker would be "\033[32m")
":": YELLOW, # sand
"@": BOLD + BRIGHT_WHITE, # player
"*": BOLD + BRIGHT_RED, # other entity
}
# 256-color terrain palette
TERRAIN_COLORS_256 = {
".": fg_256(34), # grass - richer green
"^": fg_256(242), # mountain - gray
"~": fg_256(27), # water - deeper blue
"T": fg_256(22), # forest - darker green
":": fg_256(221), # sand - warm yellow
"@": BOLD + fg_256(231), # player - bright white
"*": BOLD + fg_256(196), # other entity - bright red
}
# truecolor terrain palette
TERRAIN_COLORS_RGB = {
".": fg_rgb(76, 175, 80), # grass - material green
"^": fg_rgb(96, 125, 139), # mountain - blue-gray
"~": fg_rgb(33, 150, 243), # water - material blue
"T": fg_rgb(27, 94, 32), # forest - dark green
":": fg_rgb(255, 235, 59), # sand - bright yellow
"@": BOLD + fg_rgb(255, 255, 255), # player - pure white
"*": BOLD + fg_rgb(244, 67, 54), # other entity - material red
}
def colorize_terrain(char: str, color_depth: str = "16") -> str:
"""Return ANSI-colored version of terrain character.
Args:
char: Terrain character to colorize
color_depth: Color mode - "16", "256", or "truecolor"
Returns:
ANSI-colored character with reset code, or unchanged char if no color mapping.
"""
if color_depth == "truecolor":
color = TERRAIN_COLORS_RGB.get(char, "")
elif color_depth == "256":
color = TERRAIN_COLORS_256.get(char, "")
else:
color = TERRAIN_COLORS.get(char, "")
if color:
return f"{color}{char}{RESET}"
return char
def colorize_map(grid: list[list[str]], color_depth: str = "16") -> str:
"""Colorize a 2D grid of terrain and return as string.
Args:
grid: 2D list of terrain characters
color_depth: Color mode - "16", "256", or "truecolor"
Returns:
Newline-separated string of colorized terrain rows.
"""
lines = []
for row in grid:
colored_row = "".join(colorize_terrain(char, color_depth) for char in row)
lines.append(colored_row)
return "\n".join(lines)