Faces cut into loose piles collapse outward instead of draining column by column (t93)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -555,6 +555,21 @@ above, including:
|
|||||||
keeps its chunk awake to retry. Burning fuel beds keep their tuned settling behavior.
|
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.
|
Grain stamps, sealed corners, transport barriers and per-chunk random streams still apply.
|
||||||
|
|
||||||
|
|
||||||
|
- **Vacuum-cut loose faces spread while falling (t93 reopened, 2026-10-28).** The owner
|
||||||
|
reports that stone-only and mixed faces still stand after vacuuming into settled piles.
|
||||||
|
Sticky-step retries alone do not address the descending column formed by always choosing
|
||||||
|
straight down first. A falling grain on an exposed vertical face, backed by supported
|
||||||
|
powder, prefers an open outward diagonal. With at least 12 grains of backing above it
|
||||||
|
and unmoved support below that backing, it spills across up to eight clear grains and
|
||||||
|
falls up to eight clear grains. This is a local pressure approximation: the falling
|
||||||
|
outer column must drain quickly enough to let the body of the pile release. Shallow
|
||||||
|
faces keep the one-grain step, preserving small heaps and conveyor feeds. Check the
|
||||||
|
whole horizontal/downward path for occupancy, parts and cleats, including other chunks.
|
||||||
|
Keep one stamped move per tick and the existing eight-grain worker write halo.
|
||||||
|
Resting heaps do not enter this path; world generation keeps its recorded settling rules
|
||||||
|
and seed layouts.
|
||||||
|
|
||||||
## Ideas not built yet
|
## Ideas not built yet
|
||||||
|
|
||||||
- **Concrete**, as a stronger wall material from rubble.
|
- **Concrete**, as a stronger wall material from rubble.
|
||||||
|
|||||||
+5
-2
@@ -254,10 +254,10 @@ Exploration only: never changes grains or the random stream.
|
|||||||
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
- functions: fogAlloc, fogMark, fogSeen, fogInit, fogLook, fogRevealAround, fogRevealAll
|
||||||
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
- variables: FOG, FOG_SENSE, FOG_SENSE_FADE, fog, FW, FH
|
||||||
|
|
||||||
### src/sim/cells.js (240 lines)
|
### src/sim/cells.js (272 lines)
|
||||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
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, powder, passable, liquid, liquidSearch, gas, wakeBotPowder, simMove, moveChunk
|
||||||
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL
|
- variables: SORTP, SORTP_FUEL, LSTEP, LFILM, CORNER_SEAL, POWDER_FAN
|
||||||
|
|
||||||
### src/sim/heat.js (266 lines)
|
### src/sim/heat.js (266 lines)
|
||||||
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
Heat: conduction, melting and freezing, reactions, fire and air, steel.
|
||||||
@@ -630,6 +630,7 @@ 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/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-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/light-readback.js`: Read float source channels, including transmittance, from the WGSL light pass.
|
||||||
|
- `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/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/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_bar_stacking.js`: Resting bars snap into pairs of grains and tip off narrow supports.
|
||||||
@@ -668,6 +669,8 @@ The frame loop and boot.
|
|||||||
- `test/t_smooth.py`: The smooth look: the WebGPU pass compiles and draws, and the thermal lens still works.
|
- `test/t_smooth.py`: The smooth look: the WebGPU pass compiles and draws, and the thermal lens still works.
|
||||||
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
- `test/t_stone.js`: Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and builders' orders.
|
||||||
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
- `test/t_threads.js`: Shared allocation, checkerboard independence, and real worker-count determinism.
|
||||||
|
- `test/t_vacuum_collapse.js`: Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
||||||
|
- `test/t_vacuum_collapse.py`: Vacuum cuts in the real pixel frame loop with active browser workers and hardware graphics.
|
||||||
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
- `test/t_water.js`: Water conservation, settling, ledge pouring, boundaries, pipes and phase changes. Tall worlds keep geothermal heat out of the ponds.
|
||||||
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
|
- `test/t_weather.js`: Weather: no more springs; steam rises into clouds and rains back; showers; snow when it is freezing.
|
||||||
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
|
- `test/t_world.js`: Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag refunds, drill reach, ballistic blowing, snowy mountains.
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ checks waking within eight ticks separately from the probabilistic consumption o
|
|||||||
gamepad and world sizes. Screenshots go to
|
gamepad and world sizes. Screenshots go to
|
||||||
`shots/play/`. Pixel look by default; `python3 test/play.py shots/play smooth` uses the smooth
|
`shots/play/`. Pixel look by default; `python3 test/play.py shots/play smooth` uses the smooth
|
||||||
look.
|
look.
|
||||||
|
- `test/t_vacuum_collapse.py` cuts settled stone and mixed heaps with one normal-width
|
||||||
|
vacuum stroke per simulation tick in the real frame loop. It uses the shared
|
||||||
|
`test/pile-cut.js` fixture, requires actual helper dispatch (forced for this small world),
|
||||||
|
checks both faces within 180 ticks, and saves time-series pixel screenshots in `shots/`.
|
||||||
|
`test/t_vacuum_collapse.js` uses the same cuts inline and with Node helpers, including
|
||||||
|
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_smooth.py` checks WebGPU smooth shapes, seams, joins, fog and the thermal lens.
|
- `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/t_gpu_golden.py` compares 28 cropped frozen reference PNGs (384x288, under
|
||||||
`test/fixtures/golden/`) against WebGPU: renderer, joins, sky and four light scenes,
|
`test/fixtures/golden/`) against WebGPU: renderer, joins, sky and four light scenes,
|
||||||
|
|||||||
@@ -5,6 +5,59 @@ 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.
|
open or not verified. Keep each entry short; the git history has the details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
The remaining reproduced cause is straight-down movement taking priority over outward
|
||||||
|
fall: an exposed edge stays a descending column, blocking the next column's diagonal
|
||||||
|
exits. This is a movement/scan-order bottleneck, not lost wakes in this reproduction.
|
||||||
|
`setCell` wakes the cut; worker wakes merge each checkerboard pass. A radius-6 vacuum,
|
||||||
|
one call per tick into a settled 60-grain RSTONE heap, leaves a 39-grain cliff; after
|
||||||
|
120 ticks it is still 18, with exactly the same series when all chunks are forced awake.
|
||||||
|
RSTONE already has STICK=1. Inline and actual Node helpers agree, bot near or far.
|
||||||
|
- Added outward downward movement only for a falling exposed vertical face backed by
|
||||||
|
supported powder. Retain one downward step per tick, occupancy, parts, barriers and
|
||||||
|
burning-grain guard. Resting heaps do not enter this path. `POWDER_FAN` goes to workers
|
||||||
|
and is disabled during the existing legacy worldgen settle to preserve all seed hashes.
|
||||||
|
Rejected a horizontal-step experiment (forced-awake creep) and a broad falling-edge
|
||||||
|
rule (spread free-falling blocks too far); no old test expectation was changed.
|
||||||
|
- New shared `test/pile-cut.js`: two 60-grain triangular heaps, 600 settling ticks,
|
||||||
|
stone and stone/copper ore/iron ore/dirt, then actual radius-6 vacuum strokes.
|
||||||
|
New Node suite passes 40 checks: cuts inline/2 helpers, near/far bot, conservation,
|
||||||
|
exact inline/helper material bytes, initially stable heaps and 600 forced-awake
|
||||||
|
stable ticks afterward. Browser suite uses isolated HTTP, real NVIDIA Blackwell GPU,
|
||||||
|
real frame loop and two helpers with confirmed dispatch (small fixture forces dispatch).
|
||||||
|
Verdict is recorded at exactly tick 180. Control with face spreading disabled:
|
||||||
|
cliff stone/mixed 33/32 at cut end, 14/13 at 120, 8/5 at 180; browser assertion fails.
|
||||||
|
Fixed browser reaches <=2 by 120 and passes at 180. Earlier Node red run fails eight
|
||||||
|
collapse checks (its mixed fixture included clay); existing clay collapse checks pass.
|
||||||
|
- Full `python3 test/run_all.py --browser`: ALL PASSED, 34 headless + 9 browser suites,
|
||||||
|
including collapse 17, threads 12, vacuum 40, gameplay 361 and browser vacuum 1.
|
||||||
|
Log: `/tmp/t93b-all-browser.log`. No existing test was weakened. Browser regression
|
||||||
|
is included in the suite runner. Original seed hashes and liquid-sinking checks pass.
|
||||||
|
- Saved and inspected `shots/t93b-before.png` (control at cut end) and
|
||||||
|
`shots/t93b-after.png` (fixed after 180 ticks): vertical cut faces become slopes.
|
||||||
|
Fixed series: `shots/t93b-time-{0,60,120,180}.png`; matching control series:
|
||||||
|
`shots/t93b-control-time-{0,60,120,180}.png`. Browser control log:
|
||||||
|
`/tmp/t93b-browser-matching-control.log`. Small initial browser passes stayed inline
|
||||||
|
despite a pool existing; final regression requires actual `simPool.activeMax > 0`.
|
||||||
|
- Requested idle profile, real GPU, no concurrent tests:
|
||||||
|
`python3 tools/perf.py --scenes settled,heat --looks pixel --zooms 6 --compare tools/perf-after-t88.json`.
|
||||||
|
Rule-disabled control -> final total ms/tick: settled 1.9855 -> 2.0226 (+1.9%),
|
||||||
|
heat 2.7027 -> 2.7734 (+2.6%). Against historical t88: +12.0%/+5.7%.
|
||||||
|
Movement ms/tick 0.9639 -> 0.9818 / 1.3481 -> 1.3885; unchanged heat work also
|
||||||
|
varies 0.9412 -> 0.9593 / 1.2724 -> 1.3019. End awake counts identical, 425/494.
|
||||||
|
These timings show small run-to-run differences, not exact equality; historical settled
|
||||||
|
time remains over the profiler's 10% comparison threshold. Evidence:
|
||||||
|
`/tmp/t93b-idle-control.json`, `/tmp/t93b-idle-final.json`, `/tmp/t93b-idle-summary.log`.
|
||||||
|
An earlier control profile overlapped headless checks and was excluded from this comparison.
|
||||||
|
- Files: `src/sim/cells.js`, `src/sim/workers.js`, `src/sim/worldgen.js`,
|
||||||
|
`test/pile-cut.js`, `test/t_vacuum_collapse.js`, `test/t_vacuum_collapse.py`,
|
||||||
|
`test/run_all.py`, `docs/DESIGN.md`, `docs/TESTING.md`, generated project map,
|
||||||
|
t93 task and this log. No commit. Owner playtesting of their larger worlds remains open.
|
||||||
|
|
||||||
|
|
||||||
## 2026-10-28: t92 keep pass-through, restore original loose-grain behaviour (Codex)
|
## 2026-10-28: t92 keep pass-through, restore original loose-grain behaviour (Codex)
|
||||||
|
|
||||||
- Read the 7293787 then dadf62a t92 history and original drill in 90f2d3f. Restored the
|
- Read the 7293787 then dadf62a t92 history and original drill in 90f2d3f. Restored the
|
||||||
|
|||||||
+34
-2
@@ -77,11 +77,43 @@ const SORTP_FUEL = 0.003;
|
|||||||
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
|
// times as far for one (a two-times range strands the far end of a 200-grain-wide ledge).
|
||||||
// World generation settles with both at 1 and corner sealing off, so seeds keep the same world.
|
// World generation settles with both at 1 and corner sealing off, so seeds keep the same world.
|
||||||
let LSTEP = 8, LFILM = 4;
|
let LSTEP = 8, LFILM = 4;
|
||||||
let CORNER_SEAL = true;
|
let CORNER_SEAL = true, POWDER_FAN = true;
|
||||||
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
function powder(i, x, y, m, barriers = cleatChunks[(y >> 5) * CW + (x >> 5)]) {
|
||||||
if (y >= H - 1) return;
|
if (y >= H - 1) return;
|
||||||
const b = i + W;
|
const b = i + W;
|
||||||
if ((!barriers || (!cleatMask[i] && !cleatMask[b])) && enterFree(m, PH_POWDER, b, 0, 1)) { move(i, b, x, y, x, y + 1); 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) {
|
||||||
|
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 && y > 0 && PH[mat[i - W]] === PH_POWDER && mat[i - W + dx] === EMPTY &&
|
||||||
|
PH[mat[b - dx]] === PH_POWDER) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
move(i, b, x, y, x, y + 1); return;
|
||||||
|
}
|
||||||
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
|
// a grain at least twice as dense works its way down through a loose pile. Unlit fuel
|
||||||
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
// packs too tight for that, so a mixed furnace charge stays mixed until the fire gets in.
|
||||||
const t = mat[b];
|
const t = mat[b];
|
||||||
|
|||||||
+2
-2
@@ -86,11 +86,11 @@ function chunkPasses(heat) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function threadParams() {
|
function threadParams() {
|
||||||
return { tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,
|
return { tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,POWDER_FAN,
|
||||||
seen, bot: {...bot}, skyY: weather.skyY, barriers: !!cleatCells.length, filterCfg };
|
seen, bot: {...bot}, skyY: weather.skyY, barriers: !!cleatCells.length, filterCfg };
|
||||||
}
|
}
|
||||||
function threadSetParams(s) {
|
function threadSetParams(s) {
|
||||||
({tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,filterCfg} = s);
|
({tick,tickStamp,worldSeed,LSTEP,LFILM,SORTP,CORNER_SEAL,POWDER_FAN,filterCfg} = s);
|
||||||
seen.set(s.seen); Object.assign(bot, s.bot);
|
seen.set(s.seen); Object.assign(bot, s.bot);
|
||||||
weather.skyY = s.skyY; cleatCells = s.barriers ? [1] : [];
|
weather.skyY = s.skyY; cleatCells = s.barriers ? [1] : [];
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -245,7 +245,7 @@ function* worldSteps(seedIn, w, h) {
|
|||||||
yield [0.75, 'Settling loose ground'];
|
yield [0.75, 'Settling loose ground'];
|
||||||
// settle loose material at the water and sorting speeds the seeds were made with
|
// settle loose material at the water and sorting speeds the seeds were made with
|
||||||
awake.fill(3);
|
awake.fill(3);
|
||||||
const lstep = LSTEP, lfilm = LFILM, sortp = SORTP, cornerSeal = CORNER_SEAL; LSTEP = LFILM = 1; SORTP = 0.003; CORNER_SEAL = false;
|
const lstep = LSTEP, lfilm = LFILM, sortp = SORTP, cornerSeal = CORNER_SEAL, powderFan = POWDER_FAN; LSTEP = LFILM = 1; SORTP = 0.003; CORNER_SEAL = POWDER_FAN = false;
|
||||||
for (let k = 0; k < 260; k++) {
|
for (let k = 0; k < 260; k++) {
|
||||||
tick++; tickStamp = (tickStamp % 255) + 1;
|
tick++; tickStamp = (tickStamp % 255) + 1;
|
||||||
simMove();
|
simMove();
|
||||||
@@ -253,7 +253,7 @@ function* worldSteps(seedIn, w, h) {
|
|||||||
if (!any) break;
|
if (!any) break;
|
||||||
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
if ((k & 31) === 0) yield [0.75 + 0.24 * k / 260, 'Settling loose ground'];
|
||||||
}
|
}
|
||||||
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal;
|
LSTEP = lstep; LFILM = lfilm; SORTP = sortp; CORNER_SEAL = cornerSeal; POWDER_FAN = powderFan;
|
||||||
yield [0.99, 'Finishing'];
|
yield [0.99, 'Finishing'];
|
||||||
// re-ambient after settling, then start clean
|
// re-ambient after settling, then start clean
|
||||||
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
for (let i = 0; i < N; i++) { const m = mat[i]; if (m !== MAGMA) temp[i] = AMB[(i / w) | 0]; life[i] = PH[m] === PH_LIQUID && FRZ[m] ? 255 : 0; }
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Shared Node/browser reproduction: vacuum the foot of settled 60-grain heaps, one stroke per tick.
|
||||||
|
function pileCutWorld() {
|
||||||
|
alloc(768, 600); worldSeed = 7; RS = 123;
|
||||||
|
for (let x = 0; x < W; x++) { surfY[x] = 224; dirtY[x] = 234; graniteY[x] = basaltY[x] = magmaY[x] = H; }
|
||||||
|
computeAmbient(); fillNoise();
|
||||||
|
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
|
||||||
|
const i = y * W + x; mat[i] = y >= 224 ? STONE : EMPTY;
|
||||||
|
flags[i] = y >= 224 ? F_NAT : 0; temp[i] = AMB[y];
|
||||||
|
}
|
||||||
|
bag.fill(0); bagTotal = 0; player.tankTier = 3;
|
||||||
|
resetWeather(); weather.next = 1e15; updateRoof();
|
||||||
|
}
|
||||||
|
function pileCutSetup(peak, mixed) {
|
||||||
|
const rubble = [RSTONE, CUORE, FEORE, LDIRT];
|
||||||
|
for (let y = 164; y < 224; y++) for (let x = peak - (y - 164); x <= peak + (y - 164); x++)
|
||||||
|
setCell(y * W + x, mixed ? rubble[(x + y) % rubble.length] : RSTONE, AMB[y], 0);
|
||||||
|
}
|
||||||
|
function pileCutStroke(peak) {
|
||||||
|
bot.x = peak - 50; bot.y = 170; bot.vx = bot.vy = 0;
|
||||||
|
for (let y = 223; y >= 164; y--) for (let x = peak - 100; x < peak; x++) {
|
||||||
|
if (PH[mat[y * W + x]] !== PH_POWDER) continue;
|
||||||
|
return toolVacuum(Math.min(peak - 7, x), y - 3, 6, () => true).got;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
function pileCutProfile(peak) {
|
||||||
|
let cliff = 0, previous = 0, grains = 0;
|
||||||
|
for (let x = peak - 110; x <= peak + 110; x++) {
|
||||||
|
let y = 0; while (y < 224 && PH[mat[y * W + x]] !== PH_POWDER) y++;
|
||||||
|
const height = 224 - y; cliff = Math.max(cliff, Math.abs(height - previous)); previous = height;
|
||||||
|
for (; y < 224; y++) if (PH[mat[y * W + x]] === PH_POWDER) grains++;
|
||||||
|
}
|
||||||
|
return {cliff, grains};
|
||||||
|
}
|
||||||
+1
-1
@@ -45,7 +45,7 @@ def main():
|
|||||||
if browser:
|
if browser:
|
||||||
subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, check=True)
|
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
|
# 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_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_gpu_golden.py', 't_gpu_light.py', 'firefox_smoke.py']:
|
||||||
results.append(run_py(name))
|
results.append(run_py(name))
|
||||||
bad = 0
|
bad = 0
|
||||||
for name, code, out, secs in results:
|
for name, code, out, secs in results:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
// Real nozzle cuts into settled heaps, inline and through the real Node worker pool.
|
||||||
|
const fs = require('fs');
|
||||||
|
const {createHash} = require('crypto');
|
||||||
|
const {load, suite, enableThreads, flatWorld} = require('./harness');
|
||||||
|
const {run} = load(), {check, done} = suite();
|
||||||
|
run(fs.readFileSync(require.resolve('./pile-cut.js'), 'utf8'));
|
||||||
|
const hash = () => createHash('sha256').update(run('mat')).digest('hex');
|
||||||
|
// The longer spill must check barriers even when its starting chunk has no cleats.
|
||||||
|
flatWorld(run, 96, 160, 80);
|
||||||
|
run(`for(let y=18;y<=31;y++)setCell(y*W+29,RSTONE,AMB[y],0);
|
||||||
|
setCell(29*W+30,RSTONE,AMB[29],0);setCell(30*W+30,RSTONE,AMB[30],0);
|
||||||
|
cleatMask[30*W+34]=1;cleatMask[34*W+33]=1;
|
||||||
|
powder(30*W+30,30,30,RSTONE);`);
|
||||||
|
check('face spill stops before cleats across chunk edges',run('mat[30*W+30]===EMPTY && mat[33*W+33]===RSTONE && !cleatChunks[0]'));
|
||||||
|
(async () => {
|
||||||
|
const expected = new Map();
|
||||||
|
for (const workers of [0, 2]) for (const mixed of [false, true]) for (const near of [false, true]) {
|
||||||
|
run(`pileCutWorld();pileCutSetup(160,${mixed});bot.x=700;bot.y=100;`);
|
||||||
|
const stop = workers ? await enableThreads(run, workers) : null;
|
||||||
|
try {
|
||||||
|
run('for(let k=0;k<600;k++)simTick();');
|
||||||
|
const settled = hash();
|
||||||
|
run('awake.fill(3);for(let k=0;k<60;k++)simTick();');
|
||||||
|
check('uncut heap is settled', hash() === settled);
|
||||||
|
run(`for(let k=0;k<70;k++){pileCutStroke(160);simTick();}bot.x=${near ? 110 : 700};bot.y=170;`);
|
||||||
|
const before = run('pileCutProfile(160)');
|
||||||
|
check('vacuum removes grains without losing them', run('bagTotal') > 500 && before.grains + run('bagTotal') === 3600, JSON.stringify(before));
|
||||||
|
run('for(let k=0;k<180;k++)simTick();');
|
||||||
|
const after = run('pileCutProfile(160)'), label = `${mixed ? 'mixed' : 'stone'}, ${workers} workers, bot ${near ? 'near' : 'far'}`;
|
||||||
|
check(`vacuum face slumps within 180 ticks (${label})`, after.cliff <= 2, `before ${before.cliff}, after ${after.cliff}`);
|
||||||
|
if (workers) check('helpers actually processed chunks', run('simPool.activeMax') > 0);
|
||||||
|
const key = `${mixed}/${near}`;
|
||||||
|
if (!workers) expected.set(key, hash());
|
||||||
|
else check(`vacuum collapse matches inline bytes (${label})`, hash() === expected.get(key));
|
||||||
|
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 cut heap stays put and sleeps (${label})`, hash() === resting && run('awake.every(v=>!v)'));
|
||||||
|
} finally { if (stop) await stop(); }
|
||||||
|
}
|
||||||
|
let wideExpected;
|
||||||
|
for (const workers of [0, 2]) {
|
||||||
|
flatWorld(run, 768, 600, 224);
|
||||||
|
run('bot.x=740;bot.y=100;for(let y=104;y<224;y++)for(let x=256;x<576;x++)setCell(y*W+x,RSTONE,AMB[y],0);');
|
||||||
|
const stop = workers ? await enableThreads(run, workers) : null;
|
||||||
|
try {
|
||||||
|
run('for(let k=0;k<600;k++)simTick();');
|
||||||
|
const result = run(`(()=>{let cliff=0,previous=0,grains=0;
|
||||||
|
for(let x=0;x<W;x++){let y=0;while(y<224&&mat[y*W+x]!==RSTONE)y++;
|
||||||
|
const h=224-y;cliff=Math.max(cliff,Math.abs(h-previous));previous=h;
|
||||||
|
for(;y<224;y++)if(mat[y*W+x]===RSTONE)grains++;}
|
||||||
|
return {cliff,grains};})()`);
|
||||||
|
check(`a broad 120-grain wall also releases (${workers} workers)`, result.cliff<=2 && result.grains===38400, JSON.stringify(result));
|
||||||
|
if (!workers) wideExpected=hash();
|
||||||
|
else check('broad wall release matches inline bytes',hash()===wideExpected);
|
||||||
|
} finally { if (stop) await stop(); }
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
})().catch(e => { console.error(e); process.exitCode = 1; });
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Vacuum cuts in the real pixel frame loop with active browser workers and hardware graphics."""
|
||||||
|
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': 760}) as (_, page, url):
|
||||||
|
errors = []
|
||||||
|
page.on('pageerror', lambda e: errors.append(str(e)))
|
||||||
|
page.goto(url)
|
||||||
|
caps = capabilities(page)
|
||||||
|
assert caps['isolated'] and caps['webgpu'], caps
|
||||||
|
print('GPU', caps, flush=True)
|
||||||
|
page.evaluate("""() => {
|
||||||
|
const enter = enterGame; enterGame = function() { enter(); GAME.paused = true; };
|
||||||
|
SET.workerCap=2; SET.fog=false; SET.lighting='off'; SET.cameraLead=false; SET.autosave=0;
|
||||||
|
startNewWorld(7,768,600);
|
||||||
|
}""")
|
||||||
|
page.wait_for_function('GAME.running', timeout=60000)
|
||||||
|
page.evaluate((ROOT / 'test/pile-cut.js').read_text())
|
||||||
|
page.evaluate("""() => {
|
||||||
|
pileCutWorld(); pileCutSetup(160,false); pileCutSetup(416,true);
|
||||||
|
bot.x=700;bot.y=100;for(let k=0;k<600;k++)simTick();
|
||||||
|
renderDirty.fill(1);GPU.world=null;SM.surf=-1;
|
||||||
|
}""")
|
||||||
|
page.evaluate('startThreads()')
|
||||||
|
page.evaluate("""() => {
|
||||||
|
// Tiny isolated fixtures otherwise run inline below the production dispatch threshold.
|
||||||
|
simPool.force=true;
|
||||||
|
window.cutTicks=0;window.cutEnd=0;
|
||||||
|
const tool=toolTick;toolTick=()=>{
|
||||||
|
tool();
|
||||||
|
if(cutEnd && tick===cutEnd+180) window.cutResult={tick,cutEnd,
|
||||||
|
stone:pileCutProfile(160),mixed:pileCutProfile(416),helpers:simPool.activeMax};
|
||||||
|
if(cutTicks<140){pileCutStroke(cutTicks%2?416:160);cutTicks++;if(cutTicks===140)cutEnd=tick+1;}
|
||||||
|
};
|
||||||
|
document.querySelectorAll('.card,.toast').forEach(e=>e.remove());
|
||||||
|
$('hud').hidden=true;GAME.hidePreviews=true;SET.zoom=3;resizeRender();
|
||||||
|
bot.x=288-bot.w/2;bot.y=190-bot.h/2;snapCamera();
|
||||||
|
// Keep both heaps in view while the bot follows each vacuum stroke.
|
||||||
|
updateCamera=()=>{}; GAME.paused=false;
|
||||||
|
}""")
|
||||||
|
page.wait_for_function('cutEnd>0', timeout=30000)
|
||||||
|
for delay in (0, 60, 120, 180):
|
||||||
|
page.wait_for_function(f'tick>=cutEnd+{delay}', timeout=15000)
|
||||||
|
result = page.evaluate('({tick,cutEnd,stone:pileCutProfile(160),mixed:pileCutProfile(416),helpers:simPool.activeMax})')
|
||||||
|
print('profile', delay, result, flush=True)
|
||||||
|
page.screenshot(path=str(ROOT / f'shots/t93b-time-{delay}.png'))
|
||||||
|
page.wait_for_function('window.cutResult', timeout=15000)
|
||||||
|
result = page.evaluate('cutResult')
|
||||||
|
page.evaluate('GAME.paused=true')
|
||||||
|
page.screenshot(path=str(ROOT / 'shots/t93b-after.png'))
|
||||||
|
assert result['helpers'] > 0, result
|
||||||
|
assert result['stone']['cliff'] <= 2 and result['mixed']['cliff'] <= 2, result
|
||||||
|
assert not errors, errors
|
||||||
|
print('PASS vacuum-cut stone and mixed faces slump within 180 real-loop ticks with browser helpers')
|
||||||
Reference in New Issue
Block a user