Spaces:
Sleeping
Sleeping
| """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) | |