1#!/usr/bin/env python32"""Generate the 'what the agent can see' figure from a real overlapping Takuzu mosaic.34X by X tiles of n by n cells, overlap-by-one seams (pitch n-1). Every tile5window satisfies Takuzu on its own: balanced rows/cols, no three in a row,6distinct rows, distinct cols. Output is an inline SVG fragment.7"""8import random, sys910# usage: mock-mosaic.py [seed] [X] [n] [steps]11# X is tiles per side and must work for any value in 2..15; nothing below assumes a size.12SEED = int(sys.argv[1]) if len(sys.argv) > 1 else 713X = int(sys.argv[2]) if len(sys.argv) > 2 else 714N = int(sys.argv[3]) if len(sys.argv) > 3 else 4 # tile size (mock scale; real runs use n=8)15STEPS = int(sys.argv[4]) if len(sys.argv) > 4 else 9 # solved tiles after this many frontier expansions16assert 2 <= X <= 15 and N % 2 == 0 and 4 <= N <= 1217P = N - 1 # pitch18G = X * P + 1 # global grid size19rng = random.Random(SEED)2021grid = [[None] * G for _ in range(G)]2223def windows_of(x, y):24 for ty in range(X):25 for tx in range(X):26 ox, oy = tx * P, ty * P27 if ox <= x < ox + N and oy <= y < oy + N:28 yield ox, oy2930def line_ok(vals):31 filled = [v for v in vals if v is not None]32 if filled.count(0) > N // 2 or filled.count(1) > N // 2:33 return False34 for i in range(len(vals) - 2):35 a, b, c = vals[i:i + 3]36 if a is not None and a == b == c:37 return False38 return True3940def window_ok(ox, oy):41 rows = [[grid[oy + r][ox + c] for c in range(N)] for r in range(N)]42 cols = [[grid[oy + r][ox + c] for r in range(N)] for c in range(N)]43 for ln in rows + cols:44 if not line_ok(ln):45 return False46 full_rows = [tuple(r) for r in rows if None not in r]47 if len(full_rows) != len(set(full_rows)):48 return False49 full_cols = [tuple(c) for c in cols if None not in c]50 if len(full_cols) != len(set(full_cols)):51 return False52 return True5354def solve(i=0):55 if i == G * G:56 return True57 y, x = divmod(i, G)58 vals = [0, 1]59 rng.shuffle(vals)60 for v in vals:61 grid[y][x] = v62 if all(window_ok(ox, oy) for ox, oy in windows_of(x, y)) and solve(i + 1):63 return True64 grid[y][x] = None65 return False6667sys.setrecursionlimit(10000)68assert solve(), "no mosaic for this seed"6970# status per tile: no fixed layout. The seed tile comes from the seed, and the71# solved region grows by picking a frontier tile per step, exactly as a run would.72def neighbours(t):73 tx, ty = t74 for dx, dy in ((1, 0), (-1, 0), (0, 1), (0, -1)):75 u = (tx + dx, ty + dy)76 if 0 <= u[0] < X and 0 <= u[1] < X:77 yield u78solved = {(rng.randrange(X), rng.randrange(X))}79for _ in range(min(STEPS, X * X) - 1):80 fr = sorted({u for t in solved for u in neighbours(t)} - solved)81 if not fr:82 break83 solved.add(rng.choice(fr))84frontier = {u for t in solved for u in neighbours(t)} - solved8586def tile_cells(tx, ty):87 return {(tx * P + cx, ty * P + cy) for cy in range(N) for cx in range(N)}8889solved_cells = set().union(*(tile_cells(*t) for t in solved))90# givens on frontier tiles: ~40% of cells not already known from a solved neighbour91given_cells = set()92for t in sorted(frontier):93 cand = sorted(tile_cells(*t) - solved_cells)94 rng.shuffle(cand)95 given_cells |= set(cand[: max(2, len(cand) * 2 // 5)])96seam_cells = set()97for t in frontier:98 seam_cells |= tile_cells(*t) & solved_cells99100CELL, M = 22, 30101W = H = G * CELL + 2 * M102HUES = 7103def hue(tx, ty):104 # (tx + 3*ty) mod 7: every 8-neighbour differs, so no two tiles that share a cell share a hue105 return (tx + 3 * ty) % HUES106ALPHA = {"solved": 0.62, "frontier": 0.34, "hidden": 0.10}107def status(t):108 return "solved" if t in solved else "frontier" if t in frontier else "hidden"109110out = []111out.append(f'<svg viewBox="0 0 {W + 520} {max(H, 420)}" role="img" aria-label="A {X} by {X} mosaic of {N} by {N} Takuzu tiles that overlap by one cell. Each tile has its own semi-transparent tint; cells shared by two tiles show both tints combined. {len(solved)} solved tiles, grown from a seed tile by frontier expansion, show every 0 and 1. The {len(frontier)} frontier tiles around them show only their givens plus the line they share with a solved tile. The remaining tiles are hidden.">')112out.append('<g font-family="IBM Plex Mono, monospace" font-size="12" text-anchor="middle">')113out.append(f'<rect x="{M}" y="{M}" width="{G * CELL}" height="{G * CELL}" fill="var(--panel)"/>')114# one rect per cell, coloured by the mean of its owners' hues at a fixed115# lightness. Overlaps change hue, never lightness or opacity: no pyramid.116def owners(x, y):117 return [(ox // P, oy // P) for ox, oy in windows_of(x, y)]118def mix(hs):119 # nested equal-weight color-mix in oklch; L stays flat because every token shares an L120 if len(hs) == 1:121 return f"var(--t{hs[0]})"122 return f"color-mix(in oklch, {mix(hs[:-1])} {100 - 100 // len(hs)}%, var(--t{hs[-1]}))"123RANK = {"hidden": 0, "frontier": 1, "solved": 2}124for y in range(G):125 for x in range(G):126 ts = owners(x, y)127 st = max((status(t) for t in ts), key=RANK.get)128 fill = mix([hue(*t) for t in ts])129 out.append(f'<rect x="{M + x * CELL}" y="{M + y * CELL}" width="{CELL}" height="{CELL}" fill="{fill}" fill-opacity="{ALPHA[st]}"/>')130# cell grid lines131out.append('<g stroke="var(--rule)" stroke-width="0.6">')132for i in range(G + 1):133 out.append(f'<line x1="{M}" y1="{M + i * CELL}" x2="{M + G * CELL}" y2="{M + i * CELL}"/>')134 out.append(f'<line x1="{M + i * CELL}" y1="{M}" x2="{M + i * CELL}" y2="{M + G * CELL}"/>')135out.append('</g>')136# glyphs: solved cells and frontier givens137for y in range(G):138 for x in range(G):139 v = grid[y][x]140 if (x, y) in solved_cells:141 weight = ' font-weight="500"'142 elif (x, y) in given_cells:143 weight = ''144 else:145 continue146 out.append(f'<text x="{M + x * CELL + CELL / 2}" y="{M + y * CELL + CELL * 0.72}" fill="currentColor"{weight}>{v}</text>')147# tile outlines in the tile's own hue; hidden tiles get none148for ty in range(X):149 for tx in range(X):150 st = status((tx, ty))151 if st == "hidden":152 continue153 dash = '' if st == "solved" else ' stroke-dasharray="4 3"'154 out.append(f'<rect x="{M + tx * P * CELL}" y="{M + ty * P * CELL}" width="{N * CELL}" height="{N * CELL}" fill="none" stroke="var(--t{hue(tx, ty)})" stroke-width="1.6"{dash}/>')155out.append('</g>')156157# legend158LX = W + 20159def sw(y, kind):160 if kind == "solved":161 out.append(f'<rect x="{LX}" y="{y}" width="18" height="18" fill="var(--t3)" fill-opacity="0.62" stroke="var(--t3)" stroke-width="1.6"/>')162 elif kind == "frontier":163 out.append(f'<rect x="{LX}" y="{y}" width="18" height="18" fill="var(--t0)" fill-opacity="0.34" stroke="var(--t0)" stroke-width="1.6" stroke-dasharray="4 3"/>')164 elif kind == "hidden":165 out.append(f'<rect x="{LX}" y="{y}" width="18" height="18" fill="var(--t5)" fill-opacity="0.10" stroke="var(--rule)" stroke-width="0.6"/>')166 elif kind == "seam":167 out.append(f'<rect x="{LX}" y="{y}" width="9" height="18" fill="var(--t3)" fill-opacity="0.62"/>')168 out.append(f'<rect x="{LX + 9}" y="{y}" width="9" height="18" fill="color-mix(in oklch, var(--t3) 50%, var(--t0))" fill-opacity="0.62"/>')169out.append('<g font-family="IBM Plex Sans, Arial, sans-serif" font-size="13" fill="currentColor">')170y = 50171for kind, title, lines in [172 ("solved", "Solved tile", ["every cell committed and correct; all 16 values visible", "its tint at full strength, solid outline"]),173 ("frontier", "Frontier tile", ["touches a solved tile; the only place reveal() is accepted", "shows its givens plus the line it shares with the solved tile", "alone it has m solutions; the shared line is what pins it", "lighter tint, dashed outline"]),174 ("hidden", "Hidden tile", ["nothing revealed; never in the agent's context", "faintest tint, no outline"]),175 ("seam", "Seam", ["tiles overlap by one cell: that line belongs to both", "its colour is the mean of both hues at the same lightness", "opacity never stacks, so corners do not go brighter", "no two touching tiles share a hue: 7 hues, (tx + 3ty) mod 7", f"pitch is n-1: a {X}x{X} mosaic of {N}x{N} tiles is {G}x{G} cells"]),176 (None, "Scale", [f"X is any value in 2..15; this figure is X={X}, n={N}, seed {SEED}", f"{len(solved)} solved after {len(solved)} frontier steps from a seed tile", "the layout is never fixed: seed tile and wavefront come from the run"]),177]:178 if kind:179 sw(y, kind)180 out.append(f'<text x="{LX + 40}" y="{y + 14}" font-weight="500">{title}</text>')181 for i, ln in enumerate(lines):182 out.append(f'<text x="{LX + 40}" y="{y + 32 + i * 16}" font-size="12" fill="var(--muted)">{ln}</text>')183 y += 44 + 16 * len(lines)184out.append('</g></svg>')185print("\n".join(out))