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:
+3
-3
@@ -327,7 +327,7 @@ Tool helpers: reach, brushes, and plain digging.
|
||||
- functions: setBuildRange, inBuildRange, inReach, forBrush, drillReach, inDrillReach, toolDig
|
||||
- variables: BUILD_MAX, buildBox
|
||||
|
||||
### src/sim/drill.js (177 lines)
|
||||
### src/sim/drill.js (187 lines)
|
||||
The resonance drill: tuned pulses that shatter one material.
|
||||
- functions: drillOrigin, pulseRay, builtCell, toolPulse, crumbleAround, crumbly, shatterCell, pulseTick, pingRadius, resonancePing
|
||||
- variables: PULSE_W, PULSE_EVERY, PULSE_SPEED, PULSE_K, drillPulses, lastPulseTick, drillWidth, THIN_BONUS, CRUMBLE_MAX, THIN_BONUS_OFF, PING_VEINS
|
||||
@@ -443,7 +443,7 @@ GPU sources retain the CPU roof and transmission rules; directional relaxation f
|
||||
Smooth slope geometry and cleats, ported from slope-belt-shader.js.
|
||||
- variables: GPU_SMOOTH_SLOPE_HELPERS, GPU_SMOOTH_SLOPE_DRAW
|
||||
|
||||
### src/render/gpu/smooth-shader.js (368 lines)
|
||||
### src/render/gpu/smooth-shader.js (369 lines)
|
||||
Literal port of the legacy smooth shader; dpdy is reversed for WebGPU's top-left origin.
|
||||
- variables: GPU_SMOOTH
|
||||
|
||||
@@ -648,7 +648,7 @@ The frame loop and boot.
|
||||
- `test/t_fog.js`: Fog: surface, sight, range, saves and simulation independence.
|
||||
- `test/t_furnace_repeat.js`: The starter furnaces keep working when fired again and again without cleaning, however the charge goes in: mixed, ore first with coal on top, or blown in through the vent. Each case plays furnace_play.js, which builds the design and fires it like a player.
|
||||
- `test/t_gpu.py`: Dirty uploads, default limits, warm pipelines, world replacement and device recovery.
|
||||
- `test/t_gpu_golden.py`: Frozen legacy references, captured before retirement; compare the WebGPU world only.
|
||||
- `test/t_gpu_golden.py`: Frozen world references; seven smooth crops corrected for continuous noise in t91.
|
||||
- `test/t_gpu_light.py`: The former CPU light behavioural checks, now read from the actual GPU light grid.
|
||||
- `test/t_joins.js`: The oracle states the geometric rule independently of the precomputed neighbour tables.
|
||||
- `test/t_load.js`: Reload wakes nearby areas first, without losing distant water, fire or saved state.
|
||||
|
||||
@@ -4,6 +4,44 @@ What agents have done, newest first. Add an entry for every piece of work: the d
|
||||
note or request it was for, what changed, the files touched, the tests run, and anything left
|
||||
open or not verified. Keep each entry short; the git history has the details.
|
||||
|
||||
## 2026-10-28: t92 drill pulses pass through rubble and fluidise it (Codex)
|
||||
|
||||
- Converted every pulse loss to a per-grain factor, applied with `factor ** step` outside
|
||||
the ray sampling loop. Powder keeps 0.97 per grain; liquid, built, hard and tuned paths
|
||||
preserve their former squared half-grain factors. Tree damping and shake now scale with
|
||||
distance too. Rock damping retuned to 0.76/0.69/0.6084 per grain for narrow/medium/wide.
|
||||
- Replaced the botward nudge with random sideways/upward one-cell hops through open cells,
|
||||
using `rnd`, `move`, stamps, occupancy, part/cleat rules and sealed corners. At most three
|
||||
moves per ray/tick. Up to four shake debris events/tick reuse the existing particles;
|
||||
no new rendering or UI code, dependencies, save fields or design decisions.
|
||||
- Added drill regressions: 35-grain shield failed before (0 basalt opened), now opens 78
|
||||
basalt cells with the real steel medium reach; 115 grains opens none. Only the latter
|
||||
fixture extends range beyond the bit's reach, isolating attenuation from range.
|
||||
Exact 3% loss crosses the cutoff near 117 grains; an additional 118-grain check
|
||||
confirms extinction before any basalt damage. Fixed
|
||||
powder transmission is 0.4821/1.4 = 34.44%; quarter/half/whole-grain attenuation agrees
|
||||
for rubble, sand, ore, coal, liquids, rock, tuned skips/hits and built material.
|
||||
Seeded heap height drops 25 -> 20 in eight seconds versus control 25 -> 25, conserving
|
||||
625 grains; repeatability, movement protection, three-move/ray and four-event caps pass.
|
||||
- Before/after two-second copper-bit stone-wall fixture, depth/opened cells: narrow
|
||||
22/181 -> 24/195 (+9.1%/+7.7%), medium 13/186 -> 13/197 (0%/+5.9%), wide
|
||||
6/134 -> 6/134. Existing follow-face, width, tuning, shielding and tree checks retained.
|
||||
Fixed powder-ray microbenchmark median: 21.48 -> 21.28 ms per 20,000 calls (five warm
|
||||
samples); this is an isolated pulse cost check, not a whole-game performance measurement.
|
||||
- Files: `src/sim/drill.js`, `test/t_drill.js`, generated `docs/PROJECT_MAP.md`, t92 task
|
||||
tracking and this log. No commit. Verification: focused drill suite passes all 61 checks;
|
||||
initial full run passed all 32 headless suites but seven browser suites failed under the
|
||||
sandbox's unavailable WebGPU adapter. Host-GPU `python3 test/run_all.py --browser` exited
|
||||
0, ALL PASSED (all 32 headless suites; gameplay 361, smooth 28, console 18, caps 7,
|
||||
GPU 14, goldens 28, light 71 and Firefox 2). The additional 118-grain extinction check
|
||||
was then verified in the focused 61-check suite. Logs: `/tmp/t92-drill-before.log`,
|
||||
`/tmp/t92-drill-red.log`, `/tmp/t92-drill-after.log`, `/tmp/t92-all-tests-host.log`.
|
||||
Inspected `shots/t92-pile-before-pixel.png`, `t92-pile-after-pixel.png` and
|
||||
`t92-pile-after-smooth.png`: the drilled side is lower and flatter, with small dust puffs
|
||||
close to the surface. Setup scripts retained in `/tmp/t92-scene-before.js` and
|
||||
`/tmp/t92-scene-after.js`; capture with `tools/shot.py --setup FILE --zoom 10 --no-light
|
||||
--freeze` (add `--look smooth` for smooth). Task moved to Review; owner playtesting remains.
|
||||
|
||||
## 2026-10-28: t91 sky renders as mismatched blocks (Codex)
|
||||
|
||||
- Reproduced the owner's cloud rectangles and stepped distant hills on NVIDIA Blackwell:
|
||||
|
||||
+34
-24
@@ -8,14 +8,15 @@
|
||||
// medium: a cone, the all-rounder
|
||||
// wide: a broad fan that clears a big face close to the bot, but reaches only a short way
|
||||
// Tuned to one material, a pulse passes through everything else and shatters only that, and
|
||||
// loose grains it passes are jostled toward the bot, so a shattered vein pours out of the wall.
|
||||
// loose grains it passes are shaken into short hops, so piles flow and pockets drain.
|
||||
// Built blocks, machines and molds soak pulses up unharmed.
|
||||
// half: half the spread angle; mouth: half the width of the fan where it leaves the bot;
|
||||
// en: energy per ray; damp: energy kept after each shake (higher bores deeper); reach: x bit reach
|
||||
// en: energy per ray; damp: energy kept per grain (higher bores deeper); reach: x bit reach
|
||||
// Narrow/medium compensate for rubble now passing pulses; wide keeps its old squared loss.
|
||||
const PULSE_W = [
|
||||
{ name: 'narrow', half: 0.03, mouth: 3.2, rays: 13, en: 1.75, damp: 0.9, reach: 1.45 },
|
||||
{ name: 'medium', half: 0.24, mouth: 3, rays: 23, en: 1.4, damp: 0.84, reach: 1 },
|
||||
{ name: 'wide', half: 0.62, mouth: 3.4, rays: 41, en: 1.32, damp: 0.78, reach: 0.7 },
|
||||
{ name: 'narrow', half: 0.03, mouth: 3.2, rays: 13, en: 1.75, damp: 0.76, reach: 1.45 },
|
||||
{ name: 'medium', half: 0.24, mouth: 3, rays: 23, en: 1.4, damp: 0.69, reach: 1 },
|
||||
{ name: 'wide', half: 0.62, mouth: 3.4, rays: 41, en: 1.32, damp: 0.6084, reach: 0.7 },
|
||||
];
|
||||
const PULSE_EVERY = 8, PULSE_SPEED = 2.2, PULSE_K = 240; // ticks between pulses, cells per tick, shake per unit of energy
|
||||
let drillPulses = [], lastPulseTick = -99, drillWidth = 1;
|
||||
@@ -97,49 +98,58 @@ function shatterCell(i, x, y, m) {
|
||||
if ((stats.dug & 7) === 0) emit({ t: 'sfx', s: 'dig', a: HARD[m] });
|
||||
}
|
||||
// Advance every pulse in flight by one tick
|
||||
function pulseTick() {
|
||||
function pulseTick(step = 0.5) {
|
||||
if (!drillPulses.length) return;
|
||||
const tier = BIT_TIERS[player.bitTier];
|
||||
// Per-grain losses; squared old half-grain factors preserve the rock/built/liquid paths.
|
||||
const loose = 0.97 ** step, liquid = 0.970225 ** step, built = 0.0625 ** step;
|
||||
const skip = 0.986049 ** step, hard = 0.1225 ** step, tuned = 0.9216 ** step;
|
||||
const wood = 0.85 ** step, leaves = 0.97 ** step;
|
||||
let dust = 0;
|
||||
// cells inside the bot's body are not touched (it flies in its own little space)
|
||||
const bx0 = Math.floor(bot.x), by0 = Math.floor(bot.y), bx1 = Math.ceil(bot.x + bot.w) - 1, by1 = Math.ceil(bot.y + bot.h) - 1;
|
||||
for (const p of drillPulses) {
|
||||
p.r += PULSE_SPEED;
|
||||
const lim = Math.min(p.r, p.range), Wd = PULSE_W[p.w];
|
||||
const lim = Math.min(p.r, p.range), Wd = PULSE_W[p.w], damp = p.damp ** step;
|
||||
for (let k = 0; k < p.n; k++) {
|
||||
let e = p.en[k];
|
||||
if (e < 0.04) continue;
|
||||
const ray = pulseRay(p.ox, p.oy, p.ang, Wd, -1 + 2 * (k + p.jit) / p.n), ux = ray.ux, uy = ray.uy;
|
||||
let sd = p.s[k];
|
||||
let sd = p.s[k], kicks = 0;
|
||||
while (sd < lim && e >= 0.04) {
|
||||
sd += 0.5;
|
||||
sd += step;
|
||||
const x = Math.floor(ray.x + ux * sd), y = Math.floor(ray.y + uy * sd);
|
||||
if (x < 0 || y < 0 || x >= W || y >= H) { e = 0; break; }
|
||||
if (x >= bx0 && x <= bx1 && y >= by0 && y <= by1) continue;
|
||||
const i = y * W + x, m = mat[i], ph = PH[m];
|
||||
if (ph === PH_EMPTY || ph === PH_GAS) {
|
||||
// standing trees in the way are shaken apart too (not by a tuned pulse)
|
||||
if (bgm[i] && !p.tune && (sd * 2 & 1) === 0) { cutTree(i, x, y, tier, e * 0.5); e *= bgm[i] === WOOD ? 0.85 : 0.97; }
|
||||
if (bgm[i] && !p.tune) { const loss = bgm[i] === WOOD ? wood : leaves; cutTree(i, x, y, tier, e * step * 0.5); e *= loss; }
|
||||
continue;
|
||||
}
|
||||
if (ph === PH_LIQUID) { e *= 0.985; continue; }
|
||||
if (ph === PH_LIQUID) { e *= liquid; continue; }
|
||||
if (ph === PH_POWDER) {
|
||||
// loose grains are jostled back toward the bot, so a shattered pocket drains out of the
|
||||
// wall and a bore clears itself: straight back if there is room, else down or up a step
|
||||
e *= 0.94;
|
||||
if (stamp[i] !== tickStamp && rnd() < 0.3 * e) {
|
||||
const sx = Math.abs(ux) > 0.3 ? -Math.sign(ux) : 0, sy = Math.abs(uy) > 0.3 ? -Math.sign(uy) : 0;
|
||||
for (const [mx, my] of [[sx, sy], [sx, 1], [sx, -1], [0, sy]]) {
|
||||
if (!mx && !my) continue;
|
||||
e *= loose;
|
||||
// At most three one-cell hops per ray/tick; gravity settles the shaken grains.
|
||||
if (kicks < 3 && stamp[i] !== tickStamp && !occ[i] && !part[i] && rnd() < 1.1 * e * step) {
|
||||
const side = rnd() < 0.5 ? -1 : 1;
|
||||
for (let hop = 0; hop < 4; hop++) {
|
||||
const mx = hop < 2 ? side : -side, my = -(hop & 1);
|
||||
const nx = x + mx, ny = y + my;
|
||||
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
||||
const j = ny * W + nx;
|
||||
if ((mat[j] === EMPTY || PH[mat[j]] === PH_GAS) && !occ[j] && !part[j]) { move(i, j, x, y, nx, ny); break; }
|
||||
if (mat[j] !== EMPTY && PH[mat[j]] !== PH_GAS) continue;
|
||||
if (occ[j] || part[j] || stamp[j] === tickStamp || !enter(m, ph, j, mx, my)) continue;
|
||||
if (my && CORNER_SEAL && PH[mat[i + mx]] === PH_SOLID && PH[mat[i - W]] === PH_SOLID) continue;
|
||||
move(i, j, x, y, nx, ny); kicks++;
|
||||
if (dust < 4) { emit({ t: 'debris', x, y, m }); dust++; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (builtCell(i, x, y)) { e *= 0.25; continue; }
|
||||
if (p.tune && YLD[m] !== p.tune) { e *= 0.993; continue; }
|
||||
if (builtCell(i, x, y)) { e *= built; continue; }
|
||||
if (p.tune && YLD[m] !== p.tune) { e *= skip; continue; }
|
||||
const h = HARD[m];
|
||||
// thin rock (a corner, a nub, a sliver with open space on two or more sides) has little
|
||||
// to hold it and shakes apart faster than a flat face, so ragged edges get cleaned up
|
||||
@@ -149,10 +159,10 @@ function pulseTick() {
|
||||
if (y > 0 && PH[mat[i - W]] !== PH_SOLID) open++;
|
||||
if (y < H - 1 && PH[mat[i + W]] !== PH_SOLID) open++;
|
||||
const thin = open > 1 && !THIN_BONUS_OFF ? 1 + (open - 1) * THIN_BONUS : 1;
|
||||
if (h > tier.hard) { e *= 0.35; continue; }
|
||||
const q = digp[i] + e * thin * tier.speed * PULSE_K / (1 + h * h * 0.6);
|
||||
if (h > tier.hard) { e *= hard; continue; }
|
||||
const q = digp[i] + e * (2 * step) * thin * tier.speed * PULSE_K / (1 + h * h * 0.6);
|
||||
if (q >= 255) { shatterCell(i, x, y, m); if (!p.tune) crumbleAround(x, y); } else { digp[i] = q; renderDirtyIdx(i); }
|
||||
e *= p.tune ? 0.96 : p.damp;
|
||||
e *= p.tune ? tuned : damp;
|
||||
}
|
||||
p.en[k] = e; p.s[k] = sd;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user