def g51_packing(rng):
"""PACKING — circles claim lattice points by mutual exclusion, then each grows
only until it would touch its nearest surviving neighbor, so every mark's size
is set by the collision it avoids rather than by any global rule."""
N = rng.choice([32, 40, 50, 64])
c = Canvas(N, "GEOMETRIC", "dots", "packing", tiles=False)
C = c.C
nz = Value2D(rng, res=rng.choice([3, 4, 5]))
base_excl = rng.uniform(1.6, 3.0) * C
excl_var = rng.uniform(0.3, 1.4) * C
gap = rng.uniform(0.80, 0.97)
cells = [(i, j) for i in range(N) for j in range(N)]
rng.shuffle(cells)
accepted = []
for i, j in cells:
x, y = c.center(i, j)
u, v = (i + 0.5) / N, (j + 0.5) / N
local_excl = base_excl + excl_var * nz(u, v)
blocked = False
for (ax, ay) in accepted:
dx = x - ax
dy = y - ay
if dx * dx + dy * dy < local_excl * local_excl:
blocked = True
break
if not blocked:
accepted.append((x, y))
k = len(accepted)
for idx in range(k):
x, y = accepted[idx]
nearest = None
for jdx in range(k):
if jdx == idx:
continue
ox, oy = accepted[jdx]
d = math.hypot(x - ox, y - oy)
if nearest is None or d < nearest:
nearest = d
if nearest is None:
nearest = C * 2
r = (nearest / 2.0) * gap
c.circle(x, y, r)
return c