def g55_thaw(rng):
"""Thaw — a field-weighted, multi-seed melt front; each cell's ramp step is its arrival time.
A handful of frost seeds retreat outward together, like a thaw line eating a
frozen mosaic. Unlike a uniform flood, every frontier cell's chance of melting
next is weighted by an underlying field, so the front races through the
field's warm regions and lingers in its cold ones. Cells are ranked by
arrival order and cut into equal-sized ramp bands, then a sparse scatter of
rotated squares — tilted toward the cell each one melted from — marks the
seams between bands.
"""
N = rng.choice([25, 30, 40, 50])
steps = rng.randint(5, 9)
c = Canvas(N, rng.choice(PALETTE_NAMES), steps, "thaw", "squares")
f = Field(rng, rng.choice(["mesh", "linear", "radial", "weave", "bilinear"]),
warp=rng.uniform(0.15, 0.6), steps=steps)
f.calibrate(N)
warmth = [[0.08 + f.raw((i + 0.5) / N, (j + 0.5) / N) for j in range(N)]
for i in range(N)]
moore = rng.random() < 0.5
deltas = ([(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
if moore else [(-1, 0), (1, 0), (0, -1), (0, 1)])
nseeds = rng.randint(2, 5)
seeds = set()
while len(seeds) < nseeds:
seeds.add((rng.randrange(N), rng.randrange(N)))
seeds = list(seeds)
arrival = [[-1] * N for _ in range(N)]
frm = [[(0, 0)] * N for _ in range(N)]
order = 0
for (si, sj) in seeds:
arrival[si][sj] = order
order += 1
frontier = [] # [i, j, from_i, from_j]
infront = {} # (i, j) -> index into frontier
def push(i, j, fi, fj):
if arrival[i][j] != -1 or (i, j) in infront:
return
infront[(i, j)] = len(frontier)
frontier.append([i, j, fi, fj])
for (si, sj) in seeds:
for (di, dj) in deltas:
ni, nj = si + di, sj + dj
if 0 <= ni < N and 0 <= nj < N:
push(ni, nj, si, sj)
while frontier:
weights = [warmth[i][j] for (i, j, _, _) in frontier]
pick = rng.choices(range(len(frontier)), weights=weights, k=1)[0]
last = len(frontier) - 1
frontier[pick], frontier[last] = frontier[last], frontier[pick]
i, j, fi, fj = frontier.pop()
del infront[(i, j)]
if pick != last:
infront[(frontier[pick][0], frontier[pick][1])] = pick
arrival[i][j] = order
frm[i][j] = (fi, fj)
order += 1
for (di, dj) in deltas:
ni, nj = i + di, j + dj
if 0 <= ni < N and 0 <= nj < N:
push(ni, nj, i, j)
total = N * N
ranked = sorted((i, j) for j in range(N) for i in range(N))
ranked.sort(key=lambda p: arrival[p[0]][p[1]])
band = [[0] * N for _ in range(N)]
for rank, (i, j) in enumerate(ranked):
band[i][j] = min(steps - 1, (rank * steps) // total)
c.ground(lambda i, j: band[i][j])
seam_p = rng.uniform(0.25, 0.55)
w = c.C * rng.uniform(0.3, 0.5)
for j in range(N):
for i in range(N):
here = band[i][j]
lit = False
for (di, dj) in ((1, 0), (0, 1)):
ni, nj = i + di, j + dj
if ni < N and nj < N and band[ni][nj] != here:
lit = True
break
if not lit or rng.random() >= seam_p:
continue
if c.room() < 4:
break
fi, fj = frm[i][j]
ang = math.degrees(math.atan2(j - fj, i - fi)) if (fi, fj) != (i, j) else 0.0
cx, cy = c.center(i, j)
k = min(steps - 1, here + 1)
c.sq(cx - w / 2, cy - w / 2, w, w, k, rot=ang)
return c