Player text says Screw conveyor; code uses screw (BP_SCREW, piece id screw, screwIntake...). No compatibility code for old saves or blueprints. Suite outputs match before and after apart from names. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
311 lines
24 KiB
JavaScript
311 lines
24 KiB
JavaScript
// Screw conveyors that never carry liquid (it flows through), screw 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 screwLiquid = () => run('(() => { let n = 0; for (let i = 0; i < N; i++) if (part[i] >= P_SCREW_U && part[i] <= P_SCREW_R && 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);
|
|
}
|
|
|
|
// --- screws 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_SCREW, 0, 27, by, true); for (let bx = 27; bx <= 34; bx++) placeBlock(BP_SCREW, 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 screws ${screwLiquid()}`);
|
|
const pitClouds = run('weather.moist.reduce((a, v) => a + v, 0)');
|
|
const pitSteam = count(run, run('STEAM')); // the pit's floor is hot: some is steam on its way up at any moment
|
|
check('the pit keeps its water (bar what steams off into the clouds)', water0 - count(run, run('WATER')) - pitSteam - pitClouds <= 6, `${water0} -> ${count(run, run('WATER'))}, ${pitSteam} steam, ${pitClouds.toFixed(0)} in the clouds`);
|
|
fresh();
|
|
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_SCREW, 1, bx, 25, true);');
|
|
rect(run, 84, 60, 95, 79, run('WATER'));
|
|
ticks(run, 300);
|
|
check('water poured on a screw runs straight through it', count(run, run('WATER'), 0, 0, 319, 99) === 0 && count(run, run('WATER'), 0, 104, 319, 119) > 200 && screwLiquid() < 10, `${count(run, run('WATER'), 0, 104, 319, 119)} below, ${screwLiquid()} still inside`);
|
|
fresh();
|
|
// a vertical screw 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_SCREW, 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 screw axis.
|
|
for (const horizontal of [false, true]) {
|
|
fresh();
|
|
run(`placeBlock(BP_SCREW, ${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);
|
|
screwIntake(80*W+100, ${horizontal ? 'P_SCREW_R, 1, 0' : 'P_SCREW_U, 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_SCREW, 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 screw 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_SCREW, 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 screw fed from above (its side) fills more than one row', lanes > 1, lanes);
|
|
check('a sideways screw fed from a hopper runs full, all four grains wide', lanes === 4, lanes);
|
|
fresh();
|
|
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_SCREW, 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);
|
|
// screws run at belt pace (every other tick), so a belt under a screw's mouth keeps up with it
|
|
check('a sideways screw 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_SCREW, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_SCREW, 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 screw 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_SCREW, 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 conveyors; 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 barScrew(corner = false) {
|
|
fresh();
|
|
run(`for (let by = 20; by < 24; by++) placeBlock(BP_SCREW, 0, 25, by, true);
|
|
${corner ? 'for (let bx = 25; bx < 29; bx++) placeBlock(BP_SCREW, 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_SCREW,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_SCREW,1,bx,20,true);'); putBar(80,82);
|
|
check('a whole bar leaves a right screw', watchBar(100,'ents.length===1 && ents[0].ix>=96'));
|
|
// steam and smoke drift through screws near furnaces: a bar passes them as it does in the open
|
|
barScrew();
|
|
check('steam and smoke in a screw 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)'));
|
|
barScrew(true);
|
|
check('a bar turns from an up screw into a right screw and leaves', watchBar(200,'ents.length===1 && ents[0].ix>=116'));
|
|
fresh();
|
|
run('for(let bx=20;bx<24;bx++) placeBlock(BP_SCREW,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'));
|
|
barScrew();
|
|
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));
|
|
barScrew(); ticks(run,8);
|
|
run('globalThis.__barSave=serializeWorld(); deserializeWorld(__barSave.meta,__barSave.buffers);');
|
|
check('a bar saved mid-screw 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_SCREW,1,25,25,true); spawnBar(BARCU,100,98,0,0);'); ticks(run,100);
|
|
check('a bar resting on a horizontal screw stays on top',run('ents[0].ix===100 && ents[0].iy===98'));
|
|
|
|
for (const dir of [0, 2]) {
|
|
fresh(); run(`placeBlock(BP_SCREW,${dir},25,20,true); spawnBar(BARCU,100,${dir ? 78 : 84},0,${dir ? 0 : -0.4});`);
|
|
check(dir ? 'a falling bar enters a down screw from above' : 'an upward moving bar enters the bottom of an up screw',watchBar(20,'entScrewAt(ents[0].ix,ents[0].iy)!==0'));
|
|
}
|
|
fresh(); run('for(let bx=20;bx<24;bx++) placeBlock(BP_SCREW,1,bx,20,true); for(let by=21;by<25;by++) placeBlock(BP_SCREW,2,23,by,true);'); putBar(80,82);
|
|
check('a bar turns from a right screw down into a vertical lane',watchBar(160,'ents[0].iy>=100'));
|
|
barScrew(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'));
|
|
barScrew(); 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 screw becomes eight molten grains',run('ents.length===0') && count(run,run('MCU'))===8);
|
|
|
|
done();
|