Spaces:
Runtime error
Runtime error
File size: 9,386 Bytes
764e34c 334147e 764e34c 334147e 764e34c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 | """Rendering utilities for ARC-AGI-3 grids (64x64, 16-color palette).
Vendored from the ARC-AGI toolkit (`arc_agi/rendering.py`) and decoupled to
sit alongside the vendored ``arcengine`` engine in this folder. The core
``frame_to_rgb_array(frame)`` works on a plain 64x64 numpy array, so the
renderer can be used without the engine; ``FrameDataRaw`` is only needed by
the animation convenience functions.
"""
import sys
import time
from typing import Any, Optional, Tuple
from .arcengine import FrameDataRaw
from numpy import ndarray
# Color mapping for frame values (0-15)
COLOR_MAP: dict[int, str] = {
0: "#FFFFFFFF", # White
1: "#CCCCCCFF", # Off-white
2: "#999999FF", # neutral Light
3: "#666666FF", # neutral
4: "#333333FF", # Off Black
5: "#000000FF", # Black
6: "#E53AA3FF", # Magenta
7: "#FF7BCCFF", # Magenta Light
8: "#F93C31FF", # Red
9: "#1E93FFFF", # Blue
10: "#88D8F1FF", # Blue Light
11: "#FFDC00FF", # Yellow
12: "#FF851BFF", # Orange
13: "#921231FF", # Maroon
14: "#4FCC30FF", # Green
15: "#A356D6FF", # Purple
}
def hex_to_rgb(hex_color: str) -> tuple[int, int, int]:
"""Convert hex color string to RGB tuple.
Args:
hex_color: Hex color string (e.g., "#FFFFFFFF").
Returns:
RGB tuple (r, g, b).
"""
# Remove '#' and convert to int
hex_color = hex_color.lstrip("#")
# Parse RGBA hex (8 chars) or RGB hex (6 chars)
if len(hex_color) == 8:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
# Alpha is ignored for display
else:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return (r, g, b)
def frame_to_rgb_array(
steps: int,
frame: ndarray,
scale: int = 4,
color_map: Optional[dict[int, str]] = None,
) -> ndarray:
"""Convert a frame to an RGB numpy array for matplotlib.
Args:
frame: 2D numpy array (64x64) with values 0-15.
scale: Upscaling factor (default 4, so 64x64 -> 256x256).
color_map: Optional color mapping dict. Uses default if None.
Returns:
3D numpy array (height, width, 3) with RGB values.
"""
import numpy as np
if color_map is None:
color_map = COLOR_MAP
height, width = frame.shape
upscaled_height = height * scale
upscaled_width = width * scale
# Create RGB array
rgb_array = np.zeros((upscaled_height, upscaled_width, 3), dtype=np.uint8)
# Fill pixels
for y in range(height):
for x in range(width):
value = int(frame[y, x])
hex_color = color_map.get(value, "#000000FF")
rgb = hex_to_rgb(hex_color)
# Fill the scaled block
for dy in range(scale):
for dx in range(scale):
rgb_array[y * scale + dy, x * scale + dx] = rgb
return rgb_array
def render_frames(
steps: int,
frame_data: FrameDataRaw,
default_fps: Optional[int] = None,
scale: int = 4,
color_map: Optional[dict[int, str]] = None,
) -> None:
"""Render multiple frames with optional FPS control using matplotlib.
Args:
frame_data: FrameDataRaw object containing frame data and other information.
default_fps: Optional FPS for frame timing. If None, displays immediately.
scale: Upscaling factor (default 4, so 64x64 -> 256x256).
color_map: Optional color mapping dict. Uses default if None.
"""
try:
import matplotlib.animation as animation
import matplotlib.pyplot as plt
except ImportError as exc:
raise ImportError(
"matplotlib is required for rendering. Install with: pip install matplotlib"
) from exc
frames = frame_data.frame
if not frames:
return
# Convert frames to RGB arrays
frame_images = [
frame_to_rgb_array(steps, frame, scale, color_map) for frame in frames
]
# Calculate interval between frames if FPS is specified
interval = (1000.0 / default_fps) if default_fps and default_fps > 0 else 100
# Create figure and axis
fig, ax = plt.subplots(figsize=(8, 8))
ax.axis("off")
ax.set_title("ARC-AGI-3 Environment")
# Display first frame
im = ax.imshow(frame_images[0], interpolation="nearest")
plt.tight_layout()
def update_frame(frame_num: int) -> Tuple[Any, ...]:
"""Update the displayed frame."""
if frame_num < len(frame_images):
im.set_array(frame_images[frame_num])
return (im,)
return (im,)
# Create animation that plays automatically
# Keep reference to prevent garbage collection
anim = animation.FuncAnimation(
fig,
update_frame,
frames=len(frame_images),
interval=interval,
blit=False,
repeat=False,
)
# Show the plot - animation will play automatically
# Use non-blocking mode so frames play and code continues
plt.ion() # Turn on interactive mode
plt.show(block=False)
# Wait for animation to complete (frames * interval + small buffer)
if interval > 0:
total_time = len(frame_images) * interval / 1000.0
else:
total_time = len(frame_images) * 0.1 # Default 100ms per frame
# Small initial pause to ensure window appears and animation starts
time.sleep(0.1)
# Use plt.pause to process events and let animation play
# This ensures the animation actually renders
plt.pause(total_time + 0.1)
# Keep animation reference alive until we're done
# Close the figure after animation completes
plt.close(fig)
plt.ioff() # Turn off interactive mode
# Explicitly delete animation after closing to avoid warnings
del anim
def rgb_to_ansi(rgb: tuple[int, int, int]) -> str:
"""Convert RGB tuple to ANSI color code.
Args:
rgb: RGB tuple (r, g, b).
Returns:
ANSI escape sequence for the color.
"""
r, g, b = rgb
return f"\033[38;2;{r};{g};{b}m"
def render_frames_terminal(
steps: int,
frame_data: FrameDataRaw,
default_fps: Optional[int] = None,
scale: int = 1,
color_map: Optional[dict[int, str]] = None,
skip_deplay: bool = False,
) -> None:
"""Render frames in the terminal using ANSI color codes, overwriting in place.
Args:
frame_data: FrameDataRaw object containing frame data and other information.
default_fps: Optional FPS for frame timing. If None, displays immediately.
scale: Scaling factor (default 1, terminal uses 2 chars per pixel for better visibility).
color_map: Optional color mapping dict. Uses default if None.
"""
frames = frame_data.frame
if not frames:
return
if color_map is None:
color_map = COLOR_MAP
# Check if terminal supports colors
if not sys.stdout.isatty():
print(
"Warning: Not a terminal, colors may not display correctly", file=sys.stderr
)
# Calculate delay between frames (default 5 FPS if not specified)
fps = default_fps if default_fps and default_fps > 0 else 5
delay = 1.0 / fps
# ANSI codes
RESET = "\033[0m"
HOME = "\033[H" # Move cursor to home position (top-left)
HIDE_CURSOR = "\033[?25l" # Hide cursor
SHOW_CURSOR = "\033[?25h" # Show cursor
# Use block character for better visibility (2 chars wide)
BLOCK = "██"
# Get frame dimensions
height, width = frames[0].shape
# Build frame strings for all frames (as single strings to reduce flicker)
frame_strings = []
for frame_idx, frame in enumerate(frames):
# Build entire frame as one string
frame_str = f"Step: {steps} - State: {frame_data.state.name}\n\n"
# Frame content - build all lines first, then join
frame_lines = []
for y in range(height):
line_parts = []
for x in range(width):
value = int(frame[y, x])
hex_color = color_map.get(value, "#000000FF")
rgb = hex_to_rgb(hex_color)
ansi_color = rgb_to_ansi(rgb)
# Use block character (2 chars) for better visibility
line_parts.append(f"{ansi_color}{BLOCK}{RESET}")
frame_lines.append("".join(line_parts))
frame_str += "\n".join(frame_lines)
frame_strings.append(frame_str)
# Hide cursor to reduce flicker
print(HIDE_CURSOR, end="", flush=True)
try:
# First frame: clear screen and display in single operation to reduce flicker
print(f"{HOME}\033[2J{frame_strings[0]}", end="", flush=True)
# Update frames in place - single operation per frame to minimize flicker
for frame_idx in range(1, len(frames)):
# Single operation: move home + print new frame (no separate operations)
print(f"{HOME}{frame_strings[frame_idx]}", end="", flush=True)
# Sleep for 1/fps seconds between frames
if not skip_deplay:
time.sleep(delay)
finally:
# Always show cursor again
print(SHOW_CURSOR, end="", flush=True)
# Reset terminal colors and move cursor below the frame
print(f"\n{RESET}", end="", flush=True)
if not skip_deplay:
time.sleep(delay)
|