The game as of artifact version 24, ready for local development with coding agents. - Source split from three large files into 52 small ones under src/sim, src/render and src/ui, joined in the order in src/manifest.json by build.py. The joined code is unchanged apart from comments; every headless suite gives the same output, and screenshots in both looks are pixel-identical to before. - Light tidying with no change in behaviour: one movement scan loop, shared hazard-stripe and belt shader helpers, unused code removed, a shared check helper for the test suites. - World files can be exported as an ordinary download outside claude.ai. - Tests: broken old scripts removed, experiments moved to tools/, test/run_all.py added. Tools: shot.py (repeatable screenshots), where.py, map.py. - Docs: AGENTS.md, CLAUDE.md, NOTES.md (the owner's to-do inbox), docs/PROJECT_MAP.md, DESIGN.md, TESTING.md, systems notes, the owner's requests and an agent log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K9kLmYADKSEjuwUN3rFxDA
138 lines
11 KiB
JavaScript
138 lines
11 KiB
JavaScript
// 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);
|
|
// 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}`);
|
|
|
|
// 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 says how far the bit reaches', /Out of reach/.test(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('a spot a medium pulse reaches is out of reach for a wide one, and it says so', /Out of reach/.test(farW.status) && /wide/.test(farW.status) && !nearM.status, `${farW.status} | ${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')));
|
|
|
|
done();
|