Drill pulses pass through rubble and fluidise it; per-grain damping (t92)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 18:09:42 -04:00
co-authored by Claude Opus 5.5
parent 90f2d3f974
commit 729378770a
4 changed files with 172 additions and 27 deletions
+97
View File
@@ -35,6 +35,13 @@ 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})`));
@@ -171,6 +178,96 @@ check('a mold and what sets inside it stay safe from the drill', run(`(() => {
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;i<N;i++) if(mat[i]===RSTONE && stamp[i]===tickStamp) moved++;
return {moved:moved-1,dust:EV.filter(e=>e.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);