From 780216b17483862b467a6de07622821c7c28ea9c Mon Sep 17 00:00:00 2001 From: 44r0n7 <44r0n7+gitea@pm.me> Date: Wed, 28 Oct 2026 23:35:30 -0400 Subject: [PATCH] Tidy the t93 resting-face spill: shared open-cell and fall helpers, no behaviour change Co-Authored-By: Claude Opus 5.5 --- docs/agent-log.md | 12 ++++++++++ src/sim/cells.js | 57 ++++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/docs/agent-log.md b/docs/agent-log.md index 5c3ad8a..c4222a0 100644 --- a/docs/agent-log.md +++ b/docs/agent-log.md @@ -7,6 +7,18 @@ open or not verified. Keep each entry short; the git history has the details. +## 2026-10-28: t93 round 3 review and tidy (Claude) + +- Checked in the real game (WebGPU Chromium, real mouse): hovering the bot 100 grains up and + blowing sand straight above it at width 6 buries the bot and builds the owner's vertical wall. + Old rules: the wall still stands 5 s after release. New rules: a slope within 5 s. +- Refactor only, no behaviour change (world hash identical before and after): one `powderOpen` + test and one `powderFall` loop replace six copies of the open-cell check and three scan loops; + comments now say why resting faces need the spill and what the avalanche flag means. +- Checked: the new flag bits 8/16 collide with nothing (the renderer reads only F_NAT); stone + rubble is free-flowing (STICK 1) so it gets the rule; narrow towers and rubble or loose-dirt + mixes collapse on both old and new rules. Full suite with browser: ALL PASSED. + ## 2026-10-28: t93 round 3, nozzle-built resting sand faces (Codex) - Reproduced the owner's continuous straight-up radius-6 sand nozzle recipe, not a diff --git a/src/sim/cells.js b/src/sim/cells.js index cb6f6ae..a053f33 100644 --- a/src/sim/cells.js +++ b/src/sim/cells.js @@ -78,27 +78,33 @@ const SORTP_FUEL = 0.003; // World generation settles with both at 1 and corner sealing off, so seeds keep the same world. let LSTEP = 8, LFILM = 4; let CORNER_SEAL = true, POWDER_FAN = true; -// Every spill checks each crossed cell, including barriers outside the source chunk. +// Open to a moving grain: empty, no built part or barrier in the way. +function powderOpen(m, j, dx, dy) { + return mat[j] === EMPTY && !part[j] && !cleatMask[j] && passable(m, PH_POWDER, j, dx, dy); +} +// Lengthen a straight fall down column cell j (row y) while the cells below stay open. +function powderFall(m, j, y, drop, limit) { + while (drop < limit && y + drop + 1 < H && powderOpen(m, j + W * (drop + 1), 0, 1)) drop++; + return drop; +} +// Up to `limit` grains sideways to the last open drop, then down it. Checks every crossed cell, +// including barriers outside the source chunk. function powderSpill(i, x, y, m, dx, limit, avalanche) { let reach = 0; for (let step = 1; step <= limit; step++) { - const nx = x + dx * step, side = i + dx * step, j = side + W; - if (nx < 0 || nx >= W || mat[side] !== EMPTY || part[side] || cleatMask[side] || - !passable(m, PH_POWDER, side, dx, 0)) break; - if (mat[j] === EMPTY && !part[j] && !cleatMask[j] && passable(m, PH_POWDER, j, dx, 1)) reach = step; + const nx = x + dx * step, side = i + dx * step; + if (nx < 0 || nx >= W || !powderOpen(m, side, dx, 0)) break; + if (powderOpen(m, side + W, dx, 1)) reach = step; } if (!reach) return false; - let drop = 1; - for (let step = 2; step <= limit && y + step < H; step++) { - const j = i + W * step + dx * reach; - if (mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, 0, 1)) break; - drop = step; - } + const drop = powderFall(m, i + dx * reach, y, 1, limit); if (avalanche) avalancheMove(i, x, y, dx, reach, drop); else move(i, i + W * drop + dx * reach, x, y, x + dx * reach, y + drop); return true; } -// Removing support passes the collapse direction up the column; contact cancels it. +// An avalanche is the only memory of motion a grain has: flagged grains keep falling outward next +// tick, and the free-flowing grain above inherits the flag because its support just left. Landing +// on anything clears it. function avalancheMove(i, x, y, dx, reach, drop) { const fl = F_AVALANCHE | (dx > 0 ? F_AVALANCHE_R : 0); if (y && PH[mat[i - W]] === PH_POWDER && STICK[mat[i - W]] === 1) flags[i - W] = (flags[i - W] & ~F_AVALANCHE_R) | fl; @@ -111,29 +117,24 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) { if (flags[i] & F_AVALANCHE) { const dx = flags[i] & F_AVALANCHE_R ? 1 : -1; flags[i] &= ~(F_AVALANCHE | F_AVALANCHE_R); - let reach = 0; - for (let step = 1; step <= 2 && mat[b] === EMPTY; step++) { - const nx = x + dx * step, j = i + dx * step; - if (nx < 0 || nx >= W || mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, dx, 0)) break; - reach = step; - } - let drop = 0; - for (let step = 1; step <= 8 && y + step < H; step++) { - const j = i + dx * reach + W * step; - if (mat[j] !== EMPTY || part[j] || cleatMask[j] || !passable(m, PH_POWDER, j, 0, 1)) break; - drop = step; - } - if (drop && mat[b] === EMPTY && !part[i] && !cleatMask[i] && !(flags[i] & F_BURN)) { - avalancheMove(i, x, y, dx, reach, drop); return; + if (mat[b] === EMPTY && !part[i] && !cleatMask[i] && !(flags[i] & F_BURN)) { + let reach = 0; + while (reach < 2 && x + dx * (reach + 1) >= 0 && x + dx * (reach + 1) < W && powderOpen(m, i + dx * (reach + 1), dx, 0)) reach++; + const drop = powderFall(m, i + dx * reach, y, 0, 8); + if (drop) { avalancheMove(i, x, y, dx, reach, drop); return; } } } - // Tall free-flowing bodies topple from rest; short feeds and cohesive grains keep their repose. + // A grain on a face, open on one side and backed by powder on the other, either above a step or + // mid-face. Grains resting there only ever step one cell, so a tall face crumbles a grain at a + // time and stands as a wall (t93): when the body behind is at least 32 grains deep, it spills + // like the falling face below. Cohesive grains keep their steeper repose. if (POWDER_FAN && STICK[m] === 1 && (mat[b] === EMPTY || PH[mat[b]] === PH_POWDER) && !part[i] && !((flags[i] | flags[b]) & F_BURN) && x > 0 && x < W - 1) { const dx = mat[i - 1] === EMPTY && PH[mat[i + 1]] === PH_POWDER ? -1 : mat[i + 1] === EMPTY && PH[mat[i - 1]] === PH_POWDER ? 1 : 0; if (dx && !cleatMask[i] && ((PH[mat[b]] === PH_POWDER && y + 2 < H && mat[b + W + dx] === EMPTY) || (y > 0 && PH[mat[i - W]] === PH_POWDER && mat[i - W + dx] === EMPTY && PH[mat[b - dx]] === PH_POWDER))) { - // Read at most 24 rows away: with eight-grain writes, same-colour chunks stay independent. + // Depth is read 8 grains inside, at most 24 rows up or down: with the spill's 8-grain writes, + // that stays within the neighbouring chunks, so same-colour worker chunks never overlap. const insideX = x - dx * 8; let depth = 0; if (insideX >= 0 && insideX < W) {