def g54_jamslide(rng):
"""jamslide -- collision-halted sliding particles.
Particles spawn at random lattice cells and slide in a straight direction until they
hit the canvas edge or an already-settled particle, then lock in place. Each resting
particle is drawn as a square or dot sized by how far it traveled before the collision.
"""
N = rng.choice([16, 20, 24, 32, 40, 50])
use_circles = rng.random() < 0.5
primitive = "dots" if use_circles else "squares"
c = Canvas(N, "GEOMETRIC", primitive, "jamslide", tiles=False)
eight_way = rng.random() < 0.4
dirs = [(1, 0), (-1, 0), (0, 1), (0, -1)]
if eight_way:
dirs += [(1, 1), (1, -1), (-1, 1), (-1, -1)]
coverage_target = rng.uniform(0.30, 0.48)
fill_target = rng.uniform(0.4, 0.8)
area_frac = clamp(coverage_target / fill_target, 0.3, 0.92)
base_size = math.sqrt(area_frac)
tumble = rng.random() < 0.5
total_cells = N * N
n_particles = max(1, int(total_cells * fill_target))
order = [(i, j) for i in range(N) for j in range(N)]
rng.shuffle(order)
occupied = [[False] * N for _ in range(N)]
placements = []
for (i0, j0) in order:
if len(placements) >= n_particles:
break
if occupied[i0][j0]:
continue
dx, dy = rng.choice(dirs)
i, j = i0, j0
dist = 0
while True:
ni, nj = i + dx, j + dy
if ni < 0 or ni >= N or nj < 0 or nj >= N or occupied[ni][nj]:
break
i, j = ni, nj
dist += 1
occupied[i][j] = True
placements.append((i, j, dist))
max_dist = max((p[2] for p in placements), default=0)
if max_dist == 0:
max_dist = 1
for (i, j, dist) in placements:
cx, cy = c.center(i, j)
frac = dist / max_dist
size_frac = clamp(base_size * (0.85 + 0.3 * frac), 0.2, 0.97)
side = c.C * size_frac
if use_circles:
c.circle(cx, cy, side / 2)
else:
rot = 45 if tumble else None
c.rect(cx, cy, side, side, rot=rot)
return c