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
+80
View File
@@ -0,0 +1,80 @@
// Building: undo (Ctrl+Z), cut, replace a machine with Ctrl, pasting ghosts, and the fan heat warning.
const { load, suite, flatWorld, count } = require('./harness');
const { run } = load();
const { check, done } = suite();
const fresh = () => { flatWorld(run, 240, 160, 100); run('money = 1000; bag[RSTONE] = 400; bagTotal = 400; bot.x = 100; bot.y = 60; undoStack = []; clearAllGhosts();'); };
const belts = () => run('blockP.reduce((a, v) => a + (v === BP_BELT), 0)');
const fans = () => run("machines.filter(m => m && m.type === 'fan').length");
// placing a line of belts, then undoing it: they come down and the money comes back
fresh();
const m0 = run('money');
run("undoRecord('placing belts', () => placeBlocks(BP_BELT, 1, blockLine(22, 20, 27, 20), undefined, false))");
check('a line of 6 belts is placed', belts() === 6 && run('money') === m0 - 72, `${belts()} belts, $${run('money')}`);
check('Ctrl+Z takes them back and refunds them', run('undoLast()') === 'Undid placing belts.' && belts() === 0 && run('money') === m0, `${belts()} belts, $${run('money')}`);
check('with nothing left, it says so', run('undoLast()') === 'Nothing to undo.');
// taking a piece back, then undoing: it is built again
fresh();
run('placeBlocks(BP_BELT, 1, blockLine(22, 20, 25, 20), undefined, false); placeBlock(BP_WALL, RSTONE, 26, 20, false);');
const st0 = run('bag[RSTONE]');
run("undoRecord('taking back', () => { removePieceAt(23 * 4 + 1, 20 * 4 + 1); removePieceAt(26 * 4 + 1, 20 * 4 + 1); })");
check('taking back a belt and a wall block', belts() === 3 && run('blockP[20 * BW + 26]') === 0 && run('bag[RSTONE]') === st0 + 16, `${belts()} belts, ${run('bag[RSTONE]')} rubble`);
run('undoLast()');
check('undo builds them again from the bag', belts() === 4 && run('blockP[20 * BW + 26]') === run('BP_WALL') && run('bag[RSTONE]') === st0, `${belts()} belts, ${run('bag[RSTONE]')} rubble`);
// replacing belts with filters, then undoing: the belts come back
fresh();
run('placeBlocks(BP_BELT, 1, blockLine(22, 20, 25, 20), undefined, false);');
run("undoRecord('replacing', () => placeBlocks(BP_FILTER, 128, blockLine(23, 20, 24, 20), { m: [SAND], inv: 0 }, true))");
check('Ctrl-dragging filters over belts replaces them', run('blockP[20 * BW + 23]') === run('BP_FILTER') && belts() === 2);
run('undoLast()');
check('undo puts the belts back', belts() === 4 && run('blockP.reduce((a, v) => a + (v === BP_FILTER), 0)') === 0, belts());
// Ctrl over built blocks with a machine: they are taken down and the fan goes in their place
fresh();
run('placeBlocks(BP_BELT, 1, blockLine(22, 20, 25, 20), undefined, false);');
check('a fan will not go on a belt without Ctrl', /already/.test(run("canPlaceMachine('fan', 23, 20) || ''")));
run("undoRecord('replacing', () => { clearFootprint(23, 20, 1, 1); placeMachineAt('fan', 0, 23, 20); })");
check('with Ctrl the belt under it is taken down and the fan placed', fans() === 1 && belts() === 3);
run('undoLast()');
check('undo takes the fan back and rebuilds the belt', fans() === 0 && belts() === 4);
run('for (let y = 76; y < 80; y++) for (let x = 112; x < 116; x++) setCell(y * W + x, STONE, AMB[y], F_NAT);');
check('natural rock still stops a replace', /rock/.test(run('clearFootprint(28, 19, 1, 1)')));
// cut: everything in the box comes down into the bag and onto the clipboard; undo puts it back
fresh();
run("placeBlocks(BP_BELT, 1, blockLine(22, 20, 25, 20), undefined, false); placeMachineAt('fan', 0, 22, 18);");
const mc = run('money');
const bp = JSON.parse(run("JSON.stringify(undoRecord('cutting', () => cutRegion(22 * 4, 18 * 4, 26 * 4 - 1, 21 * 4 - 1)))"));
check('cut picks up the belts and the fan', bp && bp.blocks.length === 4 && bp.machines.length === 1, bp && `${bp.blocks.length} blocks, ${bp.machines.length} machines`);
check('and takes them down, refunded', belts() === 0 && fans() === 0 && run('money') === mc + 48 + 60, `${belts()} belts, ${fans()} fans, $${run('money')}`);
run('undoLast()');
check('undo after a cut puts it all back', belts() === 4 && fans() === 1 && run('money') === mc, `${belts()} belts, ${fans()} fans, $${run('money')}`);
// paste: ghosts appear; undo clears them, and takes back any the bot already built
fresh();
run("placeBlocks(BP_BELT, 1, blockLine(22, 20, 25, 20), undefined, false); window_bp = copyRegion(22 * 4, 20 * 4, 26 * 4 - 1, 21 * 4 - 1);");
run("undoRecord('pasting', () => pasteGhost(window_bp, 22, 17))");
check('pasting places ghosts', run('ghostBlocks.size') === 4);
run('for (let k = 0; k < 400; k++) simTick();');
const built = belts();
check('the bot builds some or all of them', built > 4, built);
check('building from ghosts is never an undo step of its own', run('undoStack.length') === 1, run('undoStack.length'));
run('undoLast()');
check('undo clears the ghosts and takes back what was built from them', belts() === 4 && run('ghostBlocks.size') === 0, `${belts()} belts, ${run('ghostBlocks.size')} ghosts`);
// a fan getting near its melting point warns you
fresh();
run("placeMachineAt('fan', 0, 22, 20); EV.length = 0;");
run('for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; for (let k = 0; k < 40; k++) { for (let y = 80; y < 84; y++) for (let x = 88; x < 92; x++) temp[y * W + x] = 900; simTick(); }');
const warn = run("EV.filter(e => e.t === 'toast' && /fan/i.test(e.text)).map(e => e.text).join(' | ')");
check('a fan at 900°C warns that it is about to melt', /1085/.test(warn) && fans() === 1, warn);
// old saves: tiny specks of too-hard rock become the rock around them, once
fresh();
run('for (let y = 120; y < 122; y++) for (let x = 60; x < 62; x++) setCell(y * W + x, FEV, AMB[y], F_NAT); for (let y = 120; y < 130; y++) for (let x = 140; x < 150; x++) setCell(y * W + x, FEV, AMB[y], F_NAT);');
const n = run('clearHardSpecks(true)');
check('a 4-grain speck of iron ore in stone becomes stone; a 100-grain vein stays', n === 1 && count(run, run('FEV')) === 100 && run('mat[120 * W + 60]') === run('STONE'), `${n} specks, ${count(run, run('FEV'))} vein`);
done();