Files
deepgrain-foundry/test/t_world.js
T
44r0n7andClaude Opus 5.5 80668a4baf Lakes fill only to their lower rim and keep clear of spawn and the dock (t96)
The lake lip was Math.min(surfY), the highest ground (y grows downward), so water was poured
above the lower rim and, once levelled, ran up to the trade dock on 4 of 12 seeds. The lip is
now the lower rim edge minus one, and each lake stays 80 grains from spawn. Seed hashes and
the shallow-ore hash re-baselined (changes only under lake basins); golden fixtures also
freeze surfY/dirtY/AMB from the previous generator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 19:45:51 -04:00

282 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 round 3: lake basins moved, so ore under them changed (414 cells, all under the two lakes).
check('shallow ore matches the original generator', oreBalance.shallowHash === 607138519, 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();