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