"""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)