// 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;'); // --- whole bars in screw tubes; position an existing bar inside for mid-ride cases function putBar(x, y) { run(`{ spawnBar(BARCU, 20, 40, 0, 0); const e=ents[0]; entMark(e,0); e.x=e.ix=${x}; e.y=e.iy=${y}; entMark(e,1); }`); } function barTube(corner = false) { fresh(); run(`for (let by = 20; by < 24; by++) placeBlock(BP_TUBE, 0, 25, by, true); ${corner ? 'for (let bx = 25; bx < 29; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);' : ''}`); putBar(100,94); } function watchBar(n, condition) { return run(`(() => { for (let k=0;k<${n};k++) { simTick(); if (${condition}) return true; } return false; })()`); } fresh(); run('for (let bx=20;bx<25;bx++) placeBlock(BP_BELT,1,bx,25,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,0,25,by,true); spawnBar(BARCU,84,98,0,0);'); check('a belt feeds a whole copper bar into a lift and out its top', watchBar(200, 'ents.length===1 && ents[0].iy+BAR_H<=84'), run('JSON.stringify(ents)')); fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82); check('a whole bar leaves a right tube', watchBar(100,'ents.length===1 && ents[0].ix>=96')); // steam and smoke drift through tubes near furnaces: a bar passes them as it does in the open barTube(); check('steam and smoke in a tube do not stop a bar', run(`(() => { for (let k = 0; k < 120; k++) { for (let y = 80; y < 94; y++) for (let x = 100; x < 104; x++) if (mat[y*W+x] === EMPTY && !occ[y*W+x]) setCell(y*W+x, (x + y) & 1 ? STEAM : SMOKE, 120, 0); simTick(); if (ents.length === 1 && ents[0].iy + BAR_H <= 80) return true; } return false; })()`), run('JSON.stringify(ents)')); barTube(true); check('a bar turns from an up tube into a right tube and leaves', watchBar(200,'ents.length===1 && ents[0].ix>=116')); fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true);'); putBar(80,82); rect(run,96,80,99,83,run('STONE')); ticks(run,100); check('a blocked mouth holds the whole bar inside without wall overlap',run('ents.length===1 && ents[0].ix===92 && !mat.some((m,i)=>m===STONE && occ[i])'),run('JSON.stringify(ents)')); rect(run,96,80,99,83,run('EMPTY')); check('the bar leaves when the mouth wall is removed',watchBar(80,'ents[0].ix>=96')); barTube(); rect(run,100,88,103,89,run('SAND')); const shared = run(`(() => { let overlap=false, rose=false; const n=mat.reduce((n,m)=>n+(m===SAND),0); for(let k=0;k<160;k++) { simTick(); if(mat.some((m,i)=>m===SAND && occ[i])) overlap=true; if(ents[0].iy<90) rose=true; } return { overlap, rose, n, after:mat.reduce((n,m)=>n+(m===SAND),0)+flyers.filter(f=>f.m===SAND).length }; })()`); check('grains and a moving bar share a lift without overlap or lost sand',!shared.overlap && shared.rose && shared.n===shared.after,JSON.stringify(shared)); barTube(); ticks(run,8); run('globalThis.__barSave=serializeWorld(); deserializeWorld(__barSave.meta,__barSave.buffers);'); check('a bar saved mid-tube continues out of its mouth',watchBar(100,'ents.length===1 && ents[0].iy+BAR_H<=80')); fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_BELT,1,bx,25,true); placeBlock(BP_WALL,RSTONE,24,25,true); placeBlock(BP_WALL,RSTONE,24,24,true); spawnBar(BARCU,80,98,0,0);'); ticks(run,100); check('a normal belt still carries a bar and stops at its blocked end',run('ents.length===1 && ents[0].ix===92 && ents[0].iy===98')); fresh(); run('placeBlock(BP_TUBE,1,25,25,true); spawnBar(BARCU,100,98,0,0);'); ticks(run,100); check('a bar resting on a horizontal tube stays on top',run('ents[0].ix===100 && ents[0].iy===98')); for (const dir of [0, 2]) { fresh(); run(`placeBlock(BP_TUBE,${dir},25,20,true); spawnBar(BARCU,100,${dir ? 78 : 84},0,${dir ? 0 : -0.4});`); check(dir ? 'a falling bar enters a down tube from above' : 'an upward moving bar enters the bottom of an up tube',watchBar(20,'entTubeAt(ents[0].ix,ents[0].iy)!==0')); } fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_TUBE,1,bx,20,true); for(let by=21;by<25;by++) placeBlock(BP_TUBE,2,23,by,true);'); putBar(80,82); check('a bar turns from a right tube down into a vertical lane',watchBar(160,'ents[0].iy>=100')); barTube(true); rect(run,100,76,103,79,run('STONE')); ticks(run,100); check('a blocked corner waits without overlapping stone',run('ents[0].iy===80 && !mat.some((m,i)=>m===STONE && occ[i])')); rect(run,100,76,103,79,run('EMPTY')); check('a blocked corner resumes when cleared',watchBar(120,'ents[0].ix>=116')); barTube(); run('ents[0].T=1200; for(let y=94;y<96;y++) for(let x=100;x<104;x++) temp[y*W+x]=1200; entTick();'); check('a bar melting inside a tube becomes eight molten grains',run('ents.length===0') && count(run,run('MCU'))===8); done();