Physics test pass for sand, molten metal and gases; stuck gas finishes, steam is not deleted (t21)
- New test/t_physics.js: heaps, sorting, sinking, world edges; molten copper levelling, setting, quenching, slag over copper, cavity filling; smoke, steam, fire, sealed pockets, the top row. - Gas in a sealed pocket kept its area asleep and never faded or condensed: gas() keeps its area awake while it lasts. - A rain drop or blown grain landing on steam deleted the steam's water: it goes to the clouds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -226,7 +226,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 (187 lines)
|
||||
### src/sim/cells.js (189 lines)
|
||||
Cell helpers (setCell, move, enter) and the falling-sand movement of powders, liquids and gases (simMove).
|
||||
- functions: setCell, swapCells, move, enter, powder, passable, liquid, gas, simMove
|
||||
- variables: SORTP, LSTEP, LFILM
|
||||
@@ -293,7 +293,7 @@ Trees (background layer), acorns, saplings, and waking freezing water.
|
||||
- functions: cutTree, treeHeld, findTreeBases, treeSupportTick, woodRoom, treeFall, coldTick, plantTick, setTreeCell, saplingTick, igniteTree
|
||||
- variables: treeBases, saplings, treeDraw
|
||||
|
||||
### src/sim/vacuum.js (166 lines)
|
||||
### src/sim/vacuum.js (172 lines)
|
||||
The vacuum, blowing grains out, grains in flight (flyers), and the flame tool. Material the vacuum would start on at the cursor: a bar, else the nearest loose grain.
|
||||
- functions: vacuumTargetAt, toolVacuum, nozzle, toolBlow, flyOpen, depositNear, bagTemp, landFlyer, flyTick, toolIgnite
|
||||
- variables: blowRR, FLY_G, FLY_Q
|
||||
@@ -472,6 +472,7 @@ The frame loop and boot.
|
||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||
- `test/t_molds.js`: Molds and furnaces stay usable: ingots drop below and keep their stubs, plugs over molds are cleared, spoiled castings can be knocked out, grains never drop through a grate by teleporting, and slag lets the fire breathe.
|
||||
- `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_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_smooth.py`: The smooth look: the WebGL pass compiles, draws, falls back cleanly, and the thermal lens still works.
|
||||
|
||||
@@ -4,6 +4,21 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||
open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
## 2026-10-24: t21 phase 2: physics test pass for sand, molten metal and gases (Claude Code, Codex)
|
||||
|
||||
- `test/t_physics.js` (41 checks): heaps, digging under a heap, ore sorting through ash, sand
|
||||
sinking in a pond, world edges; molten copper levelling (slower than water), setting, sinking in
|
||||
a pond, slag over copper, filling a cavity; smoke rising and fading (longer under a roof), steam
|
||||
condensing, fire going out, gas in sealed pockets, gas leaving at the top row. Every scenario
|
||||
checks that its areas go back to sleep.
|
||||
- Found and fixed: gas stuck in a pocket never finished fading or condensing because its area
|
||||
slept (`gas()` keeps its area awake: steady cost 1.34 -> 1.37 ms/tick); a rain drop or blown
|
||||
grain landing on steam deleted that steam's water (`landFlyer`/`depositNear` in
|
||||
`src/sim/vacuum.js` now send it to the clouds).
|
||||
- Found, owner decided: a resting pile never sorts. To follow: piles near the bot keep sorting,
|
||||
and sorting gets quick.
|
||||
- Tests: `run_all.py --browser` ALL PASSED; seeds 777 and 77 generate the same worlds.
|
||||
|
||||
## 2026-10-24: t21 phase 1: water test pass, faster pouring, rain spreads (Claude Code, Codex)
|
||||
|
||||
- `test/t_water.js` (20 checks): conservation, levelling and sleep, pools pouring off ledges,
|
||||
|
||||
@@ -126,6 +126,8 @@ function liquid(i, x, y, m) {
|
||||
}
|
||||
}
|
||||
function gas(i, x, y, m) {
|
||||
// Even blocked gas must keep aging or condensing until it is gone.
|
||||
awake[(y >> 5) * CW + (x >> 5)] = 3;
|
||||
if (m === FIRE) {
|
||||
const l = life[i];
|
||||
if (l <= 1) { mat[i] = EMPTY; life[i] = 0; wakeXY(x, y); return; }
|
||||
|
||||
+8
-2
@@ -108,7 +108,10 @@ function depositNear(m, T, x, y, skip) {
|
||||
const xx = x + dx, yy = y + dy;
|
||||
if (xx < 0 || yy < 0 || xx >= W || yy >= H) continue;
|
||||
const j = yy * W + xx;
|
||||
if (j !== skip && flyOpen(m, j)) { setCell(j, m, T, 0); return true; }
|
||||
if (j !== skip && flyOpen(m, j)) {
|
||||
if (mat[j] === STEAM) absorbSteam(xx); // displaced steam still belongs to the world's water
|
||||
setCell(j, m, T, 0); return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -121,7 +124,10 @@ function landFlyer(f, x, y) {
|
||||
for (let k = 0; k < 8; k++) {
|
||||
const yy = y - k; if (yy < 0) break;
|
||||
const j = yy * W + x;
|
||||
if (flyOpen(f.m, j)) { setCell(j, f.m, f.T, 0); return; }
|
||||
if (flyOpen(f.m, j)) {
|
||||
if (mat[j] === STEAM) absorbSteam(x); // landing replaces the gas, so keep its water in the clouds
|
||||
setCell(j, f.m, f.T, 0); return;
|
||||
}
|
||||
}
|
||||
if (!depositNear(f.m, f.T, x, y)) { if (f.rain) absorbSteam(x); else bagAdd(f.m, 1, true); } // nowhere to go: back into the bag (rain: back into the clouds)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// 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.
|
||||
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
|
||||
const { run } = load();
|
||||
const { check, done } = suite();
|
||||
const started = Date.now();
|
||||
const awake = () => run('awake.reduce((n, v) => n + (v > 0), 0)');
|
||||
const grains = m => count(run, run(m));
|
||||
function world(w = 320, ground = 120) { run('WS = 12345;'); flatWorld(run, w, 600, ground); run('RS = 12345;'); }
|
||||
// the top grain row of each column in x0..x1 (ground row if none)
|
||||
const tops = (m, x0, x1, ground) => run(`(() => { const a = []; for (let x = ${x0}; x <= ${x1}; x++) { let y = ${ground}; for (let j = 0; j < ${ground}; j++) if (mat[j*W+x] === ${m}) { y = j; break; } a.push(y); } return a; })()`);
|
||||
|
||||
// Sand and powders
|
||||
// sand poured from one point makes a heap with sloping sides, then lies still
|
||||
world();
|
||||
for (let k = 0; k < 400; k++) { run('if (mat[40*W+160] === EMPTY) setCell(40*W+160, SAND, AMB[40], 0);'); ticks(run, 2); }
|
||||
ticks(run, 1800);
|
||||
const heap = tops(run('SAND'), 100, 220, 120), peak = Math.min(...heap), wide = heap.filter(y => y < 120).length;
|
||||
check('poured sand makes a sloping heap that lies still', grains('SAND') === 400 && 120 - peak >= 5 && wide >= 12 && wide <= 80 && awake() === 0,
|
||||
`${grains('SAND')} grains, ${120 - peak} high, ${wide} wide, ${awake()} areas awake`);
|
||||
// digging out the ground under the heap lets it fall in, and none is lost
|
||||
run('for (let y = 120; y < 160; y++) for (let x = 140; x < 180; x++) setCell(y*W+x, EMPTY, AMB[y], 0);');
|
||||
ticks(run, 1800);
|
||||
check('digging under a heap makes it fall into the hole', grains('SAND') === 400 && count(run, run('SAND'), 140, 120, 179, 159) > 300,
|
||||
`${count(run, run('SAND'), 140, 120, 179, 159)} of ${grains('SAND')} in the hole`);
|
||||
asleep('sand after digging');
|
||||
// heavy ore works its way down through light ash, slowly, while the pile is being stirred (a
|
||||
// working furnace bed is always awake); a pile left at rest goes to sleep and stays as it is
|
||||
world();
|
||||
rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE'));
|
||||
rect(run, 152, 112, 169, 119, run('ASH')); rect(run, 152, 108, 169, 111, run('CUORE'));
|
||||
const oreY = (m) => run(`(() => { let s = 0, n = 0; for (let i = 0; i < N; i++) if (mat[i] === ${m}) { s += (i / W) | 0; n++; } return s / n; })()`);
|
||||
const ore0 = oreY(run('CUORE')); ticks(run, 3600); const oreRest = oreY(run('CUORE'));
|
||||
check('a resting pile of ore on ash goes to sleep as it is', Math.abs(oreRest - ore0) < 0.5 && awake() === 0, `ore rows ${ore0.toFixed(1)} -> ${oreRest.toFixed(1)}, ${awake()} awake`);
|
||||
const stir = n => run(`for (let k = 0; k < ${n}; k++) { if (k % 8 === 0) wakeXY(160, 112); simTick(); }`);
|
||||
stir(600); const ore1 = oreY(run('CUORE')); stir(5400); const ore2 = oreY(run('CUORE'));
|
||||
check('stirred, heavy ore sinks down through light ash, slowly', ore1 - oreRest < 2 && ore2 - oreRest > 2 && grains('CUORE') === 72 && grains('ASH') === 144,
|
||||
`ore rows ${oreRest.toFixed(1)} -> ${ore1.toFixed(1)} -> ${ore2.toFixed(1)}`);
|
||||
ticks(run, 1200); asleep('ore after stirring stops');
|
||||
world();
|
||||
rect(run, 150, 100, 151, 119, run('STONE')); rect(run, 170, 100, 171, 119, run('STONE'));
|
||||
rect(run, 152, 112, 169, 119, run('LDIRT')); rect(run, 152, 104, 169, 111, run('SAND'));
|
||||
const sand0 = oreY(run('SAND')); ticks(run, 6000);
|
||||
check('sand on dirt (close in weight) stays where it is', Math.abs(oreY(run('SAND')) - sand0) < 0.5, `sand rows ${sand0.toFixed(1)} -> ${oreY(run('SAND')).toFixed(1)}`);
|
||||
asleep('a pile of similar weights');
|
||||
// sand dropped into a pond sinks, and the water ends up on top
|
||||
world();
|
||||
rect(run, 150, 90, 151, 119, run('STONE')); rect(run, 190, 90, 191, 119, run('STONE'));
|
||||
rect(run, 152, 100, 189, 119, run('WATER')); rect(run, 160, 70, 181, 79, run('SAND'));
|
||||
const pond = run('worldWater()'); ticks(run, 3600);
|
||||
const sandLow = count(run, run('SAND'), 152, 112, 189, 119), waterHigh = count(run, run('WATER'), 152, 90, 189, 111);
|
||||
check('sand dropped in a pond sinks to the bottom, the water ends up on top', grains('SAND') === 220 && run('worldWater()') === pond && sandLow > 200 && waterHigh > 400,
|
||||
`${sandLow} of ${grains('SAND')} sand in the bottom rows, ${waterHigh} water above, water ${pond} -> ${run('worldWater()')}`);
|
||||
asleep('a sandy pond');
|
||||
// powders against the world's sides and on the bottom row neither leak nor wrap
|
||||
world(80);
|
||||
rect(run, 0, 60, 5, 79, run('SAND')); rect(run, 74, 60, 79, 79, run('SAND'));
|
||||
ticks(run, 1200);
|
||||
check('sand piled against the world edges stays in the world', grains('SAND') === 240 && count(run, run('SAND'), 0, 0, 79, 119) === 240, `${grains('SAND')} grains`);
|
||||
|
||||
asleep('sand at the world edges');
|
||||
world(80, 600); run('AMB.fill(20); temp.fill(20);');
|
||||
rect(run, 30, 599, 49, 599, run('SAND')); ticks(run, 600);
|
||||
check('sand on the bottom row neither leaks nor wraps', grains('SAND') === 20 && count(run, run('SAND'), 30, 599, 49, 599) === 20 && awake() === 0,
|
||||
`${grains('SAND')}/20 total, ${count(run, run('SAND'), 30, 599, 49, 599)} still on bottom row, ${awake()} awake`);
|
||||
// Filters already separate a mixed load in t_conveyors; this checks the grate itself.
|
||||
world(80);
|
||||
rect(run, 19, 60, 19, 119, run('STONE')); rect(run, 40, 60, 40, 119, run('STONE'));
|
||||
run('for (let bx = 5; bx < 10; bx++) placeBlock(BP_GRATE, 0, bx, 25, true);');
|
||||
rect(run, 24, 70, 35, 79, run('SAND')); ticks(run, 1200);
|
||||
check('sand stays on a grate without slipping through', grains('SAND') === 120 && count(run, run('SAND'), 20, 104, 39, 119) === 0 && awake() === 0,
|
||||
`${grains('SAND')}/120 sand, ${count(run, run('SAND'), 20, 104, 39, 119)} below, ${awake()} awake`);
|
||||
// Movement alone keeps the liquids molten and steam gaseous while measuring their flow.
|
||||
function motion(n) {
|
||||
run(`for (let k = 0; k < ${n}; k++) { tick++; tickStamp = (tickStamp % 255) + 1; simMove(); for (let c = 0; c < NCH; c++) if (awake[c]) awake[c]--; }`);
|
||||
}
|
||||
function asleep(name) {
|
||||
check(`${name} settles and its areas sleep`, awake() === 0, `${awake()} awake areas`);
|
||||
}
|
||||
|
||||
// Molten metal
|
||||
const levelTimes = [];
|
||||
for (const m of ['WATER', 'MCU']) {
|
||||
world(80);
|
||||
rect(run, 38, 60, 41, 99, run(m));
|
||||
let flatAt = 0;
|
||||
for (let t = 1; t <= 2400; t++) {
|
||||
motion(1);
|
||||
const a = tops(run(m), 0, 79, 120);
|
||||
if (Math.max(...a) - Math.min(...a) <= 2 && Math.max(...a) < 120) { flatAt = t; break; }
|
||||
}
|
||||
levelTimes.push(flatAt); motion(600);
|
||||
const a = tops(run(m), 0, 79, 120), uneven = Math.max(...a) - Math.min(...a);
|
||||
check(`${m === 'MCU' ? 'molten copper' : 'water'} spreads into a level pool without losing grains`, flatAt > 0 && uneven <= 2 && grains(m) === 160 && awake() === 0,
|
||||
`within 2 grains of level after ${flatAt} ticks, final difference ${uneven}, ${grains(m)}/160 grains, ${awake()} awake`);
|
||||
}
|
||||
check('molten copper takes longer than water to level', levelTimes[1] > levelTimes[0] && levelTimes[0] > 0,
|
||||
`water ${levelTimes[0]} ticks, copper ${levelTimes[1]} ticks`);
|
||||
world(80);
|
||||
run('for (let x = 30; x < 50; x++) setCell(119*W+x, MCU, FRZ[MCU] + 1, 0);');
|
||||
const freezePoint = run('FRZ[MCU]');
|
||||
let firstSet = null;
|
||||
for (let t = 0; t < 300 && firstSet === null; t++) {
|
||||
ticks(run, 1);
|
||||
firstSet = run('(() => { const i = mat.indexOf(COPPER); return i < 0 ? null : temp[i]; })()');
|
||||
}
|
||||
ticks(run, 2400);
|
||||
check('copper cools below its setting point and keeps all its metal', firstSet !== null && firstSet < freezePoint && grains('COPPER') === 20 && grains('MCU') === 0 && awake() === 0,
|
||||
`first solid ${firstSet}°C, setting point ${freezePoint}°C, ${grains('COPPER')} solid + ${grains('MCU')} molten /20, ${awake()} awake`);
|
||||
world(80);
|
||||
rect(run, 19, 70, 19, 119, run('STONE')); rect(run, 60, 70, 60, 119, run('STONE'));
|
||||
rect(run, 20, 100, 59, 119, run('WATER'));
|
||||
run('for (let y = 70; y < 80; y++) for (let x = 38; x < 42; x++) setCell(y*W+x, MCU, 1160, 0);');
|
||||
run('WS = -1557203080; weather.wind = 0.3;'); // rain passing through the plume exercises displaced steam
|
||||
const quenchWater = run('worldWater()'); let steamPeak = 0;
|
||||
for (let t = 0; t < 60; t++) { ticks(run, 5); steamPeak = Math.max(steamPeak, grains('STEAM')); }
|
||||
ticks(run, 3600);
|
||||
const bottomCopper = count(run, run('COPPER'), 20, 110, 59, 119);
|
||||
check('copper poured into a pond sinks, sets and makes steam without losses', bottomCopper === 40 && grains('COPPER') + grains('MCU') === 40 && steamPeak > 0 && run('worldWater()') === quenchWater,
|
||||
`${bottomCopper}/40 copper at bottom, ${grains('COPPER')} solid + ${grains('MCU')} molten, peak ${steamPeak} steam, water ${quenchWater} -> ${run('worldWater()')}`);
|
||||
asleep('a quenched copper pond');
|
||||
world(80);
|
||||
rect(run, 29, 70, 29, 119, run('STONE')); rect(run, 50, 70, 50, 119, run('STONE'));
|
||||
run('for (let y = 90; y < 100; y++) for (let x = 30; x < 50; x++) setCell(y*W+x, (x+y)%2 ? MCU : MSLAG, 1300, 0);');
|
||||
motion(2400);
|
||||
const slagDensity = run('DENS[MSLAG]'), copperDensity = run('DENS[MCU]');
|
||||
const upperSlag = count(run, run('MSLAG'), 30, 110, 49, 114), lowerCopper = count(run, run('MCU'), 30, 115, 49, 119);
|
||||
// Local density swaps leave small pockets mixed when the pool sleeps.
|
||||
check('lighter molten slag mostly floats above molten copper', slagDensity < copperDensity && upperSlag >= 95 && lowerCopper >= 95 && oreY(run('MSLAG')) < oreY(run('MCU')) && grains('MSLAG') === 100 && grains('MCU') === 100,
|
||||
`upper slag ${upperSlag}/100, lower copper ${lowerCopper}/100; densities slag ${slagDensity.toFixed(1)}, copper ${copperDensity}; mean rows slag ${oreY(run('MSLAG')).toFixed(1)}, copper ${oreY(run('MCU')).toFixed(1)}, grains ${grains('MSLAG')} + ${grains('MCU')}`);
|
||||
asleep('separated slag and copper');
|
||||
// The other mold suites pre-fill their cavities; here the metal must flow in from above.
|
||||
world(80);
|
||||
run(`addMold('ingot', 8, 27, RSTONE, true);
|
||||
for (let y = 100; y < 108; y++) { setCell(y*W+32, STONE, 20, 0); setCell(y*W+47, STONE, 20, 0); }
|
||||
for (let y = 104; y < 106; y++) for (let x = 33; x < 47; x++) setCell(y*W+x, MCU, 1160, 0);`);
|
||||
motion(1200);
|
||||
const cavity = run('molds[0].cavs[0].length'), filled = run('molds[0].cavs[0].filter(i => mat[i] === MCU).length');
|
||||
check('a copper pour fills every grain of an ingot cavity', filled === cavity && grains('MCU') === 28 && awake() === 0,
|
||||
`${filled}/${cavity} cavity filled, ${grains('MCU')}/28 total, ${awake()} awake`);
|
||||
|
||||
// Landing must keep steam it displaces, for both the direct and nearby fallback paths.
|
||||
for (const nearby of [false, true]) {
|
||||
world(80);
|
||||
rect(run, 39, 90, 39, 90, run('STEAM'));
|
||||
run('flyers.push({ m: WATER, T: 20 });');
|
||||
const before = run('worldWater()');
|
||||
run(`${nearby ? 'depositNear(WATER, 20, 39, 90)' : 'landFlyer(flyers[0], 39, 90)'}; flyers.pop();`);
|
||||
check(`a rain drop landing in steam keeps both grains${nearby ? ' when finding a nearby space' : ''}`, run('worldWater()') === before && grains('WATER') === 1,
|
||||
`water total ${before} -> ${run('worldWater()')}, ${grains('WATER')} water, ${run('weather.moist.reduce((a,b) => a+b, 0)')} in clouds`);
|
||||
ticks(run, 600); asleep('water after landing in steam');
|
||||
}
|
||||
|
||||
// Gases
|
||||
function chamber() {
|
||||
world(80);
|
||||
rect(run, 19, 60, 60, 60, run('STONE'));
|
||||
rect(run, 19, 60, 19, 119, run('STONE')); rect(run, 60, 60, 60, 119, run('STONE'));
|
||||
run('updateRoof();');
|
||||
}
|
||||
function smoke() {
|
||||
run('for (let y = 95; y < 100; y++) for (let x = 37; x < 43; x++) { setCell(y*W+x, SMOKE, AMB[y], 0); life[y*W+x] = 220; }');
|
||||
}
|
||||
const smokeLeft = [];
|
||||
for (const sealed of [false, true]) {
|
||||
if (sealed) chamber(); else world(80);
|
||||
smoke(); ticks(run, 30);
|
||||
check(`smoke rises ${sealed ? 'toward a sealed roof' : 'outdoors'}`, oreY(run('SMOKE')) < 85,
|
||||
`mean row ${oreY(run('SMOKE')).toFixed(1)} from 97, ${grains('SMOKE')}/30 smoke`);
|
||||
ticks(run, 90); smokeLeft.push(grains('SMOKE'));
|
||||
ticks(run, sealed ? 7200 : 600);
|
||||
check(`smoke eventually fades ${sealed ? 'under a roof' : 'outdoors'} and sleeps`, grains('SMOKE') === 0 && awake() === 0,
|
||||
`${grains('SMOKE')} smoke, ${awake()} awake`);
|
||||
}
|
||||
check('a sealed roof holds smoke longer than open air', smokeLeft[1] > smokeLeft[0],
|
||||
`at 120 ticks: outdoors ${smokeLeft[0]}/30, roof ${smokeLeft[1]}/30`);
|
||||
chamber();
|
||||
run('for (let y = 95; y < 100; y++) for (let x = 37; x < 43; x++) setCell(y*W+x, STEAM, 110, 0);');
|
||||
motion(100);
|
||||
const roofSteam = count(run, run('STEAM'), 20, 61, 59, 63), steamTops = tops(run('STEAM'), 20, 59, 120);
|
||||
const steamWidth = steamTops.filter(y => y === 61).length;
|
||||
check('trapped gas spreads along the ceiling without crossing stone', roofSteam >= 25 && steamWidth > 6 && grains('STEAM') === 30 && count(run, run('STEAM'), 20, 61, 59, 119) === 30 && count(run, run('STEAM'), 0, 0, 79, 59) === 0,
|
||||
`${roofSteam}/30 under roof, ${steamWidth} columns along ceiling, ${grains('STEAM')} total, 0 expected above roof: ${count(run, run('STEAM'), 0, 0, 79, 59)}`);
|
||||
run('AMB.fill(20); temp.fill(20); for (let x = 20; x < 60; x++) wakeHeatXY(x, 61);');
|
||||
ticks(run, 1800);
|
||||
check('steam collected under a roof condenses and runs down to the floor', grains('STEAM') === 0 && count(run, run('WATER'), 20, 118, 59, 119) === 30 && awake() === 0,
|
||||
`${grains('STEAM')} steam, ${count(run, run('WATER'), 20, 118, 59, 119)}/30 water at floor, ${awake()} awake`);
|
||||
chamber();
|
||||
run('for (let x = 30; x < 50; x++) { setCell(90*W+x, FIRE, 800, 0); life[90*W+x] = 25; }');
|
||||
ticks(run, 600);
|
||||
const burning = run('flags.reduce((n, f) => n + !!(f & F_BURN), 0)');
|
||||
check('fire with no fuel goes out and leaves nothing burning', grains('FIRE') === 0 && burning === 0 && awake() === 0,
|
||||
`${grains('FIRE')} flames, ${burning} burning grains, ${awake()} awake`);
|
||||
// Fully blocked gas cannot keep itself awake by moving sideways.
|
||||
for (const m of ['SMOKE', 'STEAM', 'FIRE']) {
|
||||
world(80);
|
||||
rect(run, 38, 89, 40, 91, run('STONE'));
|
||||
run(`setCell(90*W+39, ${m}, 20, 0); life[90*W+39] = ${m === 'SMOKE' ? 10 : 25};`);
|
||||
ticks(run, 1200);
|
||||
check(`${m.toLowerCase()} in a sealed one-grain pocket finishes changing and sleeps`, grains(m) === 0 && awake() === 0 && (m !== 'STEAM' || grains('WATER') === 1),
|
||||
`${grains(m)} gas left, ${grains('WATER')} water, ${awake()} awake`);
|
||||
}
|
||||
for (const m of ['SMOKE', 'STEAM', 'FIRE']) {
|
||||
world(80);
|
||||
run(`for (const x of [0, 40, W-1]) { setCell(x, ${m}, 110, 0); life[x] = 25; }`);
|
||||
ticks(run, 10);
|
||||
check(`${m.toLowerCase()} on the top row leaves cleanly`, grains(m) === 0 && awake() === 0 && (m !== 'STEAM' || run('worldWater()') === 3),
|
||||
`${grains(m)} gas left, ${awake()} awake, water total ${run('worldWater()')}`);
|
||||
}
|
||||
|
||||
console.log(`Physics suite time: ${Date.now() - started} ms`);
|
||||
done();
|
||||
Reference in New Issue
Block a user