262 lines
17 KiB
JavaScript
262 lines
17 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);
|
|
|
|
// 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];
|
|
const lo = j < 2 ? 4.5 : j < 5 ? 4 : 3, hi = j < 2 ? 5.5 : j < 5 ? 5 : 4;
|
|
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.5 to 5 times the original', totalOre >= 162519 * 4.5 && totalOre <= 162519 * 5, totalOre);
|
|
check('shallow ore matches the original generator', oreBalance.shallowHash === 706875613, 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-tubes 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_TUBE, 0, 30, by, true); for (let bx = 30; bx <= 35; bx++) placeBlock(BP_TUBE, 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 tubes 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`);
|
|
done();
|