// The resonance drill: pulses from the bot shake rock until it shatters. Widths trade area for // speed, tuning shatters only one material and drains it out of the wall, built blocks are left // alone, and the drill says why when nothing breaks. const { load, suite, flatWorld, rect, ticks, count } = require('./harness'); const { run } = load(); const { check, done } = suite(); // a stone wall to the right of the bot, from x 124 on, rows 60..99 function wall(m = 'STONE') { flatWorld(run, 240, 160, 100); run(`for (let y = 60; y < 100; y++) for (let x = 124; x < 240; x++) setCell(y * W + x, ${m}, AMB[y], F_NAT);`); run('bot.x = 110; bot.y = 76; stats.dug = 0; drillPulses = []; player.bitTier = 0;'); } const hold = (ax, ay, w, tune, secs) => run(`for (let k = 0; k < ${Math.round(secs * 60)}; k++) { toolPulse(${ax}, ${ay}, ${w}, ${tune || 0}); simTick(); }`); // how deep the hole goes along the aim line, and how many grains it opened in all const depth = () => run('(() => { let d = 0; for (let x = 124; x < 240; x++) { if (PH[mat[78 * W + x]] === PH_SOLID) break; d = x - 123; } return d; })()'); const opened = () => run('(() => { let n = 0; for (let y = 60; y < 100; y++) for (let x = 124; x < 240; x++) if (PH[mat[y * W + x]] !== PH_SOLID) n++; return n; })()'); wall(); run('bot.x = 116;'); hold(150, 78, 1, 0, 2); const med = { d: depth(), n: opened() }; check('held on a stone wall, medium pulses dig a hole and drop rubble', med.d >= 6 && run('stats.dug') > 30 && count(run, run('RSTONE')) > 30, `${med.d} deep, ${med.n} opened, ${count(run, run('RSTONE'))} rubble`); wall(); hold(150, 78, 1, 0, 0.3); check('rock that is shaking but has not broken yet shows it', run('(() => { let n = 0; for (let i = 0; i < N; i++) if (digp[i] > 0 && PH[mat[i]] === PH_SOLID) n++; return n; })()') > 5); // A distant cursor chooses direction; the pulse still ends at its reach. flatWorld(run, 240, 160, 100); run('bot.x = 40; bot.y = 40; player.bitTier = 0; lastPulseTick = -99;'); check('aiming past reach fires without a distance warning', run("toolPulse(220, 42, 1, 0).status") === ''); check('a distant aim still caps the pulse at its reach', run('drillPulses[drillPulses.length - 1].range === drillReach(1)')); // the widths, with a copper bit so the reach is not what stops them const wall2 = () => { wall(); run('player.bitTier = 1;'); }; wall2(); hold(150, 78, 1, 0, 2); const med2 = { d: depth(), n: opened() }; wall2(); hold(150, 78, 0, 0, 2); const nar = { d: depth(), n: opened() }; wall2(); hold(150, 78, 2, 0, 2); const wid = { d: depth(), n: opened() }; check('a narrow pulse bores deeper than a wide one', nar.d > wid.d, `narrow ${nar.d}, medium ${med2.d}, wide ${wid.d} deep`); const rowsOf = () => run('(() => { let a = 99, b = 0; for (let y = 60; y < 100; y++) for (let x = 124; x < 132; x++) if (PH[mat[y * W + x]] !== PH_SOLID) { a = Math.min(a, y); b = Math.max(b, y); } return b - a + 1; })()'); wall2(); hold(150, 78, 2, 0, 2); const wideRows = rowsOf(); wall2(); hold(150, 78, 0, 0, 2); const narRows = rowsOf(); check('the wide cone is taller at the face than the narrow one', wideRows > narRows, `wide ${wideRows} rows, narrow ${narRows}`); // Fixed two-second wall fixture: measured before t92, including normal rubble/gravity. for (const [name, got, baseline] of [['narrow', nar, {d:22,n:181}], ['medium', med2, {d:13,n:186}], ['wide', wid, {d:6,n:134}]]) { check(`${name} stone bore stays within 10% of its depth and opened area`, Math.abs(got.d - baseline.d) <= baseline.d * 0.1 && Math.abs(got.n - baseline.n) <= baseline.n * 0.1, `${got.d} deep / ${got.n} opened (before ${baseline.d} / ${baseline.n})`); } // the reach depends on the width: a narrow beam reaches furthest, a wide fan only close by run('player.bitTier = 0;'); const reaches = [0, 1, 2].map(w => run(`drillReach(${w})`)); check('a narrow pulse reaches further than a medium one, and a medium one further than a wide one', reaches[0] > reaches[1] && reaches[1] > reaches[2], reaches.join(' / ')); // following the face like a player would, narrow bores the fastest tunnel and wide opens the most rock const follow = (w) => { wall2(); run('for (let y = 50; y < 100; y++) for (let x = 124; x < 240; x++) setCell(y * W + x, STONE, AMB[y], F_NAT); bot.x = 116;'); for (let t = 0; t < 12; t++) run(`for (let k = 0; k < 15; k++) { toolPulse(Math.round(bot.x + 42), 78, ${w}, 0); simTick(); } for (let k = 0; k < 40; k++) { if (botOverlapSolid(bot.x + 4, bot.y)) break; bot.x += 1; }`); return { d: depth(), n: opened() }; }; const fN = follow(0), fM = follow(1), fW = follow(2); check('following the face, the narrow tunnel goes furthest', fN.d > fM.d * 1.3 && fM.d > fW.d, `narrow ${fN.d}, medium ${fM.d}, wide ${fW.d} deep`); check('and the wide fan opens up the most rock', fW.n > fN.n && fW.n > fM.n, `narrow ${fN.n}, medium ${fM.n}, wide ${fW.n} grains`); // the fan leaves the whole face of the bot, so rock right beside it is not missed for (const w of [0, 1, 2]) { wall(); run('for (let y = 60; y < 100; y++) for (let x = 124; x < 240; x++) setCell(y * W + x, EMPTY, AMB[y], 0); for (let y = 77; y < 81; y++) for (let x = 121; x < 124; x++) setCell(y * W + x, STONE, AMB[y], F_NAT); bot.x = 116; bot.y = 76;'); const lump = () => count(run, run('STONE'), 121, 60, 123, 81); hold(150, 74, w, 0, 1.5); check(`a ${['narrow', 'medium', 'wide'][w]} pulse aimed a little above a lump touching the bot still breaks it`, lump() <= 2, `${lump()} of 12 left`); } // tuning: copper veins in stone; tuned to copper ore only the veins break, and drain out wall(); run('for (let s = 0; s <= 1; s += 0.01) { const x = Math.round(124 + 20 * s), y = Math.round(80 - 8 * s); for (let dy = -1; dy <= 1; dy++) for (let dx = 0; dx <= 1; dx++) setCell((y + dy) * W + x + dx, CUV, AMB[y], F_NAT); }'); run('player.bitTier = 1; bot.x = 114;'); const stone0 = count(run, run('STONE')), vein0 = count(run, run('CUV')); hold(136, 77, 1, run('CUORE'), 3); const stone1 = count(run, run('STONE')), vein1 = count(run, run('CUV')); check('tuned to copper ore, the stone around the vein is untouched', stone1 === stone0, `stone ${stone0} -> ${stone1}`); check('and the vein shatters', vein1 < vein0 * 0.6, `vein ${vein0} -> ${vein1}`); const out = run('(() => { let n = 0; for (let y = 0; y < 100; y++) for (let x = 0; x < 124; x++) if (mat[y * W + x] === CUORE) n++; return n; })()'); check('shattered ore pours out of the wall toward the bot', out > 10, `${out} ore grains outside the wall`); // built blocks soak pulses up wall(); run('bag[RSTONE] = 200; bagTotal = 200; for (let by = 17; by <= 21; by++) placeBlock(BP_WALL, RSTONE, 30, by, true);'); // a built column at x 120..123 const built0 = run('(() => { let n = 0; for (let y = 68; y < 88; y++) for (let x = 120; x < 124; x++) if (mat[y * W + x] === STONE) n++; return n; })()'); hold(140, 78, 1, 0, 2); const built1 = run('(() => { let n = 0; for (let y = 68; y < 88; y++) for (let x = 120; x < 124; x++) if (mat[y * W + x] === STONE) n++; return n; })()'); check('built blocks in the way are never harmed', built0 > 0 && built1 === built0, `${built0} -> ${built1}`); check('and they shield what is behind them', opened() < med.n / 2, `${opened()} opened behind the wall`); // it says why nothing breaks wall('GRANITE'); const r = JSON.parse(run('JSON.stringify(toolPulse(140, 78, 1, 0))')); check('granite with a stone bit says which bit it needs', /too hard/.test(r.status) && /iron bit/i.test(r.status), r.status); hold(140, 78, 1, 0, 1); check('and the granite stays whole', count(run, run('GRANITE')) === 116 * 40, count(run, run('GRANITE'))); wall(); run('drillPulses = []; tick += 20;'); const far = JSON.parse(run('JSON.stringify(toolPulse(200, 78, 1, 0))')); check('aiming past the reach fires without a warning', far.fired && !far.status, far.status); run('drillPulses = []; tick += 20;'); const farW = JSON.parse(run('JSON.stringify(toolPulse(125, 78, 2, 0))')), nearM = JSON.parse(run('tick += 20; JSON.stringify(toolPulse(125, 78, 1, 0))')); check('aiming beyond a wide pulse also fires without a warning', farW.fired && !farW.status && nearM.fired && !nearM.status); // stray bits: a lump of rock left hanging when the rock round it breaks falls apart const hanging = (m) => { flatWorld(run, 240, 160, 100); run('for (let y = 60; y < 100; y++) for (let x = 140; x < 240; x++) setCell(y * W + x, STONE, AMB[y], F_NAT);'); // a 3 x 3 lump joined to the wall by a line of stone grains run(`player.bitTier = 0; for (let y = 70; y < 73; y++) for (let x = 130; x < 133; x++) setCell(y * W + x, ${m}, AMB[y], F_NAT); for (let x = 133; x < 140; x++) setCell(71 * W + x, STONE, AMB[71], F_NAT);`); run('shatterCell(71 * W + 136, 136, 71, STONE); crumbleAround(136, 71);'); }; hanging('STONE'); check('a bit of stone cut off from the wall falls apart', count(run, run('STONE'), 128, 60, 135, 80) === 0 && count(run, run('RSTONE')) === 13, `${count(run, run('STONE'), 128, 60, 135, 80)} left, ${count(run, run('RSTONE'))} rubble`); check('and the wall it came off stays whole', count(run, run('STONE'), 137, 60, 239, 99) === 100 * 40 + 3, count(run, run('STONE'), 137, 60, 239, 99)); hanging('FEV'); check('a cut-off lump with ore too hard for the bit in it stays whole', count(run, run('FEV')) === 9 && count(run, run('FEORE')) === 0, `${count(run, run('FEV'))} vein, ${count(run, run('FEORE'))} ore`); // too-hard rock is never knocked loose, however small const speckWall = (n) => { flatWorld(run, 240, 160, 100); run('for (let y = 60; y < 100; y++) for (let x = 124; x < 240; x++) setCell(y * W + x, STONE, AMB[y], F_NAT); player.bitTier = 0; drillPulses = [];'); // a lump of iron ore at the face, n grains across, three rows tall, sticking out into the open run(`for (let y = 77; y < 80; y++) for (let x = 122; x < ${122 + n}; x++) setCell(y * W + x, FEV, AMB[y], F_NAT); bot.x = 112; bot.y = 76;`); }; speckWall(2); hold(150, 78, 1, 0, 3); check('a speck of iron ore is never broken or knocked loose by a stone bit', count(run, run('FEV')) === 6 && count(run, run('FEORE')) === 0, `${count(run, run('FEV'))} left, ${count(run, run('FEORE'))} ore`); // world generation leaves no tiny specks of rock harder than what is round them run('generateWorld(424242, 1024, 640)'); // (a speck only counts if it is harder than the rock round it; another sweep finds none) const tiny = run('clearHardSpecks(false)'); check('a new world has no specks of iron, zinc or granite small enough to leave in the way', tiny === 0, `${tiny} specks`); const fe = count(run, run('FEV')); check('and still has plenty of iron ore', fe > 4000, fe); // the bot slips round a lip of rock a grain or two deep instead of stopping dead const corridor = (lip) => { flatWorld(run, 240, 160, 100); run(`for (let y = 50; y < 100; y++) for (let x = 100; x < 240; x++) if (y < 70 || y >= 78) setCell(y * W + x, STONE, AMB[y], F_NAT); for (let y = 70; y < 70 + ${lip}; y++) for (let x = 140; x < 143; x++) setCell(y * W + x, STONE, AMB[y], F_NAT); bot.x = 110; bot.y = 70; bot.vx = bot.vy = 0;`); for (let k = 0; k < 150; k++) run('botStep(1, 0, false)'); return run('bot.x'); }; const past2 = corridor(2), past8 = corridor(8); check('flying along a tunnel, the bot slips past a two-grain lip', past2 > 150, past2.toFixed(1)); check('but a wall across the tunnel still stops it', past8 < 140, past8.toFixed(1)); // trees in the way come apart into wood chunks flatWorld(run, 240, 160, 100); run('for (let y = 80; y < 100; y++) for (let x = 120; x < 122; x++) bgm[y * W + x] = WOOD; findTreeBases(); bot.x = 108; bot.y = 88; player.bitTier = 0;'); hold(121, 92, 1, 0, 3); check('a tree trunk in the path is cut into wood chunks', count(run, run('LWOOD')) > 0, count(run, run('LWOOD'))); // Melts that cool loose in the world (magma from the bag, metal spilled outside a mold) are // ordinary solids the drill can break; built walls, slope corner cells and molds stay protected. flatWorld(run, 160, 128, 120); const melt = run(`(() => { player.bitTier = BIT_TIERS.length - 1; bot.x = 60; bot.y = 60; for (let y = 80; y < 88; y++) for (let x = 60; x < 72; x++) setCell(y * W + x, MAGMA, 1250, 0); for (let k = 0; k < 2400; k++) simTick(); for (let x = 90; x < 96; x++) setCell(110 * W + x, COPPER, 20, F_CAST); // metal that set outside a mold let bas = 0, nat = 0, cu = 0, drillable = 0, cuDrill = 0; for (let i = 0; i < N; i++) { if (mat[i] === BASALT) { bas++; if (flags[i] & F_NAT) nat++; if (crumbly(i)) drillable++; } if (mat[i] === COPPER) { cu++; if (crumbly(i)) cuDrill++; } } return { bas, nat, drillable, cu, cuDrill }; })()`); check('magma from the bag cools into natural basalt', melt.bas > 0 && melt.nat === melt.bas, JSON.stringify(melt)); check('that basalt can be drilled', melt.drillable === melt.bas, JSON.stringify(melt)); check('copper that set outside a mold can be drilled for scrap', melt.cu > 0 && melt.cuDrill === melt.cu, JSON.stringify(melt)); check('basalt already stuck without the natural mark can be drilled too', run('(() => { setCell(20 * W + 20, BASALT, 20, 0); player.bitTier = BIT_TIERS.length - 1; return crumbly(20 * W + 20); })()')); check('built walls and slope corner cells stay safe from the drill', run(`(() => { placeBlock(BP_WALL, RSTONE, 5, 5, true); placeBlock(BP_SLOPE_BL, RSTONE, 8, 5, true); const wallCell = 5 * BS * W + 5 * BS + 1; let seal = -1; for (let i = 0; i < N; i++) if (slopeSealOwner(i) >= 0 && PH[mat[i]] === PH_SOLID) { seal = i; break; } return !crumbly(wallCell) && (seal < 0 || !crumbly(seal)); })()`)); check('a mold and what sets inside it stay safe from the drill', run(`(() => { bag[RSTONE] = 400; bagTotal = 400; const err = placeMoldAt('ingot', 30, 10, RSTONE); const mo = molds[molds.length - 1]; if (err || !mo) return false; for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) if (crumbly(i)) return false; return true; })()`)); // No gravity or kicks here: measure absorption through a fixed thickness, beyond normal reach // for the 115-grain case. All real medium rays still cross the powder before reaching basalt. function rubbleShield(thickness) { flatWorld(run, 240, 160, 150); return run(`(() => { RS = 12345; tick = 0; lastPulseTick = -99; drillPulses = []; player.bitTier = 3; bot.x = 10; bot.y = 76; for (let y = 10; y < 145; y++) { for (let x = 20; x < 20 + ${thickness}; x++) setCell(y*W+x, RSTONE, 20, 0); for (let x = 20 + ${thickness}; x < 220; x++) setCell(y*W+x, BASALT, 20, F_NAT); } const before = stats.dug; for (let t = 0; t < 240; t++) { tick++; tickStamp = tickStamp % 255 + 1; stamp.fill(tickStamp); toolPulse(219, 78, 1, 0); if (${thickness} > 46) for (const p of drillPulses) p.range = 180; pulseTick(); } return stats.dug - before; })()`); } const through35 = rubbleShield(35), through115 = rubbleShield(115); check('steel medium pulses shatter basalt behind 35 grains of rubble', through35 > 0, `${through35} basalt opened`); check('115 grains of rubble prevent basalt breaking during sustained drilling', through115 === 0, `${through115} basalt opened`); // 1.4 * 0.97^distance crosses 0.04 at about 117 grains; check actual extinction too. const through118 = rubbleShield(118); check('118 grains extinguish the ray before it can even shake basalt', through118 === 0 && !run('digp.some(d => d > 0)'), `${through118} basalt opened`); // Absorption over a fixed distance, isolated from gravity, kicks and cell breakage. function transmission(material, distance, step, tune = 0) { flatWorld(run, 240, 160, 150); return run(`(() => { bot.x = 0; bot.y = 0; player.bitTier = 0; stamp.fill(tickStamp); for (let y = 70; y < 90; y++) for (let x = 10; x < 180; x++) setCell(y*W+x, ${material}, 20, F_NAT); const p = {ox:20,oy:80.5,ang:0,w:1,jit:0.5,n:1,damp:PULSE_W[1].damp, en:new Float32Array([1.4]),s:new Float32Array(1),r:200,range:${distance},tune:${tune}}; drillPulses = [p]; pulseTick(${step}); return p.en[0]; })()`); } for (const [m, distance, factor, tune] of [ ['RSTONE',35,0.97,0], ['SAND',35,0.97,0], ['FEORE',35,0.97,0], ['COAL',35,0.97,0], ['WATER',10,0.985**2,0], ['GRANITE',1,0.35**2,0], ['STONE',10,0.993**2,'CUORE'], ['STONE',1,0.69,0], ['STONE',1,0.96**2,'RSTONE'], ['BELTL',1,0.25**2,0], ]) { const energies = [0.25,0.5,1].map(step => transmission(m,distance,step,tune)); const expected = 1.4 * factor ** distance; check(`${m} attenuation counts distance at quarter, half and whole-grain steps (tune ${tune})`, energies.every(e => Math.abs(e-expected) < 0.00001), energies.map(e=>e.toFixed(5)).join(' / ')); } // A settled heap must slump further than an undrilled control, conserving all its grains. function shakenPile(pulse) { flatWorld(run,240,160,110); run(`RS=12345; tick=0; lastPulseTick=-99; drillPulses=[]; player.bitTier=3; bot.x=40; bot.y=88; for(let x=65;x<116;x++) for(let y=85+Math.abs(x-90);y<110;y++) setCell(y*W+x,RSTONE,20,0); for(let t=0;t<240;t++) simTick();`); const height = () => run(`(() => {let top=110;for(let i=0;i<110*W;i++) if(mat[i]===RSTONE) top=Math.min(top,(i/W)|0);return 110-top;})()`); const before=height(), grains=count(run,run('RSTONE')); run(`for(let t=0;t<480;t++) {${pulse?'toolPulse(90,94,1,0);':''}simTick();}`); return {before,after:height(),grains,left:count(run,run('RSTONE'))}; } const restingPile=shakenPile(false), fluidPile=shakenPile(true); check('pulses fluidise a settled rubble pile so its height drops', fluidPile.after <= fluidPile.before-3 && fluidPile.after < restingPile.after-3, `resting ${restingPile.before}->${restingPile.after}, shaken ${fluidPile.before}->${fluidPile.after}`); check('shaking conserves the pile material',fluidPile.grains===fluidPile.left,`${fluidPile.grains}->${fluidPile.left}`); check('repeating a seeded pile gives the same result',JSON.stringify(shakenPile(true))===JSON.stringify(fluidPile)); // One artificially long ray crosses many exposed grains in a single tick, exercising the caps. flatWorld(run,240,160,150); const kicks = run(`(() => { RS=12345; bot.x=0;bot.y=0;tickStamp=1; EV.length=0; for(let x=20;x<120;x+=2) setCell(80*W+x,RSTONE,20,0); // Neither a covered grain nor a grain already moved this tick may be kicked. occ[80*W+20]=1; part[80*W+22]=P_PIPE; stamp[80*W+24]=tickStamp; // An open but owned destination and one stamped this tick are also off limits. part[80*W+25]=P_PIPE; occ[80*W+27]=1; stamp[80*W+29]=tickStamp; const p={ox:19.5,oy:80.5,ang:0,w:1,jit:0.5,n:8,damp:PULSE_W[1].damp, en:new Float32Array(8).fill(1.4),s:new Float32Array(8),r:200,range:100,tune:0}; // Zero spread gives eight overlapping rays, testing the per-tick dust cap as well. const half=PULSE_W[1].half,mouth=PULSE_W[1].mouth; PULSE_W[1].half=0;PULSE_W[1].mouth=0; drillPulses=[p]; pulseTick(); PULSE_W[1].half=half;PULSE_W[1].mouth=mouth; let moved=0; for(let i=0;ie.t==='debris').length, protected:mat[80*W+20]===RSTONE && mat[80*W+22]===RSTONE && mat[80*W+24]===RSTONE, blocked:mat[80*W+25]===EMPTY && mat[80*W+27]===EMPTY && mat[80*W+29]===EMPTY}; })()`); check('kicks are bounded to three per ray and dust to four puffs per tick',kicks.moved>0 && kicks.moved<=24 && kicks.dust>0 && kicks.dust<=4,JSON.stringify(kicks)); check('shaking respects occupied, owned and stamped cells',kicks.protected && kicks.blocked,JSON.stringify(kicks)); // Resonance ping (t50): finds natural veins within reach of the bit, under fog too, and // nothing else. flatWorld(run, 240, 160, 150); const ping = run(`(() => { player.bitTier = 0; const cx = 100, cy = 100, r = pingRadius(); setCell(cy * W + cx + 10, CUV, 20, F_NAT); setCell(cy * W + cx - 20, FEV, 20, F_NAT); setCell(cy * W + cx + r + 5, AUV, 20, F_NAT); // out of reach setCell((cy + 5) * W + cx, CUV, 20, 0); // not natural (set loose) setCell((cy - 5) * W + cx, STONE, 20, F_NAT); // not a vein const p = resonancePing(cx, cy); player.bitTier = BIT_TIERS.length - 1; const far = resonancePing(cx, cy); return { n: p.cells.length, found: p.found.map(m => MD[m].name), r, farFound: far.found.map(m => MD[m].name), farR: far.r }; })()`); check('a ping finds natural veins in reach', ping.n === 2 && ping.found.includes('Copper ore vein') && ping.found.includes('Iron ore vein'), JSON.stringify(ping)); check('it skips rock, loose ore and veins out of reach', !ping.found.includes('Gold ore vein') && ping.n === 2, JSON.stringify(ping)); check('a better bit pings farther', ping.farR > ping.r && ping.farFound.includes('Gold ore vein'), JSON.stringify(ping)); check('a ping never changes the world', run('(() => { const h0 = mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); resonancePing(100, 100); return h0 === mat.reduce((a, m, i) => (a * 31 + m + i) | 0, 0); })()')); done();