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
+95
View File
@@ -0,0 +1,95 @@
// Basics: drilling to rubble, the vacuum and blowing, walls, the ingot mold and bars, belts, blueprints, save/load.
const { load, suite, flatWorld, rect, ticks, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
flatWorld(run, 240, 160, 100);
run('player.tankTier = 3; player.flask = false;');
// 1. drill breaks stone loose; nothing goes into the bag
run('bot.x = 116; bot.y = 92; for (let k=0;k<80;k++) toolDig(120, 104, 3)');
check('drill leaves bag empty', run('bagTotal') === 0, run('bagTotal'));
const rub = count(run, run('RSTONE'));
check('drill turns stone into loose rubble', rub > 20, rub);
ticks(run, 60);
// 2. vacuum with a filter: only rubble
rect(run, 100, 96, 104, 99, run('COAL'));
ticks(run, 30);
const got = run('toolVacuum(120, 104, 6, m => m === RSTONE).got');
check('vacuum picks only the filtered material', run('bag[RSTONE]') > 0 && run('bag[COAL]') === 0, `${run('bag[RSTONE]')} rubble, ${run('bag[COAL]')} coal`);
run('toolVacuum(102, 98, 4, m => true)');
check('vacuum everything mode picks coal', run('bag[COAL]') > 0, run('bag[COAL]'));
// 3. drop: grains fall
const c0 = run('bag[COAL]');
run('bot.x = 100; bot.y = 60;');
run('for (let k=0;k<5;k++) { toolBlow(80, 70, 1, COAL); simTick(); }');
check('blown coal flies out of the bot', run('flyers.length') > 0, run('flyers.length'));
ticks(run, 120);
check('blown coal leaves the bag and lands on the ground', run('bag[COAL]') < c0 && run('flyers.length') === 0 && count(run, run('COAL'), 60, 80, 100, 99) > 0, `${c0}->${run('bag[COAL]')} landed ${count(run, run('COAL'), 60, 80, 100, 99)}`);
// bot flies through built walls but not through natural rock
run('bag[RSTONE] += 64; bagTotal += 64; placeBlocks(BP_WALL, RSTONE, blockRect(30, 14, 30, 17))');
check('bot can overlap a built wall', !run('botOverlapSolid(121, 58)'));
check('bot cannot overlap natural rock', run('botOverlapSolid(120, 98)'));
// trees: water passes in front of a background trunk; cutting the base drops the tree
run('for (let y = 80; y < 100; y++) for (let x = 140; x < 142; x++) bgm[y*W+x] = WOOD; for (let y = 72; y < 80; y++) for (let x = 136; x < 146; x++) bgm[y*W+x] = LEAVES;');
rect(run, 130, 96, 138, 99, run('WATER'));
ticks(run, 120);
check('water flows past a tree trunk', count(run, run('WATER'), 141, 90, 160, 99) > 0, count(run, run('WATER'), 141, 90, 160, 99));
run('bot.x = 134; bot.y = 88; for (let k=0;k<120;k++) toolDig(140, 99, 1)');
ticks(run, 5);
check('cutting the trunk base brings the tree down as wood chunks', run('bgm[85*W+140]') === 0 && count(run, run('LWOOD')) > 20, count(run, run('LWOOD')));
// 4. wall blocks cost rubble and snap to the grid
run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const r1 = run("JSON.stringify(placeBlocks(BP_WALL, RSTONE, blockLine(25, 20, 29, 20)))");
check('stone wall line placed', JSON.parse(r1).n === 5 && run('bag[RSTONE]') === 200 - 80, r1 + ' bag=' + run('bag[RSTONE]'));
check('wall cells are solid stone at grid positions', run('mat[80*W+100]') === run('STONE') && run('mat[83*W+119]') === run('STONE'));
const rm = run("removePieceAt(101, 81)");
check('taking a wall block back refunds its rubble', run('bag[RSTONE]') === 200 - 64, rm + ' ' + run('bag[RSTONE]'));
// 5. ingot mold: pour molten copper, knock out bars
run('bag[RSTONE] = 200; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const mo = run("placeMoldAt('ingot', 32, 22, RSTONE)");
check('ingot mold placed', mo === '' && run('molds.length') === 1, mo);
run('for (const cav of molds[0].cavs) for (const i of cav) setCell(i, MCU, 1160, 0)');
ticks(run, 900);
check('ingot mold sets', run('molds[0].state') === 'ready' && run('molds[0].ready.length') === 3, run('molds[0].state') + ' ' + run('molds[0].ready.length'));
const msg = run('collectMold(molds[0])');
check('knock-out spawns 3 bars', run('ents.length') === 3, msg);
ticks(run, 200);
check('bars fall and come to rest', run('ents.every(e => Math.abs(e.vy) < 0.01)'), run('JSON.stringify(ents.map(e=>[e.ix,e.iy]))'));
check('bars count as built-up copper', run('stats.bars') === 3);
// 6. vacuum bars
const bx = run('ents[0].ix + 2'), by = run('ents[0].iy + 1');
run(`toolVacuum(${bx}, ${by}, 4, m => m === BARCU)`);
check('vacuum picks up bars', run('bag[BARCU]') >= 1 && run('ents.length') < 3, run('bag[BARCU]'));
// 7. belt carries a bar: belt from block (40,24) to (46,24), powered by direct supply
run('bot.x = 165; bot.y = 70; bag[BARCU] = 20; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
const rb = run("JSON.stringify(placeBlocks(BP_BELT, 1, blockLine(40, 24, 46, 24)))");
check('belt blocks placed for $12 each', JSON.parse(rb).n === 7 && run('money') === 250 - 84, rb + ' money ' + run('money'));
run('rebuildPower(); nets[netId[96*W+161]].prods.push({out: 50});');
run('spawnBar(BARCU, 162, 90, 0, 0)');
ticks(run, 20);
const x0 = run('ents[ents.length-1].ix');
ticks(run, 60);
const x1 = run('ents[ents.length-1].ix');
check('powered belt moves the bar right', x1 > x0 + 10, `${x0} -> ${x1}`);
// 8. bar melts when heated
run('const e = ents[ents.length-1]; for (let dy=-2; dy<4; dy++) for (let dx=-2; dx<6; dx++) temp[(e.iy+dy)*W + e.ix+dx] = 1300; e.T = 1100;');
ticks(run, 3);
check('hot bar melts into molten copper', count(run, run('MCU')) > 0, count(run, run('MCU')));
// 9. machines snap to blocks
run('bag[LWOOD] = 100; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
check('water wheel placed on block grid', run("placeMachineAt('wheel', 0, 10, 10)") === '' && run('machines[1].x') === 40);
// 10. blueprint copy + ghost fill
run('bot.x = 100; bot.y = 60;');
run('bag[RSTONE] = 400; bagTotal = 0; for (let k=0;k<NMAT;k++) bagTotal += bag[k];');
run("placeBlocks(BP_WALL, RSTONE, blockRect(20, 18, 22, 19))");
const bp = run("JSON.stringify(copyRegion(80, 72, 91, 79))");
check('blueprint captures 6 wall blocks', JSON.parse(bp).blocks.length === 6, bp.slice(0, 80));
run(`pasteGhost(${bp}, 34, 14)`);
ticks(run, 20);
check('ghosts are built from the bag', run('ghostBlocks.size') === 0 && run('blockP[14*BW+34]') === run('BP_WALL'), run('ghostBlocks.size'));
// 11. save / load round trip
const before = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: molds.length, mach: machines.filter(m => m && m.type !== "dock").length, blocks: blockP.reduce((a,b)=>a+(b>0),0)})');
run('globalThis.__sv = serializeWorld(); deserializeWorld(__sv.meta, __sv.buffers);');
const after = run('JSON.stringify({bag: bagTotal, ents: ents.length, molds: molds.length, mach: machines.filter(m => m && m.type !== "dock").length, blocks: blockP.reduce((a,b)=>a+(b>0),0)})');
check('save/load keeps bag, bars, molds, machines and blocks', before === after, before + ' vs ' + after);
done();