visual_memory / server /renderer.py
kdemon1011's picture
Upload folder using huggingface_hub
816634a verified
Raw History Blame Contribute Delete
10.9 kB
"""SVG board renderer for Visual Memory Gym.
Produces deterministic board views as inline SVG text.
Each cell state (hidden, revealed, flagged, faded, fog) has a
distinct visual theme so the agent must interpret spatial layout,
colors, and icons to reason about the board.
"""
from __future__ import annotations
from typing import Any
import svgwrite
CELL_SIZE = 48
PADDING = 24
COORD_FONT_SIZE = 11
CELL_FONT_SIZE = 13
ICON_FONT_SIZE = 18
COLORS = {
"background": "#1a1a2e",
"grid_line": "#2d2d4a",
"coord_text": "#8888aa",
"hidden_fill": "#2d2d4a",
"hidden_stroke": "#3d3d5a",
"revealed_empty_fill": "#e8e8f0",
"revealed_signal_fill": "#d0e8ff",
"revealed_hazard_fill": "#ff4d4d",
"revealed_key_fill": "#ffd700",
"revealed_decoy_fill": "#c8b8e8",
"revealed_goal_fill": "#50fa7b",
"flagged_fill": "#ff6b35",
"flagged_stroke": "#ff8c5a",
"faded_fill": "#3a3a55",
"faded_stroke": "#4a4a65",
"fog_fill": "#111122",
"cell_text": "#1a1a2e",
"hazard_text": "#ffffff",
"flag_text": "#ffffff",
}
CELL_ICONS = {
"hazard": "\u2620",
"key": "\u26bf",
"decoy": "\u2662",
"goal": "\u2605",
"flag": "\u2691",
"faded": "?",
}
class Renderer:
"""Renders a visible board grid to deterministic SVG text."""
def __init__(self, cell_size: int = CELL_SIZE, padding: int = PADDING):
self.cell_size = cell_size
self.padding = padding
def render_board(
self,
visible_cells: list[list[dict]],
board_width: int,
board_height: int,
*,
scenario_type: str = "hidden_grid",
step_count: int = 0,
) -> str:
svg_w = self.padding + board_width * self.cell_size + self.padding
svg_h = self.padding + board_height * self.cell_size + self.padding
dwg = svgwrite.Drawing(size=(f"{svg_w}px", f"{svg_h}px"))
dwg.add(dwg.rect(
insert=(0, 0),
size=(svg_w, svg_h),
fill=COLORS["background"],
))
self._draw_coords(dwg, board_width, board_height)
for r in range(board_height):
for c in range(board_width):
cell = visible_cells[r][c]
x = self.padding + c * self.cell_size
y = self.padding + r * self.cell_size
self._draw_cell(dwg, x, y, cell, scenario_type)
self._draw_grid_lines(dwg, board_width, board_height)
return dwg.tostring()
def get_board_view(
self,
visible_cells: list[list[dict]],
board_width: int,
board_height: int,
*,
scenario_type: str = "hidden_grid",
step_count: int = 0,
) -> dict[str, Any]:
svg_text = self.render_board(
visible_cells,
board_width,
board_height,
scenario_type=scenario_type,
step_count=step_count,
)
hidden_count = 0
revealed_count = 0
flagged_count = 0
faded_count = 0
fog_count = 0
for row in visible_cells:
for cell in row:
state = cell.get("state", "hidden")
if state == "hidden":
hidden_count += 1
elif state == "revealed":
revealed_count += 1
elif state == "flagged":
flagged_count += 1
elif state == "faded":
faded_count += 1
elif state == "fog":
fog_count += 1
return {
"svg": svg_text,
"metadata": {
"board_width": board_width,
"board_height": board_height,
"step_count": step_count,
"scenario_type": scenario_type,
"cell_counts": {
"hidden": hidden_count,
"revealed": revealed_count,
"flagged": flagged_count,
"faded": faded_count,
"fog": fog_count,
"total": board_width * board_height,
},
},
}
# ─── Internal Drawing Methods ───────────────────────────────────
def _draw_coords(
self,
dwg: svgwrite.Drawing,
board_width: int,
board_height: int,
) -> None:
for c in range(board_width):
x = self.padding + c * self.cell_size + self.cell_size // 2
dwg.add(dwg.text(
str(c),
insert=(x, self.padding - 6),
text_anchor="middle",
font_size=COORD_FONT_SIZE,
fill=COLORS["coord_text"],
font_family="monospace",
))
for r in range(board_height):
y = self.padding + r * self.cell_size + self.cell_size // 2 + 4
dwg.add(dwg.text(
str(r),
insert=(self.padding - 8, y),
text_anchor="middle",
font_size=COORD_FONT_SIZE,
fill=COLORS["coord_text"],
font_family="monospace",
))
def _draw_grid_lines(
self,
dwg: svgwrite.Drawing,
board_width: int,
board_height: int,
) -> None:
x0 = self.padding
y0 = self.padding
x1 = self.padding + board_width * self.cell_size
y1 = self.padding + board_height * self.cell_size
for r in range(board_height + 1):
y = y0 + r * self.cell_size
dwg.add(dwg.line(
start=(x0, y),
end=(x1, y),
stroke=COLORS["grid_line"],
stroke_width=1,
))
for c in range(board_width + 1):
x = x0 + c * self.cell_size
dwg.add(dwg.line(
start=(x, y0),
end=(x, y1),
stroke=COLORS["grid_line"],
stroke_width=1,
))
def _draw_cell(
self,
dwg: svgwrite.Drawing,
x: int,
y: int,
cell: dict,
scenario_type: str,
) -> None:
state = cell.get("state", "hidden")
content = cell.get("content")
if state == "fog":
self._draw_fog_cell(dwg, x, y)
elif state == "hidden":
self._draw_hidden_cell(dwg, x, y)
elif state == "flagged":
self._draw_flagged_cell(dwg, x, y)
elif state == "faded":
self._draw_faded_cell(dwg, x, y)
elif state == "revealed" and content:
self._draw_revealed_cell(dwg, x, y, content)
else:
self._draw_hidden_cell(dwg, x, y)
def _draw_hidden_cell(self, dwg: svgwrite.Drawing, x: int, y: int) -> None:
dwg.add(dwg.rect(
insert=(x + 1, y + 1),
size=(self.cell_size - 2, self.cell_size - 2),
fill=COLORS["hidden_fill"],
stroke=COLORS["hidden_stroke"],
stroke_width=1,
rx=3,
ry=3,
))
def _draw_fog_cell(self, dwg: svgwrite.Drawing, x: int, y: int) -> None:
dwg.add(dwg.rect(
insert=(x + 1, y + 1),
size=(self.cell_size - 2, self.cell_size - 2),
fill=COLORS["fog_fill"],
rx=3,
ry=3,
))
def _draw_flagged_cell(self, dwg: svgwrite.Drawing, x: int, y: int) -> None:
dwg.add(dwg.rect(
insert=(x + 1, y + 1),
size=(self.cell_size - 2, self.cell_size - 2),
fill=COLORS["flagged_fill"],
stroke=COLORS["flagged_stroke"],
stroke_width=1,
rx=3,
ry=3,
))
cx = x + self.cell_size // 2
cy = y + self.cell_size // 2 + 5
dwg.add(dwg.text(
CELL_ICONS["flag"],
insert=(cx, cy),
text_anchor="middle",
font_size=ICON_FONT_SIZE,
fill=COLORS["flag_text"],
))
def _draw_faded_cell(self, dwg: svgwrite.Drawing, x: int, y: int) -> None:
dwg.add(dwg.rect(
insert=(x + 1, y + 1),
size=(self.cell_size - 2, self.cell_size - 2),
fill=COLORS["faded_fill"],
stroke=COLORS["faded_stroke"],
stroke_width=1,
rx=3,
ry=3,
))
cx = x + self.cell_size // 2
cy = y + self.cell_size // 2 + 5
dwg.add(dwg.text(
CELL_ICONS["faded"],
insert=(cx, cy),
text_anchor="middle",
font_size=CELL_FONT_SIZE,
fill=COLORS["coord_text"],
font_family="monospace",
))
def _draw_revealed_cell(
self,
dwg: svgwrite.Drawing,
x: int,
y: int,
content: dict,
) -> None:
cell_type = content.get("type", "empty")
value = content.get("value")
fill = COLORS["revealed_empty_fill"]
text_color = COLORS["cell_text"]
label = ""
if cell_type == "empty":
fill = COLORS["revealed_empty_fill"]
elif cell_type == "signal":
fill = COLORS["revealed_signal_fill"]
label = self._format_signal_value(value)
elif cell_type == "hazard":
fill = COLORS["revealed_hazard_fill"]
text_color = COLORS["hazard_text"]
label = CELL_ICONS["hazard"]
elif cell_type == "key":
fill = COLORS["revealed_key_fill"]
label = CELL_ICONS["key"]
elif cell_type == "decoy":
fill = COLORS["revealed_decoy_fill"]
label = CELL_ICONS["decoy"]
elif cell_type == "goal":
fill = COLORS["revealed_goal_fill"]
label = CELL_ICONS["goal"]
dwg.add(dwg.rect(
insert=(x + 1, y + 1),
size=(self.cell_size - 2, self.cell_size - 2),
fill=fill,
rx=3,
ry=3,
))
if label:
cx = x + self.cell_size // 2
cy = y + self.cell_size // 2 + 5
font_size = ICON_FONT_SIZE if len(label) <= 2 else CELL_FONT_SIZE
dwg.add(dwg.text(
label,
insert=(cx, cy),
text_anchor="middle",
font_size=font_size,
fill=text_color,
font_family="monospace",
))
def _format_signal_value(self, value: Any) -> str:
if value is None:
return ""
if isinstance(value, int):
return str(value)
if isinstance(value, dict):
lo = value.get("min", "?")
hi = value.get("max", "?")
return f"{lo}-{hi}"
if isinstance(value, list):
return ",".join(str(v) for v in value)
return str(value)