Files
deepgrain-foundry/test/t_conveyors.js
T

254 lines
19 KiB
JavaScript

// Screw tubes that never carry liquid (it flows through), tube and belt throughput, filters.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
const tubeLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_TU && part[i] <= P_TR && PH[mat[i]] === PH_LIQUID) n++; return n; })()');
// grains that land in the sink region are counted and taken away, so outlets never back up
function rate(x0, x1, y0, y1, warm, meas) {
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
for (let k = 0; k < warm; k++) run(`simTick(); ${sink}`);
run('__got = 0');
for (let k = 0; k < meas; k++) run(`simTick(); ${sink}`);
return run('__got') / (meas / 60);
}
// --- tubes are sealed: no water in, no water carried
fresh();
run('for (let y = 120; y < 170; y++) for (let x = 100; x < 124; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
rect(run, 100, 150, 123, 169, run('SAND'));
rect(run, 100, 130, 123, 149, run('WATER'));
run('for (let by = 22; by <= 41; by++) placeBlock(BP_TUBE, 0, 27, by, true); for (let bx = 27; bx <= 34; bx++) placeBlock(BP_TUBE, 1, bx, 21, true);');
const water0 = count(run, run('WATER'));
ticks(run, 900);
const wOut = count(run, run('WATER'), 124, 0, 319, 119), sOut = count(run, run('SAND'), 124, 0, 319, 119);
check('a lift standing in a flooded pit brings up the sand but not the water', sOut > 150 && wOut === 0, `sand out ${sOut}, water out ${wOut}, water standing in the tubes ${tubeLiquid()}`);
const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)');
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitClouds.toFixed(0)} in the clouds`);
fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_TUBE, 1, bx, 25, true);');
rect(run, 84, 60, 95, 79, run('WATER'));
ticks(run, 300);
check('water poured on a tube runs straight through it', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 200 && tubeLiquid() < 10, `${count(run, run('WATER'), 0, 104, 319, 119)} below, ${tubeLiquid()} still inside`);
fresh();
// a vertical tube pointing up, its top open to a pool: the water drains down through it, against the screw
run('for (let by = 20; by <= 26; by++) placeBlock(BP_TUBE, 0, 25, by, true); for (let y = 60; y < 80; y++) { setCell(y*W+99, STONE, AMB[y], 0); setCell(y*W+104, STONE, AMB[y], 0); }');
rect(run, 100, 60, 103, 79, run('WATER'));
ticks(run, 400);
check('water runs down through an upward screw, which never pushes it back', count(run, run('WATER'), 0, 0, 319, 79) === 0, count(run, run('WATER'), 0, 0, 319, 79));
// Intake itself fills the far lanes first, for either tube axis.
for (const horizontal of [false, true]) {
fresh();
run(`placeBlock(BP_TUBE, ${horizontal ? 1 : 0}, 25, 20, true);`);
const filled = run(`(() => {
const lanes = [];
for (let k = 0; k < BS; k++) {
tickStamp++;
const x = ${horizontal ? 100 : 99}, y = ${horizontal ? 79 : 80};
setCell(y*W+x, SAND, AMB[y], 0);
tubeIntake(80*W+100, ${horizontal ? 'P_TR, 1, 0' : 'P_TU, 0, -1'});
for (let lane = 0; lane < BS; lane++) if (mat[(80+${horizontal ? 'lane' : '0'})*W+100+${horizontal ? '0' : 'lane'}] === SAND && !lanes.includes(lane)) lanes.push(lane);
}
return lanes;
})()`);
check(horizontal ? 'a right screw takes grains from above into every row' : 'an up screw takes grains from the side into every column', filled.join() === '3,2,1,0', filled);
}
// A finite heap rides into the side of a six-block lift; remove each mouth flyer once.
const sideSeed = run('RS');
fresh();
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let by = 19; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
rect(run, 64, 40, 119, 99, run('SAND'));
const sideLift = run(`(() => {
let out = 0; const lanes = [0, 0, 0, 0];
const grains = () => mat.reduce((n, m) => n + (m === SAND), 0) + flyers.filter(f => f.m === SAND).length;
const before = grains();
for (let k = 0; k < 3000; k++) {
simTick();
for (let x = 128; x < 132; x++) if (mat[88*W+x] === SAND) lanes[x-128]++;
for (let f = flyers.length - 1; f >= 0; f--) if (flyers[f].m === SAND && flyers[f].y < 76 && flyers[f].vy < 0) { out++; flyers.splice(f, 1); }
}
return { out, lanes, before, after: grains() + out };
})()`);
console.log('Side-fed lift, 3000 ticks: ' + JSON.stringify(sideLift));
check('a belt feeding a lift from the side fills all four lanes', sideLift.lanes.every(n => n > 0), sideLift.lanes);
check('the side-fed lift conserves its sand, including mouth flyers', sideLift.before === sideLift.after, `${sideLift.before} -> ${sideLift.after}`);
// Before side spreading: 1459 grains left this lift in 3000 ticks (one lane, nearly full).
// The same belt and heap deliver 2243 into an open drop, the most the lift can be given.
check('a side-fed lift carries at least 90% of what its belt brings (it was 65%)', sideLift.out >= Math.floor(0.9 * 2243), sideLift.out);
// Same belt and finite heap, with a non-blocking sink at the tube side.
run(`RS = ${sideSeed}`);
fresh();
run('RS = 7; for (let bx = 16; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 64, 40, 119, 99, run('SAND'));
const beltDelivery = run(`(() => {
let out = 0;
for (let k = 0; k < 3000; k++) {
simTick();
for (let y = 0; y < H; y++) for (let x = 128; x < W; x++) {
const i = y*W+x;
if (mat[i] === SAND) { out++; setCell(i, EMPTY, AMB[y], 0); }
}
for (let f = flyers.length - 1; f >= 0; f--) if (flyers[f].m === SAND && flyers[f].x >= 128) { out++; flyers.splice(f, 1); }
}
return out;
})()`);
console.log('Belt delivery to a non-blocking sink, 3000 ticks: ' + beltDelivery);
// --- throughput
fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
rect(run, 80, 0, 95, 79, run('SAND'));
ticks(run, 60);
const lanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
check('a sideways tube fed from above (its side) fills more than one row', lanes > 1, lanes);
check('a sideways tube fed from a hopper runs full, all four grains wide', lanes === 4, lanes);
fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
rect(run, 80, 0, 95, 79, run('SAND'));
const tr = rate(120, 319, 84, 119, 90, 300);
// tubes run at belt pace (every other tick), so a belt under a tube's mouth keeps up with it
check('a sideways tube moves around a hundred grains a second', tr > 80 && tr < 150, `${tr.toFixed(0)}/s`);
fresh();
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
rect(run, 90, 130, 129, 195, run('SAND'));
run('for (let by = 20; by <= 37; by++) placeBlock(BP_TUBE, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);');
const lr = rate(164, 319, 80, 119, 400, 300);
const liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
check('a lift turns its corner without squeezing down to one lane', liftLanes === 4 && lr > 30, `${lr.toFixed(0)}/s, ${liftLanes} lanes at the top`);
fresh();
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 80, 20, 95, 99, run('SAND'));
const br = rate(128, 319, 104, 119, 90, 300);
ticks(run, 0);
check('a belt carries a pile off at several dozen grains a second', br > 50, `${br.toFixed(0)}/s`);
fresh();
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 96, 80, 111, 99, run('WATER'));
ticks(run, 300);
check('water on a belt drains through it', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 280, count(run, run('WATER'), 0, 0, 319, 99));
fresh();
// a belt under standing water: walls hold a pool over its middle, and sand still rides through it
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true); for (let y = 90; y < 98; y++) { setCell(y*W+100, STONE, AMB[y], 0); setCell(y*W+108, STONE, AMB[y], 0); }');
rect(run, 101, 90, 107, 99, run('WATER'));
rect(run, 80, 92, 87, 99, run('SAND')); rect(run, 79, 88, 79, 99, run('STONE'));
run('globalThis.__past = 0;');
for (let k = 0; k < 600; k++) { run('for (let y = 92; y < 100; y++) for (let x = 101; x < 108; x++) if (mat[y*W+x] === EMPTY) setCell(y*W+x, WATER, AMB[y], 0); simTick(); for (let y = 104; y < 120; y++) for (let x = 128; x < 319; x++) if (mat[y*W+x] === SAND) { mat[y*W+x] = EMPTY; __past++; }'); }
const wetPast = run('__past') + count(run, run('SAND'), 108, 0, 319, 119);
check('sand rides through water standing on a belt and on past it', wetPast === 64, `${wetPast} of 64`);
fresh();
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 80, 60, 95, 99, run('SAND'));
ticks(run, 40);
const depth = run('(() => { let d = 0; for (let y = 99; y > 90; y--) if (mat[y*W+110] === SAND) d++; else break; return d; })()');
check('the load rides a belt up to a block deep', depth >= 3, depth);
// --- filters
fresh();
// a filter for copper ore under a mixed pile; everything else rides right and off the end
// fed from a walled hopper over its left end, the way a tube or belt would drop a mixed load on it
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [CUORE] });');
run('for (let y = 30; y < 120; y++) setCell(y*W+79, STONE, AMB[y], 0); for (let y = 30; y < 96; y++) setCell(y*W+88, STONE, AMB[y], 0);');
run('for (let y = 40; y < 96; y++) for (let x = 80; x < 88; x++) setCell(y*W+x, (x + y) % 3 ? SAND : CUORE, AMB[y], 0)');
const ore0 = count(run, run('CUORE')), sand0 = count(run, run('SAND'));
// what rides off the far end is taken away and counted, so its pile never spreads back under the filter
run('globalThis.__past = [0, 0];');
for (let k = 0; k < 1200; k++) run('simTick(); for (let y = 104; y < 120; y++) for (let x = 120; x < 319; x++) { const i = y*W+x; if (mat[i] === SAND) { __past[0]++; mat[i] = EMPTY; } else if (mat[i] === CUORE) { __past[1]++; mat[i] = EMPTY; } }');
const oreUnder = count(run, run('CUORE'), 80, 104, 119, 119), sandUnder = count(run, run('SAND'), 80, 104, 119, 119);
const sandPast = run('__past[0]');
check('a filter lets its material fall through, even from under the rest of the load', oreUnder > ore0 * 0.9, `${oreUnder} of ${ore0} ore below`);
check('everything else rides along on top and off the end', sandUnder === 0 && sandPast > sand0 * 0.9, `${sandUnder} sand fell through, ${sandPast} of ${sand0} carried off`);
fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_FILTER, 0, bx, 25, true, { m: [CUORE] });');
rect(run, 84, 70, 95, 89, run('WATER'));
ticks(run, 400);
check('liquids drain through a filter', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 220, count(run, run('WATER'), 0, 104, 319, 119));
fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [SAND] }); for (let bx = 30; bx < 34; bx++) placeBlock(BP_FILTER, 0, bx, 25, true, { m: [BARCU] });');
run('spawnBar(BARCU, 82, 96, 0, 0); spawnBar(BARCU, 124, 96, 0, 0);');
ticks(run, 200);
const bars = JSON.parse(run('JSON.stringify(ents.map(e => [e.ix, e.iy]))'));
check('a bar rides along a filter set for something else', bars.some(([x, y]) => x > 100 || y > 104), JSON.stringify(bars));
check('and falls through a filter set for its own kind', bars.some(([x, y]) => x >= 116 && x < 136 && y >= 104), JSON.stringify(bars));
// carry mode with several materials: sand and coal ride along, everything else drops
fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [SAND, COAL], inv: 1 });');
run('for (let y = 30; y < 120; y++) setCell(y*W+79, STONE, AMB[y], 0); for (let y = 30; y < 96; y++) setCell(y*W+88, STONE, AMB[y], 0);');
run('for (let y = 40; y < 96; y++) for (let x = 80; x < 88; x++) setCell(y*W+x, [SAND, CUORE, COAL, FEORE][(x + y * 3) & 3], AMB[y], 0)');
const k0 = { sand: count(run, run('SAND')), coal: count(run, run('COAL')), cu: count(run, run('CUORE')), fe: count(run, run('FEORE')) };
run('globalThis.__past = {};');
for (let k = 0; k < 1200; k++) run('simTick(); for (let y = 104; y < 120; y++) for (let x = 120; x < 319; x++) { const i = y*W+x, m = mat[i]; if (m !== EMPTY && m !== STONE) { __past[m] = (__past[m] || 0) + 1; mat[i] = EMPTY; } }');
const under = { sand: count(run, run('SAND'), 80, 104, 119, 119), coal: count(run, run('COAL'), 80, 104, 119, 119), cu: count(run, run('CUORE'), 80, 104, 119, 119), fe: count(run, run('FEORE'), 80, 104, 119, 119) };
check('carry mode: the chosen materials ride along and off the end', under.sand === 0 && under.coal === 0 && run('(__past[SAND] || 0) + (__past[COAL] || 0)') > 0.9 * (k0.sand + k0.coal), JSON.stringify(under));
check('and everything else drops through, both kinds of ore', under.cu > 0.9 * k0.cu && under.fe > 0.9 * k0.fe, `${under.cu}/${k0.cu} copper, ${under.fe}/${k0.fe} iron`);
// changing what a filter passes pushes out what no longer fits; blueprints and saves keep the setting
fresh();
run('placeBlock(BP_FILTER, 0, 25, 25, true, { m: [SAND] }); for (let y = 100; y < 104; y++) for (let x = 100; x < 104; x++) setCell(y*W+x, SAND, AMB[y], 0);');
run('setFilter(25 * BW + 25, { m: [CUORE] })');
check('switching a filter to copper ore pushes the sand inside it out on top', count(run, run('SAND'), 100, 100, 103, 103) === 0 && count(run, run('SAND'), 96, 80, 108, 99) === 16);
run('placeBlock(BP_FILTER, 128, 30, 25, true, { m: [FEORE, CUORE], inv: 1 });');
const bp = JSON.parse(run('JSON.stringify(flipBlueprint(copyRegion(120, 100, 123, 103)).blocks)'));
check('a flipped blueprint keeps the filter settings and turns its direction', bp[0][3] === 0 && JSON.stringify(bp[0][4]) === JSON.stringify({ m: [run('CUORE'), run('FEORE')].sort((a, b) => a - b), inv: 1 }), JSON.stringify(bp));
run('clearAllGhosts(); pasteGhost(flipBlueprint(copyRegion(120, 100, 123, 103)), 34, 25); bot.x = 130; bot.y = 80; for (let k = 0; k < 30; k++) ghostTick();');
check('pasting it builds a filter with the same materials and mode', run('blockP[25*BW+34]') === run('BP_FILTER') && run('JSON.stringify(filterInfo(25*BW+34))') === JSON.stringify({ m: [run('CUORE'), run('FEORE')].sort((a, b) => a - b), inv: 1 }), run('JSON.stringify(filterInfo(25*BW+34))'));
run('globalThis.__s = serializeWorld(); deserializeWorld(__s.meta, __s.buffers);');
check('save and load keep filters, their materials, mode and direction', run('blockP[25*BW+30]') === run('BP_FILTER') && run('blockV[25*BW+30]') === 128 && run('filterInfo(25*BW+30).inv') === 1 && run('filterInfo(25*BW+30).m.length') === 2 && run('part[100*W+120]') === run('P_FILTER'));
// a save from before: one material kept in the block variant
run('__s.meta.filters = []; __s.buffers.blockV[25*BW+30] = 128 | SAND; deserializeWorld(__s.meta, __s.buffers);');
check('older filters keep the one material they had', run('JSON.stringify(filterInfo(25*BW+30))') === JSON.stringify({ m: [run('SAND')], inv: 0 }) && run('blockV[25*BW+30]') === 128);
// the run of matching filters in a row
fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [SAND] }); placeBlock(BP_FILTER, 128, 26, 25, true, { m: [COAL] }); placeBlock(BP_FILTER, 0, 27, 25, true, { m: [COAL] });');
check('a filter line is the touching filters that do the same thing', run('filterRun(25*BW+22).length') === 6 && run('filterRun(25*BW+26).length') === 1 && run('filterRun(25*BW+27).length') === 1);
// replacing what is built: hold the modifier and it swaps, refunding the old piece
fresh();
run('money = 200; for (let bx = 20; bx < 24; bx++) placeBlock(BP_BELT, 1, bx, 25, true); bot.x = 88; bot.y = 80;');
let r = JSON.parse(run('JSON.stringify(placeBlocks(BP_FILTER, 128, blockLine(20, 25, 23, 25), { m: [SAND] }, false))'));
check('without the modifier, built blocks are left alone', r.n === 0 && /already built/.test(r.status) && run('money') === 200, JSON.stringify(r));
r = JSON.parse(run('JSON.stringify(placeBlocks(BP_FILTER, 128, blockLine(20, 25, 23, 25), { m: [SAND] }, true))'));
check('with it, belts are swapped for filters, paying the difference', r.n === 4 && run('blockP.reduce((a, v) => a + (v === BP_FILTER), 0)') === 4 && run('blockP.reduce((a, v) => a + (v === BP_BELT), 0)') === 0 && run('money') === 200 + 4 * 12 - 4 * 40, `${JSON.stringify(r)} money ${run('money')}`);
r = JSON.parse(run('JSON.stringify(placeBlocks(BP_FILTER, 0, blockLine(20, 25, 23, 25), { m: [COAL], inv: 1 }, true))'));
check('replacing a filter with a filter just re-sets it, at no cost', r.n === 4 && run('money') === 200 + 48 - 160 && run('filterInfo(25*BW+21).inv') === 1 && run('blockV[25*BW+21]') === 0, `${JSON.stringify(r)} money ${run('money')}`);
run('bag[RSTONE] = 64; bagTotal = 64;');
r = JSON.parse(run('JSON.stringify(placeBlocks(BP_WALL, RSTONE, blockLine(20, 25, 21, 25), undefined, true))'));
check('and a wall can replace them too', r.n === 2 && run('blockP[25*BW+20]') === run('BP_WALL') && run('!filterCfg.has(25*BW+20)'), JSON.stringify(r));
check('filters are a buildable piece with a price', run('PIECE_BY_ID.filter && !PIECE_BY_ID.filter.hidden && PIECE_BY_ID.filter.cost[0][0] === CASH'));
check('no trucks', run('typeof truck') === 'undefined');
// --- a backed-up heap must not shear over its bottom layer
// Watch belt beats, not settling between beats: copper must not ride over sand that stays put.
run(`globalThis.__carry = carryColumn;
globalThis.__shear = 0;
carryColumn = function(a, d, pass) {
const bottom = a, watch = [];
const sand = mat[a] === SAND && stamp[a] !== tickStamp && !occ[a];
if (sand) for (let i = a - W; i >= 0 && mat[i] !== EMPTY; i -= W) {
if (mat[i] === CUORE && stamp[i] !== tickStamp) watch.push(i);
}
__carry(a, d, pass);
if (sand && mat[bottom] === SAND && stamp[bottom] !== tickStamp)
for (const i of watch) if (mat[i] !== CUORE) __shear++;
};`);
for (const lift of [true, false]) {
fresh();
run('RS = 7; __shear = 0; for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
if (lift) run('for (let by = 15; by <= 24; by++) placeBlock(BP_TUBE, 0, 32, by, true);');
else run('for (let by = 10; by <= 25; by++) placeBlock(BP_WALL, RSTONE, 32, by, true);');
rect(run, 88, 94, 119, 99, run('SAND'));
rect(run, 88, 70, 119, 93, run('CUORE'));
ticks(run, 4000);
const shear = run('__shear');
check(lift ? 'a heap feeding an upward screw does not shear over stuck sand' : 'a heap against a wall does not shear over stuck sand', shear === 0, `${shear} copper steps over stationary sand in 4000 ticks`);
if (lift) {
const left = count(run, run('SAND'), 80, 0, 127, 99);
const lifted = count(run, run('SAND'), 128, 0, 319, 93);
check('the bottom sand leaves the belt through the upward screw too', left < 192 / 2 && lifted > 0, `${192 - left} of 192 sand left the belt, ${lifted} above its bottom layer at the lift`);
}
}
run('carryColumn = __carry;');
done();