Files
deepgrain-foundry/test/t_world.js
T
44r0n7andClaude Opus 5.5 daeb3b2238 Rebuild lake generation: bowls in valleys filled to their spill level (t96)
Lakes no longer pour water to a guessed lip and repair it afterwards. Each lake lowers the land
in the nearest valley with a smoothstep bowl, and water fills it to its spill level (the lower of
the highest ground on each side, Priority-Flood in one dimension), so it is flat and contained by
construction. Clay beds, no caves or loose pockets close under lakes, trees kept off the shore.
Removes the levelling pass, the lower-rim lip and the bank-slope cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cj2RPXdPykKMEz5BXXczsj
2026-10-29 20:55:52 -04:00

283 lines
19 KiB
JavaScript

// Trees from acorns, charcoal, brick, density, water/lava/snow/ice, full-bag
// refunds, drill reach, ballistic blowing, snowy mountains.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
const fresh = () => { flatWorld(run, 240, 160, 100); run('player.tankTier = 3; saplings = []; stats.grown = 0;'); };
const box = (x0, y0, x1, y1, m) => rect(run, x0, y0, x1, y1, m);
const { createHash } = require('node:crypto');
const worldHash = () => { const hash = createHash('sha256'); for (const k of ['mat', 'temp', 'flags']) { const a = run(k); hash.update(new Uint8Array(a.buffer, a.byteOffset, a.byteLength)); } return hash.digest('hex'); };
run('generateWorld(4242, 512, 384)');
const generatedHash = worldHash();
const progress = [];
run('generateWorld')(4242, 512, 384, (f, label) => progress.push([f, label]));
check('generation progress rises to 1 with named steps', progress.length > 10 && progress[0][0] === 0 && progress.at(-1)[0] === 1 && progress.every(([f, label], i) => f >= 0 && f <= 1 && (!i || f >= progress[i - 1][0]) && typeof label === 'string' && label.length > 0));
check('generation callback keeps the same world', worldHash() === generatedHash);
// 1. acorns sprout on soil under open sky and grow into a tree
fresh();
box(100, 100, 140, 103, run('DIRT'));
run('setCell(99*W+120, ACORN, AMB[99], 0); updateRoof();');
ticks(run, 1200);
check('acorn sprouts into a sapling', run('saplings.length') === 1 && run('mat[99*W+120]') !== run('ACORN'), run('saplings.length'));
ticks(run, 5600);
const wood = run('bgm.reduce((a, v) => a + (v === WOOD), 0)'), leaves = run('bgm.reduce((a, v) => a + (v === LEAVES), 0)');
check('sapling grows into a full tree', run('stats.grown') === 1 && wood >= 14 && leaves > 40, `wood ${wood}, leaves ${leaves}`);
// acorn on bare stone does not sprout
run('setCell(99*W+60, ACORN, AMB[99], 0); updateRoof();');
box(55, 100, 65, 100, run('STONE'));
ticks(run, 1500);
check('acorn on stone stays an acorn', run('mat[99*W+60]') === run('ACORN') && run('saplings.length') === 0);
// cutting the grown tree drops acorns
run('bot.x = 114; bot.y = 86; for (let k=0;k<160;k++) toolDig(121, 99, 1)');
ticks(run, 20);
check('felling a tree drops acorns and wood', count(run, run('ACORN')) >= 2 && count(run, run('LWOOD')) > 10, `${count(run, run('ACORN'))} acorns, ${count(run, run('LWOOD'))} wood`);
// 2. charcoal: wood chunks heated in a sealed stone box (a kiln) char instead of burning
fresh();
box(110, 80, 130, 99, run('STONE')); box(112, 82, 128, 97, run('LWOOD'));
run('for (let y=80;y<=99;y++) for (let x=110;x<=130;x++) { const i=y*W+x; temp[i] = 520; wakeHeatXY(x,y); }');
for (let k = 0; k < 30; k++) { run('for (let y=80;y<=99;y++) for (let x=110;x<=130;x++) { const i=y*W+x; if (mat[i]===STONE) temp[i] = 560; }'); ticks(run, 20); }
const ch = count(run, run('CHARCOAL'), 110, 80, 130, 99), wl = count(run, run('LWOOD'), 110, 80, 130, 99);
check('sealed kiln turns wood into charcoal', ch > 100, `charcoal ${ch}, wood left ${wl}`);
// smothered: a burning wood pile buried under dirt chars inside
fresh();
box(110, 92, 130, 99, run('LWOOD'));
run('for (let k=0;k<60;k++){ for (let x=112;x<130;x+=4) toolIgnite(x, 92); simTick(); }');
ticks(run, 60);
box(106, 86, 134, 91, run('LDIRT'));
ticks(run, 1800);
check('a buried burning pile leaves charcoal', count(run, run('CHARCOAL')) > 20, count(run, run('CHARCOAL')));
// charcoal burns hot
fresh();
box(110, 94, 130, 99, run('CHARCOAL'));
run('for (let k=0;k<60;k++){ toolIgnite(120, 94); simTick(); }');
ticks(run, 600);
const tc = run('(()=>{let t=0;for(let y=88;y<100;y++)for(let x=105;x<135;x++)t=Math.max(t,temp[y*W+x]);return Math.round(t)})()');
check('charcoal burns over 1100°C', tc > 1100, tc);
// 3. brick: clay lumps mixed into a walled coal fire turn into brick rubble
fresh();
box(104, 76, 107, 99, run('STONE')); box(133, 76, 136, 99, run('STONE')); box(104, 76, 136, 79, run('STONE'));
run('for (let y=76;y<80;y++) for (let x=118;x<122;x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
run('{ const r = mulberry32(5); for (let y=90;y<=99;y++) for (let x=108;x<=132;x++) setCell(y*W+x, r() < 0.4 ? LCLAY : COAL, AMB[y], 0); }');
run('bot.x = 115; bot.y = 82; for (let k=0;k<60;k++){ for (const x of [110,120,130]) { for (let y=90;y<99;y++) if (mat[y*W+x]===COAL && mat[(y-1)*W+x]===EMPTY) { toolIgnite(x,y); break; } } simTick(); }');
ticks(run, 3600);
check('clay lumps in a walled coal fire become brick rubble', count(run, run('RBRICK')) > 15, `${count(run, run('RBRICK'))} brick, ${count(run, run('LCLAY'))} clay left`);
// 4. density: a pile sorts heavy under light; wood floats, ore sinks
fresh();
box(110, 84, 130, 99, run('COAL')); box(110, 80, 130, 83, run('CUORE'));
const oreY0 = run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===CUORE){s+=(i/W)|0;n++;} return s/n})()');
ticks(run, 3600);
const oreY1 = run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===CUORE){s+=(i/W)|0;n++;} return s/n})()');
check('ore settles down through a coal pile', oreY1 > oreY0 + 3, `${oreY0.toFixed(1)} -> ${oreY1.toFixed(1)}`);
fresh();
box(100, 80, 101, 99, run('STONE')); box(140, 80, 141, 99, run('STONE'));
box(102, 86, 139, 99, run('WATER')); box(110, 70, 115, 73, run('LWOOD')); box(125, 70, 130, 73, run('RSTONE'));
ticks(run, 900);
const wy = run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===LWOOD){s+=(i/W)|0;n++;} return s/n})()');
const ry = run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===RSTONE){s+=(i/W)|0;n++;} return s/n})()');
check('wood chunks float, rubble sinks', wy < 89 && ry > 95, `wood y ${wy.toFixed(1)}, rubble y ${ry.toFixed(1)}`);
// 5. lava meets water: steam and a basalt crust
fresh();
box(100, 80, 101, 99, run('STONE')); box(140, 80, 141, 99, run('STONE'));
run('for (let y=90;y<=99;y++) for (let x=102;x<=139;x++) setCell(y*W+x, MAGMA, 1250, 0)');
box(102, 84, 139, 87, run('WATER'));
ticks(run, 240);
check('water on lava flashes to steam and crusts it to basalt', count(run, run('BASALT')) > 20 && (count(run, run('STEAM')) + count(run, run('WATER'))) < 150, `basalt ${count(run, run('BASALT'))}, steam ${count(run, run('STEAM'))}, water ${count(run, run('WATER'))}`);
// 6. snow melts by a fire; water freezes in cold air high up
fresh();
box(110, 90, 116, 99, run('COAL')); box(117, 88, 126, 99, run('SNOW'));
run('for (let y=88;y<=99;y++) for (let x=117;x<=126;x++) temp[y*W+x] = -5;');
run('bot.x = 108; bot.y = 80; for (let k=0;k<60;k++){ toolIgnite(112, 90); simTick(); }');
ticks(run, 1800);
check('fire melts snow into water', count(run, run('SNOW')) < 100 && count(run, run('WATER')) + count(run, run('STEAM')) > 0, `snow ${count(run, run('SNOW'))}, water ${count(run, run('WATER'))}`);
check('the air is below freezing high up', run('AMB[5]') < -1, run('AMB[5].toFixed(1)'));
fresh();
box(100, 8, 140, 9, run('STONE')); box(100, 4, 101, 7, run('STONE')); box(139, 4, 140, 7, run('STONE'));
run('for (let y=5;y<=7;y++) for (let x=102;x<=138;x++) setCell(y*W+x, WATER, AMB[y], 0)');
ticks(run, 2400);
check('water up in the cold air freezes into ice', count(run, run('ICE')) > 20, `ice ${count(run, run('ICE'))}, water ${count(run, run('WATER'))}`);
const sv = run('(()=>{ const s = serializeWorld(); deserializeWorld(s.meta, s.buffers); let t = 99; for (let i=0;i<N;i++) if (mat[i]===ICE) t = Math.min(t, temp[i]); return t; })()');
check('sub-zero temperatures survive save and load', sv < 0, sv);
// 7. full bag: what does not fit drops where the block stood
fresh();
run('player.tankTier = 0; bag[RSTONE] = 16; bag[LDIRT] = 1500 - 16; bagTotal = 1500; bot.x = 110; bot.y = 60;');
run('placeBlocks(BP_WALL, RSTONE, [[30, 18]])');
run('bag[LDIRT] = 1500; bagTotal = 1500;');
const msg = run('removePieceAt(121, 73)');
check('removing a block with a full bag drops its rubble in place', count(run, run('RSTONE'), 120, 72, 123, 75) === 16 && /bag full/.test(msg), msg);
run('bag[BARCU] = 20; bag[LDIRT] = 1480; bagTotal = 1500; placeBlocks(BP_BELT, 1, [[32, 18]]); bag[LDIRT] += 1; bagTotal = 1500;');
const m0 = run('money'); run('removePieceAt(129, 73)'); ticks(run, 60);
check('a belt removed with a full bag still refunds its price as money', run('money') === m0 + 12, `${m0} -> ${run('money')}`);
// 8. drill reach grows with the bit
fresh();
run('bot.x = 100; bot.y = 70;');
const far = run('JSON.stringify(toolDig(102, 102, 2))');
check('stone bit cannot reach 30 grains away', JSON.parse(far).dug === 0 && /Out of reach/.test(far), far);
run('player.bitTier = 3;'); run('for (let k=0;k<30;k++) toolDig(102, 102, 2)');
check('steel bit reaches that far', count(run, run('RSTONE')) > 5, count(run, run('RSTONE')));
// 9. blown grains fly past the cursor until gravity brings them down
fresh();
run('bot.x = 100; bot.y = 60; bag[SAND] = 200; bagTotal = 200;');
for (let k = 0; k < 20; k++) run('toolBlow(120, 55, 0, SAND); simTick();');
ticks(run, 200);
const sx = run('(()=>{let s=0,n=0;for(let i=0;i<N;i++) if(mat[i]===SAND){s+=i%W;n++;} return n ? s/n : 0})()');
check('blown sand carries on past the cursor and lands beyond it', sx > 121 && run('flyers.length') === 0, `landed around x ${sx.toFixed(1)} (cursor at 120)`);
// 10. a generated world has a snowy mountain
run('generateWorld(4242, 2048, 1024, null)');
const snow = run('(()=>{let n=0;for(let i=0;i<N;i++) if(mat[i]===SNOW) n++; return n})()');
const peak = run('Math.min(...surfY)');
check('worldgen raises a snow-capped mountain', snow > 80 && run(`AMB[${peak}]`) < 0, `snow ${snow}, peak y ${peak}, ${run(`AMB[${peak}].toFixed(1)`)}°C`);
// the layout: plenty of sky, the first ores as shallow as ever, the deep layers stretched out below
check('a third of a new world is sky', Math.abs(run('surfBase / H') - 0.3) < 0.01, run('surfBase'));
const shallowOre = run('(()=>{let cu=0,fe=0;for(let x=0;x<W;x++)for(let d=0;d<150;d++){const m=mat[(surfY[x]+d)*W+x];if(m===CUV)cu++;else if(m===FEV)fe++;}return [cu,fe]})()');
check('copper and iron ore are within reach of the surface', shallowOre[0] > 2000 && shallowOre[1] > 200, shallowOre);
check('the deep layers fill the extra depth', run('depthY(0.905) - surfBase') > 600 && run('Math.abs(depthY(0.28) - surfBase - 131) <= 1'), run('[depthY(0.28), depthY(0.5), depthY(0.905)].map(y => y - surfBase)'));
check('it is still warm a hundred cells above the ground', run('AMB[surfBase - 100]') > 0, run('AMB[surfBase - 100].toFixed(1)'));
// Compare ore density in the generated shallow and deep underground, excluding air.
const oreDepth = run(`(() => {
let shallow = 0, shallowOre = 0, deep = 0, deepOre = 0, deepCu = 0;
const ores = [COALV, CUV, FEV, SNV, ZNV, AGV, AUV];
for (let y = 0; y < H; y++) {
const d = y - surfBase;
const fy = d <= shallowD ? d / shallowD * 0.3 : 0.3 + (d - shallowD) / (underD - shallowD) * 0.7;
for (let x = 0; x < W; x++) {
const m = mat[y * W + x];
if (y < surfY[x] || m === EMPTY) continue;
const ore = ores.includes(m);
if (fy < 0.25) { shallow++; if (ore) shallowOre++; }
if (fy > 0.6) { deep++; if (ore) deepOre++; }
if (fy > 0.5 && m === CUV) deepCu++;
}
}
return [shallowOre / shallow, deepOre / deep, deepCu];
})()`);
check('deep ground has at least three times the shallow ore density', oreDepth[1] >= 3 * oreDepth[0], `shallow ${oreDepth[0]}, deep ${oreDepth[1]}`);
check('copper veins also form below half depth', oreDepth[2] > 0, oreDepth[2]);
// Medium seed 424242: keep each ore's increase within its intended balance.
run('generateWorld(424242, 3072, 1280, null)');
const oreBalance = run(`(() => {
const ids = [COALV, CUV, FEV, ZNV, SNV, AGV, AUV], counts = ids.map(() => 0);
let shallowHash = 2166136261;
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
const m = mat[y * W + x], j = ids.indexOf(m);
if (j >= 0) counts[j]++;
if (depthU(y) < 0.25) shallowHash = Math.imul(shallowHash ^ (j >= 0 ? m : 0), 16777619) >>> 0;
}
return { counts, shallowHash };
})()`);
const oldOre = [33493, 26508, 77679, 11797, 7723, 3638, 1681];
const oreNames = ['coal', 'copper', 'iron', 'zinc', 'tin', 'silver', 'gold'];
oreNames.forEach((name, j) => {
const ratio = oreBalance.counts[j] / oldOre[j];
// coal is lower than the rest: seams deep enough to light in the natural heat are left out
const lo = j === 0 ? 3 : j < 5 ? 4 : 2.5, hi = j === 0 ? 4 : j < 5 ? 5 : 3.5;
check(`${name} stays within its intended increase`, ratio >= lo && ratio <= hi, ratio.toFixed(2));
});
const totalOre = oreBalance.counts.reduce((a, b) => a + b, 0);
check('total ore is about 4 to 4.5 times the original', totalOre >= 162519 * 4 && totalOre <= 162519 * 4.5, totalOre);
// t96: lakes are bowls in the nearest valley with no caves or loose pockets close under them, so ore
// changed there (724 cells, all within the old and new lake spans).
check('shallow ore matches the original generator', oreBalance.shallowHash === 3875519651, oreBalance.shallowHash);
run('generateWorld(99, 4096, 1536, null)');
const marks = run('(()=>{let lakes=0,peaks=0,inLake=false;for(let x=1;x<W-1;x++){const l=mat[(surfY[x]-1)*W+x]===WATER;if(l&&!inLake)lakes++;inLake=l;if(surfY[x]<surfBase-100&&surfY[x]<=surfY[x-1]&&surfY[x]<surfY[x+1])peaks++;}return [lakes,peaks]})()');
check('a large world has several lakes and mountains across it', marks[0] >= 2 && marks[1] >= 2, marks);
// --- round 5: trees under water, cut saplings, blown grains stay out of the bag
fresh();
box(100, 100, 140, 103, run('DIRT'));
run('for (let y = 82; y < 100; y++) for (let x = 120; x < 122; x++) bgm[y*W+x] = WOOD; for (let y = 74; y < 82; y++) for (let x = 115; x < 127; x++) bgm[y*W+x] = LEAVES;');
box(100, 84, 101, 99, run('STONE')); box(140, 84, 141, 99, run('STONE')); box(102, 84, 139, 99, run('WATER'));
run('bot.x = 114; bot.y = 80; for (let k=0;k<200;k++) toolDig(120, 98, 1)');
ticks(run, 200);
check('a tree under water can be cut down', run('bgm[90*W+120]') === 0 && count(run, run('LWOOD')) > 10, `${count(run, run('LWOOD'))} wood chunks`);
fresh();
box(100, 100, 140, 103, run('DIRT'));
run('setCell(99*W+120, ACORN, AMB[99], 0); updateRoof();');
ticks(run, 1200 + 3000);
run('bot.x = 114; bot.y = 86; for (let k=0;k<160;k++) toolDig(120, 98, 1)');
ticks(run, 30);
run('for (let i = 0; i < N; i++) if (mat[i] === ACORN) setCell(i, EMPTY, 20, 0)'); // keep its acorns from sprouting a new one
ticks(run, 2400);
const left = run('bgm.reduce((a, v) => a + (v === WOOD), 0)');
check('a half-grown tree that is cut down stays down', run('saplings.length') === 0 && left < 4, `trunk cells left ${left}, saplings ${run('saplings.length')}`);
fresh();
run('bot.x = 110; bot.y = 85; bag[SAND] = 300; bagTotal = 300;');
for (let k = 0; k < 400 && run('bag[SAND]') > 0; k++) run('toolBlow(125, 99, 5, SAND); simTick();');
ticks(run, 400);
check('blown grains never come back into the bag', run('bag[SAND]') === 0 && count(run, run('SAND')) === 300, `bag ${run('bag[SAND]')}, world ${count(run, run('SAND'))}`);
// --- round 6: lifts, falling trees, machines without power, the bot and walls
fresh();
// a shaft 16 wide, a column of up-screws from its floor, turned sideways at the top to unload onto the ground
run('for (let y = 100; y < 132; y++) for (let x = 120; x < 128; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
run('bot.x = 110; bot.y = 90;');
// unload well above the ground: a pile that reaches the outlet would block it
run('for (let by = 19; by <= 32; by++) placeBlock(BP_SCREW, 0, 30, by, true); for (let bx = 30; bx <= 35; bx++) placeBlock(BP_SCREW, 1, bx, 18, true);');
box(120, 116, 127, 131, run('SAND'));
const sand0 = count(run, run('SAND'));
ticks(run, 1800);
const lifted = count(run, run('SAND'), 136, 0, 239, 99);
check('a column of upward screw conveyors lifts sand out of a shaft, unpowered', lifted > sand0 * 0.8, `${lifted} of ${sand0} grains brought up and out`);
fresh();
run('for (let bx = 25; bx <= 30; bx++) placeBlock(BP_BELT, 1, bx, 22, true); rebuildPower();');
box(101, 84, 104, 87, run('SAND'));
ticks(run, 600);
check('belts run without any power', count(run, run('SAND'), 110, 0, 239, 99) > 8, count(run, run('SAND'), 110, 0, 239, 99));
fresh();
box(100, 100, 140, 103, run('DIRT'));
run('for (let y = 86; y < 100; y++) for (let x = 120; x < 122; x++) bgm[y*W+x] = WOOD; for (let y = 76; y < 86; y++) for (let x = 115; x < 127; x++) bgm[y*W+x] = LEAVES; findTreeBases();');
run('for (let y = 100; y < 104; y++) for (let x = 116; x < 126; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
ticks(run, 60);
check('a tree with its ground dug out from under it falls', run('bgm[90*W+120]') === 0 && count(run, run('LWOOD')) > 10, count(run, run('LWOOD')));
fresh();
// a stone box of sand; the bot flies in through the wall: no sand may end up outside
box(110, 80, 113, 99, run('STONE')); box(130, 80, 133, 99, run('STONE')); box(110, 96, 133, 99, run('STONE'));
run('for (let y=80;y<100;y++) for (let x=110;x<134;x++) flags[y*W+x] = 0;');
box(114, 84, 129, 95, run('SAND'));
const inside0 = count(run, run('SAND'), 114, 0, 129, 99);
run('bot.x = 100; bot.y = 88;');
for (let k = 0; k < 120; k++) run('botStep(1, 0, false); simTick();');
for (let k = 0; k < 160; k++) run('botStep(-1, 0, false); simTick();');
ticks(run, 120);
check('the bot flying through a wall into sand never pushes sand out', count(run, run('SAND'), 114, 0, 129, 99) === inside0, `${inside0} -> ${count(run, run('SAND'), 114, 0, 129, 99)} inside`);
// a settled sand pile with the bot sitting in it: not one grain moves
fresh();
box(110, 92, 133, 99, run('STONE'));
box(114, 84, 129, 91, run('SAND'));
ticks(run, 60);
const pile = () => run('(() => { let s = ""; for (let y = 70; y < 92; y++) for (let x = 110; x < 134; x++) s += mat[y * W + x] === SAND ? 1 : 0; return s; })()');
const pile0 = pile();
run('bot.x = 119; bot.y = 84; bot.vx = bot.vy = 0;');
for (let k = 0; k < 90; k++) run('botStep(0, 0, false); simTick();');
check('the bot resting inside a sand pile leaves every grain where it was', pile() === pile0);
// sand poured onto the bot falls through it and piles up just as it would with the bot elsewhere
const pour = (bx) => {
fresh(); box(110, 96, 133, 99, run('STONE'));
run(`bot.x = ${bx}; bot.y = 86; bot.vx = bot.vy = 0;`);
for (let k = 0; k < 140; k++) run('if (k_ % 2 === 0) setCell(60 * W + 120, SAND, AMB[60], 0); botStep(0, 0, false); simTick();'.replace('k_', String(k)));
ticks(run, 60);
return [count(run, run('SAND'), 110, 60, 133, 95), count(run, run('SAND'), 110, 60, 133, 84)];
};
const [withBot, aboveBot] = pour(118), [without] = pour(200);
check('sand poured onto the bot falls through it to the ground', withBot === without && aboveBot === 0, `${withBot} grains landed (${without} with the bot away), ${aboveBot} held up above it`);
// deep ore never sits in rock hot enough to light or melt it: a reloaded world wakes every area,
// and burning coal or running gold down there kept thousands of areas busy forever
run('generateWorld(77, 2048, 1024, null)');
const hotOre = run('(() => { let n = 0; for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) { const m = mat[y * W + x]; if ([COALV, CUV, FEV, SNV, ZNV, AGV, AUV].includes(m) && (IGN[m] ? AMB[y] > IGN[m] - 120 : AMB[y] > MELT[m] - 200)) n++; } return n; })()');
check('no ore is generated where the rock is hot enough to light or melt it', hotOre <= 20, hotOre);
run('globalThis.__s = serializeWorld(); deserializeWorld(JSON.parse(JSON.stringify(__s.meta)), __s.buffers); for (let k = 0; k < 900; k++) simTick();');
const busy = run('(() => { let aw = 0, fl = 0; for (let c = 0; c < NCH; c++) if (awake[c]) aw++; for (let i = 0; i < N; i++) if (mat[i] === FLAME) fl++; return [aw, NCH, fl]; })()');
check('a reloaded world settles: no fires deep down, few areas still moving', busy[2] === 0 && busy[0] < busy[1] * 0.1, busy);
done();