// Uses for waste rock and sand: glass bricks from a fan furnace, the Saw, the silo and // builders' orders. const { load, suite, flatWorld, count } = require('./harness'); const { run } = load(); const { check, done } = suite(); const bars = (m) => run(`ents.filter(e => e.m === ${m}).length`); // ---- glass: a brick furnace over an ingot mold, a fan blowing in through a side grate, // charged with sand and coal flatWorld(run, 240, 160, 100); run('money = 9999; bot.x = 60; bot.y = 40;'); const bp = JSON.parse(run("JSON.stringify(smelterBlueprint('ingot', 1, 3, RBRICK))")); const ox = 24, oy = 25 - bp.bh; run(`(() => { const bp = ${JSON.stringify(bp)}; for (const [dx, dy, code, v] of bp.blocks) placeBlock(code, v, ${ox} + dx, ${oy} + dy, true); for (const [k, dx, dy, wl] of bp.molds) addMold(k, ${ox} + dx, ${oy} + dy, wl, true); // a grate low in the left wall, and a fan a block out from it blowing in removeBlock(${ox}, ${oy} + 3); placeBlock(BP_GRATE, 0, ${ox}, ${oy} + 3, true); addMachine('fan', ${ox} - 2, ${oy} + 3, 0); })()`); const cx0 = (ox + 1) * 4, cx1 = (ox + bp.bw - 1) * 4 - 1, cy0 = (oy + 1) * 4, cy1 = (oy + 4) * 4 - 1; run(`{ const r = mulberry32(9); for (let y = ${cy0 + 5}; y <= ${cy1}; y++) for (let x = ${cx0}; x <= ${cx1}; x++) if (mat[y * W + x] === EMPTY) setCell(y * W + x, r() < 0.5 ? SAND : COAL, AMB[y], 0); }`); const sand0 = count(run, run('SAND')); run(`for (let k = 0; k < 60; k++) { toolIgnite(${cx0 + 3}, ${cy0 + 5}); toolIgnite(${cx1 - 3}, ${cy0 + 5}); simTick(); }`); let peak = 0; for (let t = 0; t < 6; t++) { run('for (let k = 0; k < 600; k++) simTick();'); peak = Math.max(peak, run(`(() => { let p = 0; for (let y = ${cy0}; y <= ${cy1}; y++) for (let x = ${cx0}; x <= ${cx1}; x++) p = Math.max(p, temp[y * W + x]); return Math.round(p); })()`)); } const st = run('molds[0].state'), glassIn = run('molds[0].cavs.flat().filter(i => mat[i] === GLASS).length'); check('a fan furnace melts the sand and the molten glass runs into the ingot mold', glassIn >= 8, `peak ${peak}°C, ${sand0} sand -> ${count(run, run('SAND'))}, ${glassIn} glass in the mold, state ${st}`); run('{ const mo = molds[0]; for (let y = mo.y + mo.h; y < mo.y + mo.h + 2; y++) for (let x = mo.x; x < mo.x + mo.w; x++) setCell(y * W + x, EMPTY, AMB[y], 0); } checkMolds();'); check('opening space below drops glass bricks', bars(run('BARGL')) >= 2 && run('ents.every(e => e.iy >= molds[0].y + molds[0].h)'), `${bars(run('BARGL'))} bricks`); check('glass bricks sell for much more than the sand in them', run('PRICE[BARGL]') >= 8 * run('PRICE[SAND]') * 10, run('PRICE[BARGL]')); check('glass in a bit mold is too brittle to make a bit', run("METAL_TIER[GLASS]") === 0); // ---- Saw stone blocks ride a belt and build walls flatWorld(run, 240, 160, 100); run("money = 9999; bot.x = 60; bot.y = 40; stats.bricks = 0; placeBlock(BP_SAW, 0, 26, 19, true); for (let bx = 24; bx <= 34; bx++) placeBlock(BP_BELT, 1, bx, 21, true);"); run('for (let k = 0; k < 100; k++) { setCell(75 * W + 104, RSTONE, AMB[75], 0); stamp[75 * W + 104] = 0; for (let t = 0; t < 4; t++) { tick++; tickStamp++; procBlocksTick(); } for (let t = 0; t < 10; t++) simTick(); } for (let k = 0; k < 300; k++) simTick();'); check('Saw stone blocks ride a belt away from under it', run('ents.filter(e => e.m === BARSB && e.ix > 120).length') >= 3, run('JSON.stringify(ents.filter(e => e.m === BARSB).map(e => e.ix))')); check('stone blocks build cut stone walls, 2 a block', run('WALL_OF[BARSB]') === run('CUTST') && JSON.stringify(run('wallCost(BARSB, 16)')) === JSON.stringify([[run('BARSB'), 2]])); check('cut stone stands more heat than rough stone', run('MELT[CUTST]') > run('MELT[STONE]')); run('bag[BARSB] = 4; bagTotal = 4; placeBlock(BP_WALL, BARSB, 10, 20, false);'); const wallOk = count(run, run('CUTST')) === 16 && run('bag[BARSB]') === 2; run('removeBlock(10, 20)'); check('a cut stone wall is laid from 2 bricks and gives them back', wallOk && run('bag[BARSB] + ents.filter(e => e.m === BARSB).length') >= 4 - 0, `${run('bag[BARSB]')} in the bag`); // ---- the silo: one material in through the hatch, out of the gate when it is open flatWorld(run, 240, 160, 100); run("money = 9999; bot.x = 60; bot.y = 30; placeMachineAt('silo', 0, 26, 1);"); // x 104..115, y 4..27; gate at y 27 const silo = 'machines.find(m => m && m.type === "silo")'; run('for (let k = 0; k < 30; k++) { for (let x = 107; x < 113; x++) setCell(2 * W + x, RSTONE, AMB[2], 0); simTick(); }'); check('grains dropped in its hatch go into the silo', run(`${silo}.sn`) >= 150 && run(`${silo}.sm`) === run('RSTONE'), run(`${silo}.sn`)); run('for (let x = 107; x < 113; x++) setCell(2 * W + x, SAND, AMB[2], 0); for (let k = 0; k < 20; k++) simTick();'); check('a different material does not go in on top', run(`${silo}.sm`) === run('RSTONE') && count(run, run('SAND'), 104, 0, 115, 6) > 0); check('it will not be taken down while it holds something', /Empty the silo/.test(run('removePieceAt(110, 12)')) && run(`!!${silo}`)); const held = run(`${silo}.sn`); run('interactAt(110, 12); for (let k = 0; k < 60; k++) simTick();'); const out1 = count(run, run('RSTONE'), 100, 27, 120, 99); check('F opens the gate and it pours out underneath', out1 > 30 && run(`${silo}.sn`) === held - out1, `${out1} out, ${run(`${silo}.sn`)} left of ${held}`); run('interactAt(110, 12); for (let k = 0; k < 60; k++) simTick();'); check('F shuts it again', count(run, run('RSTONE'), 100, 27, 120, 99) === out1); run('(() => { const s = serializeWorld(); deserializeWorld(JSON.parse(JSON.stringify(s.meta)), s.buffers); })()'); check('what is in a silo is saved', run(`${silo}.sn`) === held - out1 && run(`${silo}.sm`) === run('RSTONE')); // ---- block hoppers: feed the top edge, clear the outlet between jobs function hopper(code, stack = false) { flatWorld(run, 240, 160, 120); run(`money = 9999; stats.refined = 0; stats.bricks = 0; stats.planks = 0; placeBlock(${code}, 0, 20, 18, true); ${stack ? 'placeBlock(BP_GRIND, 0, 20, 19, true);' : ''}`); } function feedHopper(m, n, bottom = 76, clearItems = true) { run(`${clearItems ? "for (const e of ents.slice()) removeEnt(e);" : ""} for (let k = 0; k < ${n}; k++) { for (let y = ${bottom}; y < ${bottom}+2; y++) for (let x = 80; x < 84; x++) setCell(y*W+x, EMPTY, 20, 0); setCell(71*W+80, ${m}, 60, 0); stamp[71*W+80] = 0; for (let t = 0; t < 4; t++) { tick++; tickStamp++; procBlocksTick(); } }`); } hopper('BP_SAW'); feedHopper('RSTONE', 8); check('a Saw cuts eight stone grains into one stone block below', run('stats.bricks === 1 && ents[0].iy === 76')); hopper('BP_SAW'); feedHopper('LWOOD', 8); check('a Saw cuts eight wood chunks into one plank below', run('stats.planks === 1 && ents.length === 1 && ents[0].m === PLANK && ents[0].iy === 76 && seen[PLANK]')); check('two planks pay for a wood wall block', JSON.stringify(run('wallCost(PLANK, 16)')) === JSON.stringify([[run('PLANK'), 2]])); run('bag[PLANK] = 4; bagTotal = 4; bot.x = 40; bot.y = 80; placeBlock(BP_WALL, PLANK, 10, 20, false);'); check('placing a plank wall builds wood and uses two planks', count(run, run('WOOD')) === 16 && run('bag[PLANK] === 2')); run('removeBlock(10, 20);'); check('removing a plank wall refunds its two planks', run('bag[PLANK] === 4') && count(run, run('WOOD')) === 0); check('cut stone is named Stone block', run('MD[BARSB].name') === 'Stone block'); hopper('BP_SAW'); feedHopper('RSTONE', 5); feedHopper('LWOOD', 5); feedHopper('RSTONE', 3); check('mixed feed finishes only the stone block first', run('stats.bricks === 1 && stats.planks === 0 && ents.length === 1 && ents[0].m === BARSB && procState.get(18*BW+20).cut[LWOOD] === 5')); run('entMark(ents[0], 0); ents[0].ix = 96; ents[0].x = 96; entMark(ents[0], 1);'); feedHopper('LWOOD', 3, 76, false); check('mixed feed keeps separate wood and stone counts', run('stats.bricks === 1 && stats.planks === 1 && ents.length === 2 && ents.filter(e => e.m === BARSB).length === 1 && ents.filter(e => e.m === PLANK).length === 1')); feedHopper('CUORE', 1); check('a Saw passes other grains through at their temperature', run('mat[76*W+81] === CUORE && temp[76*W+81] === 60')); hopper('BP_SAW'); run('for (let x = 80; x < 84; x++) setCell(76*W+x, STONE, 20, 0); setCell(71*W+80, RSTONE, 20, 0); stamp[71*W+80] = 0; for (let k = 0; k < 8; k++) { tick++; tickStamp++; procBlocksTick(); }'); check('a full outlet stops a Saw before consuming stone', run('mat[71*W+80] === RSTONE && procState.get(18*BW+20).blocked')); hopper('BP_SAW'); feedHopper('LWOOD', 7); run('setCell(77*W+80, STONE, 20, 0); setCell(71*W+80, LWOOD, 20, 0); stamp[71*W+80] = 0; for (let k = 0; k < 8; k++) { tick++; tickStamp++; procBlocksTick(); }'); check('a Saw holds the eighth wood chunk until a whole plank fits', run('mat[71*W+80] === LWOOD && procState.get(18*BW+20).cut[LWOOD] === 7 && procState.get(18*BW+20).blocked && stats.planks === 0')); hopper('BP_SAW', true); feedHopper('LWOOD', 8, 80); check('a plank passes the Grinder and drops below the stack', run('stats.planks === 1 && stats.refined === 0 && ents[0].m === PLANK && ents[0].iy === 80')); hopper('BP_GRIND'); feedHopper('RSTONE', 200); check('a Grinder counts every ground grain and reduces rubble', run('stats.refined === 200') && count(run, run('RSTONE')) === 0); hopper('BP_GRIND'); run('placeBlock(BP_GRIND, 0, 21, 18, true); for (let k = 0; k < 80; k++) { for (let x = 80; x < 88; x++) { setCell(71*W+x, GRAVEL, 20, 0); stamp[71*W+x] = 0; setCell(76*W+x, EMPTY, 20, 0); } tick++; tickStamp++; procBlocksTick(); }'); check('two Grinders work twice one block rate', run('stats.refined === 40'), run('stats.refined')); hopper('BP_SAW', true); feedHopper('RSTONE', 8, 80); check('a Saw over a Grinder drops its brick under the stack', run('stats.bricks === 1 && ents[0].iy === 80')); feedHopper('GRAVEL', 40, 80); check('gravel passes through the Saw and is ground below', run('stats.refined === 40')); run('removeBlock(20, 18); removeBlock(20, 19);'); check('removing hoppers refunds costs, clears cells and state', run('money === 10149 && mat[72*W+80] === EMPTY && mat[76*W+80] === EMPTY && procState.size === 0')); hopper('BP_SAW'); feedHopper('RSTONE', 3); run('(() => { const s = serializeWorld(); deserializeWorld(s.meta, s.buffers); })(); procBlocksTick();'); check('loading rebuilds processors and drops partial brick state', run('procBlocks.length === 1 && procState.get(18*BW+20).cut[RSTONE] === 0 && procState.get(18*BW+20).cut[LWOOD] === 0 && SAVE_VERSION === 2')); // ---- removed machines in old saves are refunded and leave no hull or machine ids flatWorld(run, 240, 160, 100); run("money = 1000; addMachine('fan', 10, 10, 0); placeDock(160);"); for (const [type, refund] of [['refiner', 120], ['cutter', 90], ['unknown', 0]]) { const result = run(`(() => { const saved = serializeWorld(), meta = JSON.parse(JSON.stringify(saved.meta)); const before = machines.filter(Boolean).length, cash = money; meta.machines.push({ type: '${type}', bx: 20, by: 10, dir: 0, on: true }); saved.buffers.mat[40 * W + 80] = HULL; deserializeWorld(meta, saved.buffers); return [machines.filter(Boolean).length === before, money - cash, mat[40 * W + 80] === EMPTY && mach[40 * W + 80] === 0]; })()`); check(`loading a saved ${type} skips it and refunds exactly $${refund}`, result[0] && result[1] === refund && result[2], result); } // ---- builders' orders: bulk rubble, gravel and stone blocks at a few times a tiny price flatWorld(run, 240, 160, 100); run('seen[PLANK] = 0; seen[RSTONE] = 1; seen[GRAVEL] = 1; seen[BARSB] = 1; orders = []; ordersDone = 0;'); const os = JSON.parse(run('JSON.stringify(Array.from({ length: 40 }, () => makeBuildersOrder()))')); const okOrders = os.every(o => o && o.who === 'Builders' && [run('RSTONE'), run('GRAVEL'), run('SAND'), run('BARSB')].includes(o.m) && o.pay > 0 && o.pay < 400); check("builders post orders for rubble, gravel and stone blocks that pay a little", okOrders, os.slice(0, 4).map(o => `${o.qty} of ${o.m} for $${o.pay}`).join(', ')); run('seen[PLANK] = 1; orders = BUILD_GOODS.filter(([m]) => m !== PLANK).map(([m]) => ({ m }));'); check('builders buy planks in lots of 20 to 80', run('(() => { const o = makeBuildersOrder(); return o.m === PLANK && o.qty >= 20 && o.qty <= 80 && o.pay > 0 && Math.abs(PRICE[PLANK] - 2.4) < 0.00001; })()')); run('orders = [];'); const regular = JSON.parse(run('JSON.stringify(Array.from({ length: 60 }, () => { const o = makeOrder(); return o && !o.who ? o.m : 0; }))')); check('ordinary buyers never ask for thousands of grains of rubble', !regular.includes(run('RSTONE')) && !regular.includes(run('GRAVEL'))); done();