Loose walls slump to a natural slope instead of eroding layer by layer (t93)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 18:58:40 -04:00
co-authored by Claude Opus 5.5
parent dadf62a28d
commit 807217b22c
7 changed files with 146 additions and 3 deletions
+7
View File
@@ -548,6 +548,13 @@ above, including:
and upward escapes when blocked. Grains fall normally after each hop. Dust must show as a few
soft puffs where grains move; the previous rubble-coloured debris squares were not visible.
- **Loose walls release from their face (t93, 2026-10-28).** The owner wants tall loose
rubble faces to collapse quickly toward a pile instead of shedding one surface layer at
a time. An open diagonal step with another empty grain below it bypasses the sticky delay;
shallow steps retain the material's stickiness. A legal sticky step that misses its chance
keeps its chunk awake to retry. Burning fuel beds keep their tuned settling behavior.
Grain stamps, sealed corners, transport barriers and per-chunk random streams still apply.
## Ideas not built yet
- **Concrete**, as a stronger wall material from rubble.
+3 -1
View File
@@ -254,7 +254,7 @@ Exploration only: never changes grains or the random stream.
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
### src/sim/cells.js (223 lines)
### src/sim/cells.js (240 lines)
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
- functions: setCell, swapCells, swapCellData, move, enter, enterFree, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL
@@ -656,6 +656,7 @@ The frame loop and boot.
- `test/t_nozzle.js`: Blowing aims within the nozzle's reach, with room for the brush and settling.
- `test/t_perf.js`: Profiling fixtures start in the same state after previous scenes and warm-up ticks.
- `test/t_physics.js`: Sand and other powders, molten metal and gases: heaps, sorting, sinking, setting, rising and fading. Water has its own suite (t_water.js). Tall worlds keep geothermal heat out of the way.
- `test/t_pile_collapse.js`: Tall loose faces release together; supported heaps stop moving, including when revisited.
- `test/t_pipes.js`: Pipes, pumps and outlets move liquids; the Grinder grinds waste rock for trace ore.
- `test/t_racks.js`: Rack intake, FIFO storage, gate, removal and saved-state validation.
- `test/t_render_dirty.js`: Render invalidation must not depend on whether a grain moved or the chunk stayed awake.
@@ -709,3 +710,4 @@ The frame loop and boot.
- `tools/scenes/t40-sloped-belt.js`: Self-contained: a flat feed climbs, crosses the top, and runs downhill; a filter sorts below.
- `tools/scenes/t88-light.js`: Frozen light comparisons, also used by the WebGPU browser regression suite.
- `tools/scenes/t88-renderer.js`: Paused comparison scene: ordinary grains, fog, heat, trees and exceptional geometry.
- `tools/scenes/t93-pile-collapse.js`: Left to right: 40x20 walls of sand, mixed rubble, and rubble with 20% clay. Capture at --ticks 0 and --ticks 180, --zoom 3 --no-light --freeze.
+44
View File
@@ -5,6 +5,50 @@ note or request it was for, what changed, the files touched, the tests run, and
open or not verified. Keep each entry short; the git history has the details.
## 2026-10-28: t93 loose faces slump without sticky-step stalls (Codex)
- Inspected the owner's screenshot and traced `powder`, `enterFree`, `move`, `moveChunk`,
waking and checkerboard worker passes. Movement-only 40x20 walls on a flat floor, with
normal sleep countdown and the bot far away: sand reaches a <=1-grain surface step in
44 ticks; rubble (stone/copper ore/iron ore/dirt) in 89; 20% clay never reaches it through
2400 ticks (2-grain ledge). Always-awake control reaches it in 249 ticks, pure clay in 585;
STICK=1 takes 44 for every mix. Cause: probabilistic sticky diagonal steps throttle face
release, and three-tick chunk sleep can strand a legal step after missed rolls. Bottom-up
scan and stamps still allow sand to collapse; empty destinations are accepted before the
target-stamp check. No scheduler or stamp changes were needed.
- Added a fallback only after a missed sticky roll: an open diagonal with another empty cell
below releases immediately; a legal shallow step wakes its chunk to retry. Retained the
original fast path, density/displacement rolls, built-surface sliding, sealed corners,
occupancy, transport and cleat checks, one-step stamps and per-chunk streams. Burning
grains/supports retain their tuned behavior: extending the fallback into burning beds
failed vent-fed furnace cleanliness. A shared-loop rewrite was also rejected after profiling.
- New `t_pile_collapse` has 17 checks: all 800 grains retained; sand, mixed rubble and
20% clay faces <=2-grain surface steps by 180 ticks at offsets 0 and 17; cohesive pure
clay <=3. After 1200 forced-awake settling ticks, another 600 leave all heap material bytes
unchanged and chunks sleep afterward. Settled clay is 27 high versus sand 23. Exact final
test against original cells.js fails three collapse checks; final code passes. Added a
clay/rubble face spanning chunk rows to the real 0/1/2/8-worker/reversed-pass hash fixture.
- Final `python3 test/run_all.py --browser` exited 0, ALL PASSED: 33 headless suites,
including furnace 9, collapse 17 and threads 12; browser gameplay 361, smooth 28,
console 18, caps 7, GPU 15, goldens 28, light 71 and Firefox 2. Evidence:
`/tmp/t93-final-browser.log`. Earlier full browser run also passed. Diff whitespace check passes.
- Final isolated hardware profile, NVIDIA Blackwell, requested command:
`python3 tools/perf.py --scenes settled,heat --looks pixel --zooms 6 --compare tools/perf-after-t88.json`.
Original-code control -> final total ms/tick: settled 2.0179 -> 1.9790 (-1.9%),
heat 2.7255 -> 2.7650 (+1.5%). Against historical t88: 1.8056 -> 1.9790 (+9.6%),
2.6231 -> 2.7650 (+5.4%). Final movement costs 0.9715/1.3816 ms/tick versus control
0.9951/1.3647. Earlier profiles varied, including unchanged heat work; final run had no
concurrent tests. Evidence: `/tmp/t93-perf-fast.json`, `/tmp/t93-idle-control.json`,
`/tmp/t93-red-exact.log`, `/tmp/t93-collapse-final.log`.
- Captured and inspected pixel `shots/t93-before.png` and final `shots/t93-after.png`:
three 40x20 walls become sloped heaps after 180 full simulation ticks. Reproduce with
`tools/scenes/t93-pile-collapse.js`, `--ticks 0` / `--ticks 180`, `--zoom 3 --no-light --freeze`.
No page errors. Rendering code unchanged; smooth screenshot not taken.
- Files: `src/sim/cells.js`, `test/t_pile_collapse.js`, `test/t_threads.js`,
`tools/scenes/t93-pile-collapse.js`, `docs/DESIGN.md`, generated `docs/PROJECT_MAP.md`,
t93 records and this log. All subtasks ticked; t93 moved to Review with a completion
report and game checks. No commit. Owner playtesting remains open.
## 2026-10-28: t92 follow-up, visible dust and a botward channel through rubble (Codex)
- Traced shake events through `drainEvents` to `spawnDebris` and `drawParticles`. Events were
+18 -1
View File
@@ -100,12 +100,29 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
}
// stickiness is grain against grain: on anything built (a wall, a slope, a belt) every material
// slides freely, so a chute carries clay as well as sand while clay heaps stay steep
if (rnd() < STICK[m] || (PH[t] === PH_SOLID && !(flags[b] & F_NAT))) {
const slide = rnd() < STICK[m] || (PH[t] === PH_SOLID && !(flags[b] & F_NAT));
const d = (RS & 2) ? 1 : -1;
if (slide) {
let nx = x + d;
if (nx >= 0 && nx < W && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i + d]] !== PH_SOLID && (!barriers || !cleatMask[i + d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b + d])) && enterFree(m, PH_POWDER, b + d, d, 1)) { move(i, b + d, x, y, nx, y + 1); return; }
nx = x - d;
if (nx >= 0 && nx < W && (!CORNER_SEAL || (PH[mat[b]] !== PH_SOLID && (!barriers || !cleatMask[b])) || (PH[mat[i - d]] !== PH_SOLID && (!barriers || !cleatMask[i - d]))) && (!barriers || (!cleatMask[i] && !cleatMask[b - d])) && enterFree(m, PH_POWDER, b - d, -d, 1)) { move(i, b - d, x, y, nx, y + 1); return; }
return;
}
if ((flags[i] | flags[b]) & F_BURN) return; // keep the tuned settling of furnace beds
for (let side = 0; side < 2; side++) {
const dx = side ? -d : d, nx = x + dx, j = b + dx;
if (nx < 0 || nx >= W || (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS)) continue;
if (CORNER_SEAL && (PH[t] === PH_SOLID || (barriers && cleatMask[b])) &&
(PH[mat[i + dx]] === PH_SOLID || (barriers && cleatMask[i + dx]))) continue;
if (barriers && (cleatMask[i] || cleatMask[j])) continue;
if (!passable(m, PH_POWDER, j, dx, 1)) continue;
// Cohesion delays shallow slides, not a face with two open grains below the step.
if (y + 2 < H && mat[j + W] === EMPTY) {
move(i, j, x, y, nx, y + 1); return;
}
// A missed sticky step must get another chance before its chunk sleeps.
chunkWake((y >> 5) * CW + (x >> 5));
}
}
function passable(m, ph, j, dx, dy) {
+48
View File
@@ -0,0 +1,48 @@
// Tall loose faces release together; supported heaps stop moving, including when revisited.
const {load, suite, flatWorld} = require('./harness');
const {createHash} = require('node:crypto');
const {run} = load();
const {check, done} = suite();
function wall(material, offset = 0) {
flatWorld(run, 192, 600, 96);
run(`RS=123; worldSeed=7; bot.x=500; bot.y=40;
for(let y=56;y<96;y++)for(let x=${80+offset};x<${100+offset};x++)setCell(y*W+x,${material},20,0);`);
}
function motion(n, revisit = false) {
run(`for(let k=0;k<${n};k++) {
tick++;tickStamp=tickStamp%255+1;
if (${revisit}) awake.fill(3);
simMove();
for(let c=0;c<NCH;c++)if(awake[c])awake[c]--;
}`);
}
function profile() {
return run(`(()=>{const h=[];let n=0,top=96;
for(let x=0;x<W;x++){let y=0;while(y<96&&PH[mat[y*W+x]]!==PH_POWDER)y++;
h.push(96-y);top=Math.min(top,y);
for(;y<96;y++)if(PH[mat[y*W+x]]===PH_POWDER)n++;
}
let cliff=0;for(let x=1;x<W;x++)cliff=Math.max(cliff,Math.abs(h[x]-h[x-1]));
return {cliff,height:96-top,n};})()`);
}
const hash = () => createHash('sha256').update(run('mat')).digest('hex');
const rubble = '[RSTONE,CUORE,FEORE,LDIRT][(x+y)%4]';
let sandHeight;
for(const [name, material] of [['sand','SAND'],['mixed rubble',rubble],['rubble with clay',`(x+y)%5===0?LCLAY:${rubble}`],['clay','LCLAY']]) {
for(const offset of [0,17]) {
wall(material,offset);
const maxCliff = name==='clay' ? 3 : 2;
let slopedAt=0;
for(let k=1;k<=180;k++) {motion(1);if(profile().cliff<=maxCliff&&!slopedAt)slopedAt=k;}
const p=profile();
check(`${name} face slumps within 180 ticks (offset ${offset})`,slopedAt>0&&p.cliff<=maxCliff&&p.n===800,JSON.stringify({slopedAt,...p}));
motion(1200,true);
if(name==='sand'&&offset===0)sandHeight=profile().height;
if(name==='clay'&&offset===0)check('sticky clay holds a steeper settled heap than sand',profile().height>sandHeight,`sand ${sandHeight}, clay ${profile().height}`);
const settled=hash();
// Force visits so sleeping cannot hide continued creeping.
motion(600,true);motion(3);
check(`${name} settled heap stays put and sleeps (offset ${offset})`,hash()===settled&&run('awake.every(v=>v===0)')&&profile().n===800);
}
}
done();
+3
View File
@@ -26,6 +26,9 @@ function setup() {
const m=materials[(random()*materials.length)|0];
setCell(y*W+x,m,m===MAGMA?1450:m===MCU?1200:m===STEAM?110:20,0);
}
// A cohesive vertical face spans chunk rows and must collapse identically in every pool.
for(let y=50;y<90;y++)for(let x=260;x<280;x++)
setCell(y*W+x,(x+y)%5===0?LCLAY:RSTONE,20,0);
addMachine('wheel',40,35,0);
for(let x=0;x<W;x++)setCell(31*W+x,STONE,20,F_NAT);
setCell(31*W+W-10,EMPTY,20,0);
+22
View File
@@ -0,0 +1,22 @@
// Left to right: 40x20 walls of sand, mixed rubble, and rubble with 20% clay.
// Capture at --ticks 0 and --ticks 180, --zoom 3 --no-light --freeze.
{
const left = Math.max(4, SX - 400), floor = SY + 40;
const rubble = [RSTONE, CUORE, FEORE, LDIRT];
for (let x = left; x < left + 390; x++) {
surfY[x] = floor; dirtY[x] = floor + 8;
for (let y = floor - 90; y < floor + 8; y++) {
bgm[y * W + x] = 0;
setCell(y * W + x, y >= floor ? STONE : EMPTY, 20, y >= floor ? F_NAT : 0);
}
}
for (let pile = 0; pile < 3; pile++) {
const start = left + 60 + pile * 120;
for (let y = floor - 40; y < floor; y++) for (let x = start; x < start + 20; x++) {
const m = pile === 0 ? SAND : pile === 2 && (x + y) % 5 === 0 ? LCLAY : rubble[(x + y) % 4];
setCell(y * W + x, m, 20, 0);
}
}
updateRoof();
window.CAM = [left + 195, floor - 24];
}