"""Deterministic renderer and analytic arm for marked articulation search.""" from __future__ import annotations import random from collections import deque from copy import deepcopy from PIL import Image, ImageDraw COLOR_RGB = { "red": (208, 61, 67), "blue": (48, 105, 190), "green": (42, 142, 86), "purple": (137, 72, 170), } COLORS = tuple(COLOR_RGB) GOLD = (242, 184, 54) QUESTION = ( "Four colored cell islands are shown, each with one gold pin. Which color's " "island would split into separate pieces if the single cell holding its gold " "pin were removed? Exactly one qualifies. Answer red, blue, green, or purple." ) def _neighbors(cell: tuple[int, int]) -> tuple[tuple[int, int], ...]: row, col = cell return ((row - 1, col), (row + 1, col), (row, col - 1), (row, col + 1)) def _component_count(cells: set[tuple[int, int]]) -> int: remaining = set(cells) count = 0 while remaining: count += 1 start = remaining.pop() queue = [start] while queue: current = queue.pop() for neighbor in _neighbors(current): if neighbor in remaining: remaining.remove(neighbor) queue.append(neighbor) return count def _components_after_removal(cells: set[tuple[int, int]], marker: tuple[int, int]) -> int: return _component_count(cells - {marker}) def _grow_shape(rng: random.Random) -> set[tuple[int, int]]: cells = {(rng.randrange(5), rng.randrange(5))} while len(cells) < 13: frontier = sorted( { neighbor for cell in cells for neighbor in _neighbors(cell) if 0 <= neighbor[0] < 5 and 0 <= neighbor[1] < 5 and neighbor not in cells } ) cells.add(rng.choice(frontier)) return cells def _sample_pattern(rng: random.Random, split_after_marked_removal: bool) -> tuple[list[list[int]], list[int]]: for _ in range(20000): cells = _grow_shape(rng) if {row for row, _ in cells} != set(range(5)) or {col for _, col in cells} != set(range(5)): continue eligible = [] for marker in sorted(cells): if sum(neighbor in cells for neighbor in _neighbors(marker)) != 2: continue pieces = _components_after_removal(cells, marker) if (pieces == 2) == split_after_marked_removal: eligible.append(marker) if eligible: marker = rng.choice(eligible) return [list(cell) for cell in sorted(cells)], list(marker) raise RuntimeError("could not sample a balanced marked cell island") def _sample_origins(rng: random.Random) -> list[list[int]]: origins: list[tuple[int, int]] = [] for _ in range(4): for _attempt in range(10000): candidate = (rng.randint(28, 416), rng.randint(28, 416)) if all( abs(candidate[0] - prior[0]) >= 105 or abs(candidate[1] - prior[1]) >= 105 for prior in origins ): origins.append(candidate) break else: raise RuntimeError("could not place four separated islands") rng.shuffle(origins) return [list(item) for item in origins] def sample_scene(seed: int) -> dict: rng = random.Random(int(seed)) target_color = COLORS[int(seed) % len(COLORS)] origins = _sample_origins(rng) color_order = list(COLORS) rng.shuffle(color_order) clusters = [] for color, origin in zip(color_order, origins, strict=True): cells, marker = _sample_pattern(rng, color == target_color) clusters.append( {"color": color, "origin": origin, "cells": cells, "marker": marker} ) return { "width": 512, "height": 512, "background": [250, 248, 242], "cell_step": 14, "tile_size": 12, "bridge_width": 4, "marker_radius": 4, "clusters": clusters, } def _scores(scene: dict) -> list[tuple[int, str]]: scores = [] for cluster in scene["clusters"]: cells = {tuple(cell) for cell in cluster["cells"]} marker = tuple(cluster["marker"]) score = ( _components_after_removal(cells, marker) if marker in cells and _component_count(cells) == 1 else 0 ) scores.append((score, str(cluster["color"]))) return scores def analytic_gold(scene: dict) -> str: scores = sorted(_scores(scene), reverse=True) if len(scores) != 4 or scores[0][0] < 2 or scores[0][0] == scores[1][0]: raise ValueError("scene lacks a unique marked articulation winner") return scores[0][1] def margin(scene: dict) -> float: scores = sorted((score for score, _ in _scores(scene)), reverse=True) return float(scores[0] - scores[1]) def is_quarantined(scene: dict) -> bool: try: return margin(scene) < 1.0 or analytic_gold(scene) not in COLORS except Exception: return True def render(scene: dict) -> Image.Image: image = Image.new( "RGB", (int(scene["width"]), int(scene["height"])), tuple(scene["background"]) ) draw = ImageDraw.Draw(image) step = int(scene["cell_step"]) tile = int(scene["tile_size"]) bridge = int(scene["bridge_width"]) radius = int(scene["marker_radius"]) for cluster in scene["clusters"]: color = COLOR_RGB[str(cluster["color"])] ox, oy = (int(value) for value in cluster["origin"]) cells = {tuple(int(value) for value in cell) for cell in cluster["cells"]} for row, col in sorted(cells): x = ox + col * step y = oy + row * step draw.rounded_rectangle((x, y, x + tile - 1, y + tile - 1), radius=2, fill=color) half = bridge // 2 for row, col in sorted(cells): cx = ox + col * step + (tile - 1) // 2 cy = oy + row * step + (tile - 1) // 2 if (row, col + 1) in cells: draw.rectangle((cx, cy - half, cx + step, cy + half), fill=color) if (row + 1, col) in cells: draw.rectangle((cx - half, cy, cx + half, cy + step), fill=color) marker_row, marker_col = (int(value) for value in cluster["marker"]) mx = ox + marker_col * step + (tile - 1) // 2 my = oy + marker_row * step + (tile - 1) // 2 draw.ellipse((mx - radius, my - radius, mx + radius, my + radius), fill=GOLD) return image def latent_symmetries(scene: dict) -> list[tuple[str, dict]]: twin = deepcopy(scene) twin["clusters"] = list(reversed(twin["clusters"])) return [("reverse_disjoint_draw_order", twin)]