Deepgrain Foundry: move from a claude.ai artifact to a local repository

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
This commit is contained in:
2026-10-23 16:05:31 -04:00
co-authored by Claude Opus 5.5
commit 3f971a94c2
100 changed files with 11928 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
// Plays a starter design the way a player would: paste the blueprint, let the
// bot build it from the bag, blow an even ore/coal mix inside, light it, wait,
// knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
const { load, flatWorld, ticks } = require('../../test/harness');
const { run } = load();
const id = process.argv[2] || 'smelter1', ore = +(process.argv[3] || 40), coal = +(process.argv[4] || 40);
flatWorld(run, 240, 160, 100);
for (let k = 0; k < +(process.argv[5] || 0); k++) run('rnd()'); // shake the dice: how robust is the design?
run(`bag[RSTONE] = 900; bag[LCLAY] = 200; bag[COAL] = ${coal}; bag[CUORE] = ${ore}; bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];`);
const d = run(`JSON.stringify(STARTER_DESIGNS.find(d => d.id === '${id}').bp())`);
const bp = JSON.parse(d);
// sit it on the ground: bottom row at block 24 (ground starts at block 25)
const ox = 24, oy = 25 - bp.bh;
run(`bot.x = ${ox * 4 + bp.bw * 2 - 2}; bot.y = ${oy * 4 - 14}; pasteGhost(${d}, ${ox}, ${oy});`);
for (let k = 0; k < 40 && run('ghostBlocks.size + ghostMolds.length') > 0; k++) ticks(run, 30);
console.log(id, `${bp.bw}x${bp.bh} blocks;`, 'ghosts left', run('ghostBlocks.size + ghostMolds.length'), 'molds', run('molds.length'), 'stone used', 900 - run('bag[RSTONE]'), 'clay used', 200 - run('bag[LCLAY]'));
// fly inside (the bot passes through walls) and blow the mix across the chamber floor
const cx0 = (ox + 1) * 4, cx1 = (ox + bp.bw - 1) * 4 - 1, floorY = (oy + 4) * 4 - 1;
run(`bot.x = ${(cx0 + cx1) / 2 - 2}; bot.y = ${oy * 4};`);
run(`GCOL = [CUORE, COAL];`);
for (let k = 0; k < 400 && run('bag[CUORE] + bag[COAL]') > 0; k++) {
const tx = cx0 + 1 + (k * 5) % (cx1 - cx0 - 2);
run(`toolBlow(${tx}, ${floorY - 1}, 0, CUORE, GCOL); simTick();`);
}
ticks(run, 60);
const inside = run(`(()=>{let o=0,c=0;for(let y=${oy * 4};y<=${floorY};y++)for(let x=${cx0};x<=${cx1};x++){const m=mat[y*W+x];if(m===CUORE)o++;else if(m===COAL)c++;}return [o,c]})()`);
console.log('charge inside', inside, 'left in bag', run('[bag[CUORE], bag[COAL]]'));
// light the top of the pile from inside, then leave
// like a player: hold the igniter on a few coal grains that touch air
const spots = run(`(()=>{const out=[];for(let y=${oy * 4 + 4};y<=${floorY};y++)for(let x=${cx0};x<=${cx1};x++){const i=y*W+x;if(mat[i]===COAL&&mat[i-W]===EMPTY)out.push([x,y]);}return out})()`);
const pick = [spots[0], spots[Math.floor(spots.length / 2)], spots[spots.length - 1]].filter(Boolean);
run(`for (let k=0;k<50;k++){ ${pick.map(([x, y]) => `toolIgnite(${x}, ${y});`).join('')} simTick(); }`);
run(`bot.x = ${ox * 4 - 30}; bot.y = ${oy * 4 - 20};`);
let t = 0;
for (const dt of [900, 900, 1800, 1800]) {
ticks(run, dt); t += dt;
console.log(`${t / 60}s`, run(`JSON.stringify(molds.map(m => m.state + (m.ready.length ? ':' + m.ready.length : '') + (m.flaw ? ':' + m.flaw : '')))`),
'ore left', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===CUORE)n++;return n})()`), 'copper set outside molds', run(`(()=>{let n=0;const cav=new Set(molds.flatMap(m=>m.cavs.flat()));for(let i=0;i<N;i++)if(mat[i]===COPPER&&!cav.has(i))n++;return n})()`),
'in molds', run(`(()=>{let n=0;for(const i of molds.flatMap(m=>m.cavs.flat()))if(mat[i]===COPPER||mat[i]===MCU)n++;return n})()`), 'slag', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===SLAG)n++;return n})()`), 'molten', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===MCU)n++;return n})()`));
}
function dump(title) {
console.log('--', title);
const ch = { [run('EMPTY')]: '.', [run('COAL')]: 'c', [run('CUORE')]: 'o', [run('MCU')]: 'M', [run('COPPER')]: '=', [run('SLAG')]: 's', [run('ASH')]: 'a', [run('FIRE')]: '^', [run('SMOKE')]: '~', [run('STONE')]: '#', [run('SCRCU')]: '%' };
const rows = run(`(()=>{const out=[];for (let y = ${oy * 4 - 2}; y < ${(oy + bp.bh) * 4 + 2}; y++) { const r = []; for (let x = ${ox * 4 - 2}; x < ${(ox + bp.bw) * 4 + 2}; x++) { const i = y * W + x; r.push([mat[i], part[i], Math.round(temp[i])]); } out.push(r); } return out; })()`);
for (const r of rows) console.log(r.map(([m, p]) => p === 5 && m === 0 ? '+' : p === 5 ? (ch[m] || '?').toUpperCase() : (ch[m] || '?')).join(''));
}
if (process.env.DUMP) dump('before knocking out');
const msgs = run(`JSON.stringify(molds.map(m => collectMold(m)))`);
console.log('knock out:', msgs, '| bars on the ground', run('ents.length'), '| bit tier', run('player.bitTier'));