def g53_colony(rng):
"""Colony — a cyclic cellular automaton; each cell's settled state is its ramp step.
States 0..steps-1 chase each other cyclically on a wrapped lattice: a cell
advances to the next state once enough of its neighbours already hold it.
Run long enough, random noise resolves into rotating spiral fronts, and the
settled state of every cell becomes its ramp index directly — no field, no
quantiser, the automaton IS the ramp.
"""
N = rng.choice([30, 40, 50])
steps = rng.randint(5, 9)
c = Canvas(N, rng.choice(PALETTE_NAMES), steps, "colony", "squares", tiles=True)
moore = rng.random() < 0.6
if moore:
deltas = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
threshold = rng.choice([2, 3, 3, 4])
else:
deltas = [(-1, 0), (1, 0), (0, -1), (0, 1)]
threshold = rng.choice([1, 1, 2])
state = [[rng.randrange(steps) for _ in range(N)] for _ in range(N)]
gens = int(N * rng.uniform(1.8, 2.4))
for _ in range(gens):
nxt = [[0] * N for _ in range(N)]
for j in range(N):
row_above = state[j - 1]
row = state[j]
row_below = state[(j + 1) % N]
for i in range(N):
s = row[i]
nx = (s + 1) % steps
cnt = 0
for (di, dj) in deltas:
src = row_above if dj == -1 else (row_below if dj == 1 else row)
if src[(i + di) % N] == nx:
cnt += 1
if cnt >= threshold:
break
nxt[j][i] = nx if cnt >= threshold else s
state = nxt
c.ground(lambda i, j: state[j][i])
C = c.C
hi = steps // 2
for j in range(N):
row_above = state[j - 1]
row = state[j]
row_below = state[(j + 1) % N]
for i in range(N):
s = row[i]
nx = (s + 1) % steps
front = (row_above[i] == nx or row_below[i] == nx or
row[i - 1] == nx or row[(i + 1) % N] == nx)
if front:
ins = C * 0.30
cx, cy = i * C + ins, j * C + ins
c.sq(cx, cy, C - 2 * ins, C - 2 * ins, (s + hi) % steps)
return c