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
283 lines
19 KiB
JavaScript
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();
|