Resting faces steeper than the angle of repose topple: blown sand towers collapse (t93)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -570,6 +570,21 @@ above, including:
|
||||
Resting heaps do not enter this path; world generation keeps its recorded settling rules
|
||||
and seed layouts.
|
||||
|
||||
- **Nozzle-built sand towers topple from rest (t93 round 3, 2026-10-28).** The owner
|
||||
reproduced vertical sand towers by continuously blowing straight up. Landing places grains
|
||||
at or above impact; supported face grains need to release before the outer falling column
|
||||
clears. A tall body now spills from a supported exposed face as well as a falling one.
|
||||
Apply this to free-flowing powders (STICK=1); cohesive grains retain their stickier release
|
||||
and do not inherit the outward motion. Require an open side and a steep drop, backed eight grains inward by a continuous column
|
||||
at least 32 grains high (look at most 24 rows each way). This leaves short feeds and the
|
||||
deliberately steeper small clay heaps on their existing settling rules. The spill is still
|
||||
bounded to eight grains horizontally and vertically, checking every crossed cell.
|
||||
Two cell flag bits retain the collapse direction while the released grain falls: up to two
|
||||
grains outward and eight down per tick. Removing support passes that direction to the grain
|
||||
immediately above, so the falling layer drains promptly. Contact below cancels this motion;
|
||||
ordinary repose rules take over. Keep stamped moves, barriers, burning-bed protection and
|
||||
worker independence. The existing worldgen POWDER_FAN switch disables new face releases.
|
||||
|
||||
## Ideas not built yet
|
||||
|
||||
- **Concrete**, as a stronger wall material from rubble.
|
||||
|
||||
+7
-4
@@ -225,9 +225,9 @@ Material definitions: ids, properties and metal families. ids, and drill/tank ti
|
||||
- functions: def, metalFamily
|
||||
- variables: EMPTY, GRASS, DIRT, LDIRT, SAND, GRAVEL, CLAY, STONE, GRANITE, BASALT, NMAT, MD, FIRETO
|
||||
|
||||
### src/sim/material-tables.js (54 lines)
|
||||
### src/sim/material-tables.js (55 lines)
|
||||
Material tables: typed properties, casting groups, flags, parts and tool tiers. Typed property tables
|
||||
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, P_NONE, P_SCREW_U, P_SCREW_D, P_SCREW_L, P_SCREW_R, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL, SCREW_PARTS, SCREW_DIRS, IS_SCREW, SCREW_DX, SCREW_DY, BIT_TIERS, TANK_TIERS, REACH
|
||||
- variables: PH, DENS, KC, ICAP, CASTMETAL, METAL_TIER, SOLIDMETAL, F_NAT, F_CAST, F_BURN, F_AVALANCHE, F_AVALANCHE_R, P_NONE, P_SCREW_U, P_SCREW_D, P_SCREW_L, P_SCREW_R, P_GRATE, P_GOPEN, P_WHEEL, P_INTAKE, P_FILTER, P_PIPE, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL, SCREW_PARTS, SCREW_DIRS, IS_SCREW, SCREW_DX, SCREW_DY, BIT_TIERS, TANK_TIERS, REACH
|
||||
|
||||
### src/sim/pieces.js (179 lines)
|
||||
Everything the player can build (PIECES): blocks, machines and molds, with costs and descriptions; filter settings; wall materials and their cost.
|
||||
@@ -254,9 +254,9 @@ 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 (272 lines)
|
||||
### src/sim/cells.js (321 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
|
||||
- functions: setCell, swapCells, swapCellData, move, enter, enterFree, powderSpill, avalancheMove, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
|
||||
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL, POWDER_FAN
|
||||
|
||||
### src/sim/heat.js (266 lines)
|
||||
@@ -630,11 +630,14 @@ The frame loop and boot.
|
||||
- `test/harness.js`: Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one script, the same way build.py joins them) so scenarios can drive it headlessly in Node. Errors point at the real file and line (sim/pipes.js:42), not at the joined script.
|
||||
- `test/light-cases.js`: Lighting behaviour checks executed by the GPU browser harness.
|
||||
- `test/light-readback.js`: Read float source channels, including transmittance, from the WGSL light pass.
|
||||
- `test/pile-blow.js`: Shared reproduction: continuously blow plain bagged sand straight above a stationary bot.
|
||||
- `test/pile-cut.js`: Shared Node/browser reproduction: vacuum the foot of settled 60-grain heaps, one stroke per tick.
|
||||
- `test/play.py`: Play-test in a real browser: mouse, keyboard and a mocked gamepad drive the game; save and continue. Screenshots go to shots/play/ (or the folder given as the first argument). Pixel look unless the second argument is 'smooth'.
|
||||
- `test/run_all.py`: Run the test suites and report.
|
||||
- `test/t_bar_stacking.js`: Resting bars snap into pairs of grains and tip off narrow supports.
|
||||
- `test/t_basics.js`: Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
|
||||
- `test/t_blow_collapse.js`: Owner's upward nozzle recipe, inline and with real shared-memory helpers.
|
||||
- `test/t_blow_collapse.py`: Straight-up bagged sand nozzle in the real pixel frame loop.
|
||||
- `test/t_build.js`: Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
|
||||
- `test/t_caps.py`: Capability detection, title notices and the required-WebGPU startup gate.
|
||||
- `test/t_conveyors.js`: Screw conveyors that never carry liquid (it flows through), screw and belt throughput, filters.
|
||||
|
||||
@@ -86,6 +86,13 @@ checks waking within eight ticks separately from the probabilistic consumption o
|
||||
bot-near/far, grain conservation, equal material bytes and forced-awake resting stability.
|
||||
It also checks a broad 120-grain wall within 600 ticks and a spill stopped by cleats
|
||||
across chunk edges, where the source chunk's one-grain barrier flag is clear.
|
||||
- `test/t_blow_collapse.py` continuously blows bagged sand straight above a stationary bot
|
||||
at nozzle radius 6 for 1200 real frame-loop ticks, then checks the pile after 480 idle
|
||||
ticks with actual helper dispatch. Pixel captures are `shots/t93c-before.png` and
|
||||
`shots/t93c-after.png`. `test/t_blow_collapse.js` repeats the recipe inline and with
|
||||
1/2/8 Node helpers, compares material and motion flags, conserves all bag/flying/landed
|
||||
sand, and forces visits to check resting stability. It also directly checks a supported
|
||||
face releasing immediately and a spill stopped by a cleat across a chunk boundary.
|
||||
- `test/t_smooth.py` checks WebGPU smooth shapes, seams, joins, fog and the thermal lens.
|
||||
- `test/t_gpu_golden.py` compares 28 cropped frozen reference PNGs (384x288, under
|
||||
`test/fixtures/golden/`) against WebGPU: renderer, joins, sky and four light scenes,
|
||||
|
||||
@@ -6,6 +6,51 @@ open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
vacuum cut: 1200 toolBlow ticks under open sky. Matching original-code Node and real
|
||||
pixel frame-loop browser controls fail the new regression. Browser, NVIDIA RTX 5070,
|
||||
two active helpers: 79-grain cliff at blow end, still 61 after 480 idle ticks. Active
|
||||
chunks remain; neither sticky rolls (sand already has STICK=1) nor missing wakes explain it.
|
||||
- Cause: landFlyer/depositNear stack grains at or above impact, while supported exposed
|
||||
grains only slide a single diagonal cell. The earlier fan rule requires empty space below
|
||||
and misses the resting face. Simply extending the initial spill leaves the falling layer
|
||||
blocking further release. Tall bodies now release from rest and carry a bounded outward
|
||||
fall through the newly unsupported column; contact below cancels that motion. Cohesive
|
||||
grains retain their existing release and do not inherit it. A continuous 32-grain backing
|
||||
column eight grains inward distinguishes towers from short feeds. Read reach <=24,
|
||||
write reach <=8, no per-grain allocation, existing worldgen switch retained.
|
||||
- New shared nozzle fixture and headless/browser regressions cover the actual tool, direct
|
||||
supported-face release, cross-chunk cleats, conservation, 0/1/2/8 helpers, exact material
|
||||
and motion-flag bytes, and forced-awake stability. Larger equal-mass clay/sand check:
|
||||
7200 grains, final heights 90/75. Rejected broad release and persistent motion on contact
|
||||
experiments after they changed small heaps, clay or transport checks; no old test weakened.
|
||||
- Final headless suite passes. Served pixel browser nozzle check passes at exactly 480 idle
|
||||
ticks: cliff 32 -> 1, all 40000 grains retained in bag/flight/pile, final pile sleeps.
|
||||
Saved and inspected shots/t93c-before.png and shots/t93c-after.png; matching old-rule
|
||||
captures are shots/t93c-control-{before,after}.png. Final full
|
||||
python3 test/run_all.py --browser: ALL PASSED, 35 headless and 10 browser suites,
|
||||
including nozzle 25, collapse 17, vacuum 44, threads 12, gameplay 361, both browser
|
||||
collapse regressions, original worldgen hashes and liquids. Log: /tmp/t93c-all-browser.log.
|
||||
Ticked round-3 feedback with a report and moved t93 to Review. Owner playtesting
|
||||
remains open. No commit.
|
||||
- Isolated real-GPU requested profile, no concurrent tests: original-code control -> final
|
||||
total ms/tick: settled 1.9359 -> 1.9413 (+0.3%), heat 2.6679 -> 2.7080 (+1.5%).
|
||||
Versus historical tools/perf-after-t88.json: +7.5%/+3.2%; the control already measures
|
||||
+7.2%/+1.7%. Awake counts match (425/494). Movement ms/tick 0.9520 -> 0.9570 and
|
||||
1.3460 -> 1.3850; unchanged heat work 0.9060 -> 0.9067 and 1.2425 -> 1.2424.
|
||||
Logs/JSON: /tmp/t93c-perf-{control,final}.{log,json}. Some real-loop frame-tail timings
|
||||
are noisier and exceed the profiler's 10% marker; the requested isolated simulation
|
||||
tick averages are within a few percent of the matching control.
|
||||
- Files: src/sim/cells.js, src/sim/material-tables.js, test/pile-blow.js,
|
||||
test/t_blow_collapse.{js,py}, test/run_all.py, docs/DESIGN.md, docs/TESTING.md,
|
||||
regenerated docs/PROJECT_MAP.md, t93 task and this log. Further evidence:
|
||||
/tmp/t93c-red.log, /tmp/t93c-browser-red.log, /tmp/t93c-browser-final.log,
|
||||
/tmp/t93c-final-headless2.log.
|
||||
|
||||
|
||||
## 2026-10-28: t93 reopened, vacuum-cut faces drain in columns (Codex)
|
||||
|
||||
- Read AGENTS, the t93 task/log and 807217b; inspected both new owner screenshots.
|
||||
|
||||
+68
-19
@@ -78,9 +78,75 @@ 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.
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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.
|
||||
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;
|
||||
flags[i] |= fl;
|
||||
move(i, i + dx * reach + W * drop, x, y, x + dx * reach, y + drop);
|
||||
}
|
||||
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
||||
if (y >= H - 1) return;
|
||||
const b = i + W;
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Tall free-flowing bodies topple from rest; short feeds and cohesive grains keep their 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.
|
||||
const insideX = x - dx * 8;
|
||||
let depth = 0;
|
||||
if (insideX >= 0 && insideX < W) {
|
||||
const inside = i - dx * 8;
|
||||
let up = 0;
|
||||
while (up < 24 && y >= up && PH[mat[inside - W * up]] === PH_POWDER) up++;
|
||||
let down = 1;
|
||||
while (down <= 24 && up + down <= 32 && y + down < H && PH[mat[inside + W * down]] === PH_POWDER) down++;
|
||||
depth = up + down - 1;
|
||||
}
|
||||
if (depth >= 32 && powderSpill(i, x, y, m, dx, 8, true)) return;
|
||||
}
|
||||
}
|
||||
if ((!barriers || (!cleatMask[i] && !cleatMask[b])) && enterFree(m, PH_POWDER, b, 0, 1)) {
|
||||
// A falling face fans outward; dropping straight down keeps a packed column in its way.
|
||||
if (POWDER_FAN && mat[b] === EMPTY && !part[i] && !(flags[i] & F_BURN) && x > 0 && x < W - 1) {
|
||||
@@ -91,25 +157,8 @@ function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
||||
// Deep faces spill across the falling column, within the eight-grain worker halo.
|
||||
let depth = 0;
|
||||
while (depth < 12 && y >= depth && PH[mat[i - dx - W * depth]] === PH_POWDER) depth++;
|
||||
let reach = 0;
|
||||
for (let step = 1; step <= (depth === 12 && stamp[b - dx] !== tickStamp ? 8 : 1); 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;
|
||||
}
|
||||
if (reach) {
|
||||
let drop = 1;
|
||||
if (depth === 12 && stamp[b - dx] !== tickStamp) {
|
||||
for (let step = 2; step <= 8 && 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;
|
||||
}
|
||||
}
|
||||
move(i, i + W * drop + dx * reach, x, y, x + dx * reach, y + drop); return;
|
||||
}
|
||||
const reach = depth === 12 && stamp[b - dx] !== tickStamp ? 8 : 1;
|
||||
if (powderSpill(i, x, y, m, dx, reach, false)) return;
|
||||
}
|
||||
}
|
||||
move(i, b, x, y, x, y + 1); return;
|
||||
|
||||
@@ -28,6 +28,7 @@ const SOLIDMETAL = new Uint8Array(NMAT); for (const m of [COPPER, IRON, STEEL, T
|
||||
|
||||
// ---------- Flags & parts ----------
|
||||
const F_NAT = 1, F_CAST = 2, F_BURN = 4;
|
||||
const F_AVALANCHE = 8, F_AVALANCHE_R = 16; // transient outward fall, cleared on contact
|
||||
const P_NONE = 0, P_SCREW_U = 1, P_SCREW_D = 2, P_SCREW_L = 3, P_SCREW_R = 4, P_GRATE = 5, P_GOPEN = 6, P_WHEEL = 7, P_INTAKE = 8, P_FILTER = 9, P_PIPE = 10, P_SCREW_UR = 11, P_SCREW_DR = 12, P_SCREW_DL = 13, P_SCREW_UL = 14;
|
||||
const SCREW_PARTS = [P_SCREW_U, P_SCREW_R, P_SCREW_D, P_SCREW_L, P_SCREW_UR, P_SCREW_DR, P_SCREW_DL, P_SCREW_UL];
|
||||
const SCREW_DIRS = [[0, -1], [1, 0], [0, 1], [-1, 0], [1, -1], [1, 1], [-1, 1], [-1, -1]];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Shared reproduction: continuously blow plain bagged sand straight above a stationary bot.
|
||||
function pileBlowSetup() {
|
||||
pileCutWorld();
|
||||
for (let x=0;x<W;x++) { surfY[x]=480; dirtY[x]=490; }
|
||||
computeAmbient();
|
||||
for(let y=0;y<H;y++)for(let x=0;x<W;x++) {
|
||||
const i=y*W+x;mat[i]=y>=480?STONE:EMPTY;flags[i]=y>=480?F_NAT:0;temp[i]=AMB[y];
|
||||
}
|
||||
bot.x=384-bot.w/2;bot.y=330-bot.h/2;bot.vx=bot.vy=0;
|
||||
bag[SAND]=40000;bagTotal=40000;updateRoof();
|
||||
}
|
||||
function pileBlowStroke() { toolBlow(383.5,250,6,SAND); }
|
||||
function pileBlowProfile() {
|
||||
let cliff=0,previous=0,grains=0,height=0;
|
||||
for(let x=0;x<W;x++) {
|
||||
let y=0;while(y<480&&mat[y*W+x]!==SAND)y++;
|
||||
const h=480-y;cliff=Math.max(cliff,Math.abs(h-previous));previous=h;height=Math.max(height,h);
|
||||
for(;y<480;y++)if(mat[y*W+x]===SAND)grains++;
|
||||
}
|
||||
return {cliff,height,grains,flying:flyers.length,bag:bagTotal,awake:awake.filter(v=>v).length};
|
||||
}
|
||||
+1
-1
@@ -45,7 +45,7 @@ def main():
|
||||
if browser:
|
||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
||||
# one at a time: two headless browsers at once can starve each other and flake
|
||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
|
||||
for name in ['play.py', 't_smooth.py', 't_dev_console.py', 't_caps.py', 't_gpu.py', 't_vacuum_collapse.py', 't_blow_collapse.py', 't_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
|
||||
results.append(run_py(name))
|
||||
bad = 0
|
||||
for name, code, out, secs in results:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Owner's upward nozzle recipe, inline and with real shared-memory helpers.
|
||||
const fs = require('fs');
|
||||
const {createHash} = require('crypto');
|
||||
const {load, suite, enableThreads} = require('./harness');
|
||||
const {run} = load(), {check, done} = suite();
|
||||
for (const file of ['pile-cut', 'pile-blow']) run(fs.readFileSync(require.resolve('./'+file+'.js'), 'utf8'));
|
||||
const hash = () => {const h=createHash('sha256');for(const a of run('[mat,flags,life]'))h.update(a);return h.digest('hex');};
|
||||
// A supported grain must release immediately, without relying on gravity underneath it.
|
||||
run(`pileBlowSetup();for(let y=270;y<340;y++)for(let x=356;x<376;x++)setCell(y*W+x,SAND,20,0);
|
||||
powder(300*W+356,356,300,SAND);`);
|
||||
check('a resting tall face topples promptly',run('mat[300*W+356]===EMPTY && mat[308*W+348]===SAND && (flags[299*W+356]&F_AVALANCHE)'));
|
||||
run(`pileBlowSetup();for(let y=270;y<340;y++)for(let x=356;x<376;x++)setCell(y*W+x,SAND,20,0);
|
||||
cleatMask[300*W+352]=1;powder(300*W+356,356,300,SAND);`);
|
||||
check('resting face spill stops at a cross-chunk cleat',run('mat[308*W+353]===SAND && mat[308*W+348]===EMPTY'));
|
||||
(async () => {
|
||||
let expected;
|
||||
for (const workers of [0, 1, 2, 8]) {
|
||||
run('pileBlowSetup()');
|
||||
const stop = workers ? await enableThreads(run, workers) : null;
|
||||
try {
|
||||
run('for(let k=0;k<1200;k++){pileBlowStroke();simTick();}');
|
||||
const before = run('pileBlowProfile()');
|
||||
check(`fast upward nozzle builds a large pile (${workers} helpers)`, before.grains > 15000 && before.height > 100, JSON.stringify(before));
|
||||
run('bot.x=700;bot.y=100;for(let k=0;k<480;k++)simTick();');
|
||||
const after = run('pileBlowProfile()');
|
||||
check(`blown sand slumps within eight seconds (${workers} helpers)`, after.cliff <= 2, JSON.stringify(after));
|
||||
check('all blown sand is retained', after.grains + after.flying + after.bag === 40000);
|
||||
if (workers) {
|
||||
check('helpers actually process the blown pile', run('simPool.activeMax') > 0);
|
||||
check('blown collapse matches inline material and motion flags', hash() === expected);
|
||||
} else expected = hash();
|
||||
run('for(let k=0;k<1200;k++){awake.fill(3);simTick();}');
|
||||
const resting = hash();
|
||||
run('for(let k=0;k<600;k++){awake.fill(3);simTick();}for(let k=0;k<3;k++)simTick();');
|
||||
check('settled blown sand stays put and sleeps', hash() === resting && run('awake.every(v=>!v)'));
|
||||
} finally { if(stop) await stop(); }
|
||||
}
|
||||
const heights = [];
|
||||
for (const material of ['SAND','LCLAY']) {
|
||||
run(`pileBlowSetup();bag.fill(0);bagTotal=0;bot.x=700;
|
||||
for(let y=360;y<480;y++)for(let x=350;x<410;x++)setCell(y*W+x,${material},20,0);
|
||||
for(let k=0;k<1800;k++){awake.fill(3);simTick();}`);
|
||||
heights.push(run(`(()=>{let height=0;for(let x=0;x<W;x++){let y=0;
|
||||
while(y<480&&mat[y*W+x]!==${material})y++;height=Math.max(height,480-y);}return height;})()`));
|
||||
}
|
||||
check('large clay heaps remain steeper than equal sand heaps',heights[1]>heights[0],JSON.stringify(heights));
|
||||
done();
|
||||
})().catch(e => { console.error(e); process.exitCode = 1; });
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Straight-up bagged sand nozzle in the real pixel frame loop."""
|
||||
import pathlib
|
||||
import sys
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / 'tools'))
|
||||
from use_venv import ensure_playwright; ensure_playwright()
|
||||
from browser import launch, consume_gpu_option, capabilities
|
||||
consume_gpu_option()
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
with launch(viewport={'width': 1440, 'height': 900}) as (_, page, url):
|
||||
errors = []
|
||||
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||
page.goto(url)
|
||||
print('GPU', capabilities(page), flush=True)
|
||||
page.evaluate("""() => {
|
||||
const enter=enterGame;enterGame=()=>{enter();GAME.paused=true;};
|
||||
SET.workerCap=2;SET.maxSteps=1;SET.fog=false;SET.lighting='off';SET.cameraLead=false;SET.autosave=0;
|
||||
startNewWorld(7,768,600);
|
||||
}""")
|
||||
page.wait_for_function('GAME.running', timeout=60000)
|
||||
for file in ('pile-cut', 'pile-blow'):
|
||||
page.evaluate((ROOT / f'test/{file}.js').read_text())
|
||||
page.evaluate("""() => {
|
||||
pileBlowSetup();renderDirty.fill(1);GPU.world=null;SM.surf=-1;
|
||||
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());
|
||||
$('hud').hidden=true;GAME.hidePreviews=true;SET.zoom=3;resizeRender();
|
||||
snapCamera();updateCamera=()=>{};
|
||||
}""")
|
||||
page.evaluate('startThreads()')
|
||||
page.evaluate("""() => {
|
||||
simPool.force=true;window.blowTicks=0;window.blowEnd=0;
|
||||
const tool=toolTick;toolTick=()=>{tool();if(blowTicks<1200){pileBlowStroke();blowTicks++;}};
|
||||
const advance=simTick;simTick=()=>{
|
||||
advance();
|
||||
if(blowTicks===1200 && !blowEnd){blowEnd=tick;window.blowBefore=pileBlowProfile();GAME.paused=true;}
|
||||
else if(blowEnd && tick===blowEnd+480){window.blowAfter=pileBlowProfile();GAME.paused=true;}
|
||||
};
|
||||
GAME.paused=false;
|
||||
}""")
|
||||
page.wait_for_function('window.blowBefore', timeout=120000)
|
||||
print('before', page.evaluate('blowBefore'), flush=True)
|
||||
page.screenshot(path=str(ROOT / 'shots/t93c-before.png'))
|
||||
page.evaluate('() => {bot.x=700;bot.y=100;GAME.paused=false;}')
|
||||
page.wait_for_function('window.blowAfter', timeout=60000)
|
||||
result=page.evaluate('({before:blowBefore,after:blowAfter,helpers:simPool.activeMax})')
|
||||
page.screenshot(path=str(ROOT / 'shots/t93c-after.png'))
|
||||
print('result', result, flush=True)
|
||||
assert result['before']['grains']>15000 and result['before']['height']>100, result
|
||||
assert result['after']['cliff']<=2 and result['helpers']>0, result
|
||||
assert sum(result['after'][k] for k in ('grains','flying','bag'))==40000, result
|
||||
assert not errors, errors
|
||||
print('PASS upward nozzle pile collapses within 480 real-loop ticks with active helpers')
|
||||
Reference in New Issue
Block a user