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 b270c4e818
97 changed files with 11710 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// What does a running factory cost when the player is somewhere else?
// Builds K self-running modules in a generated world, far from the bot, and
// measures simulation time per tick. Each module: a coal + copper-ore furnace
// burning in a stone-block box, a spring feeding a water wheel, a shaft carrying
// its power to a 10-block belt that hauls sand off a hopper.
const { load } = require('../../test/harness');
const { run } = load();
const seed = 424242, w = +(process.argv[2] || 1024), h = +(process.argv[3] || 640);
run(`generateWorld(${seed}, ${w}, ${h}, null); player.tankTier = 3;`);
const awake = () => run('(()=>{let a=0,h=0;for(let c=0;c<NCH;c++){if(awake[c])a++;if(hot[c])h++;}return [a,h,NCH]})()');
function measure(n) {
const t0 = performance.now();
run(`for (let k=0;k<${n};k++) simTick();`);
return (performance.now() - t0) / n;
}
run('for (let k=0;k<600;k++) simTick();');
const idle = measure(600);
const [ia, ih, nch] = awake();
console.log(`world ${w}x${h}: ${nch} areas of 32x32`);
console.log(`idle: ${idle.toFixed(2)} ms/tick, ${ia} moving areas, ${ih} warm areas`);
function buildModule(mx, my) {
run(`(() => {
const mx = ${mx}, my = ${my};
// clear the sky box we build in
for (let y = my*4 - 4; y < (my+9)*4; y++) for (let x = mx*4 - 4; x < (mx+20)*4; x++) { const i = y*W+x; if (PH[mat[i]] !== PH_SOLID) setCell(i, EMPTY, AMB[y], 0); }
// furnace: 4x4 blocks of stone with a 2x2 hollow and a vent on top
for (let by = my; by < my+4; by++) for (let bx = mx; bx < mx+4; bx++) {
const inner = bx > mx && bx < mx+3 && by > my && by < my+3;
if (inner || (by === my && bx === mx+2)) continue;
placeBlock(BP_WALL, RSTONE, bx, by, true);
}
const ix = (mx+1)*4, iy = (my+1)*4;
for (let y = iy+3; y < iy+8; y++) for (let x = ix; x < ix+8; x++) setCell(y*W+x, ((x+y)&3) ? COAL : CUORE, AMB[y], 0);
for (let x = ix; x < ix+8; x++) { temp[(iy+3)*W+x] = 700; wakeHeatXY(x, iy+3); }
// spring above a water wheel
for (let y = my*4; y < my*4+2; y++) for (let x = (mx+6)*4; x < (mx+7)*4; x++) { mat[y*W+x] = SPRING; flags[y*W+x] = F_NAT; }
addMachine('wheel', mx+5, my+1, 0);
// shaft from the wheel's side down to a belt line
placeBlock(BP_SHAFT, 0, mx+8, my+2, true); placeBlock(BP_SHAFT, 0, mx+9, my+2, true);
placeBlock(BP_SHAFT, 0, mx+9, my+3, true); placeBlock(BP_SHAFT, 0, mx+9, my+4, true);
for (let bx = mx+9; bx < mx+20; bx++) placeBlock(BP_BELT, 1, bx, my+5, true);
// sand hopper on the first belt blocks
for (let y = (my+5)*4 - 12; y < (my+5)*4; y++) for (let x = (mx+10)*4; x < (mx+13)*4; x++) setCell(y*W+x, SAND, AMB[y], 0);
})()`);
run('powerDirty = true;');
}
const report = [];
const botBx = run('Math.floor(bot.x / 4)');
const slots = [];
for (let mx = 6; mx + 22 < w / 4 && slots.length < 8; mx += 28) if (Math.abs(mx + 10 - botBx) > 24) slots.push(mx);
for (const K of [1, 4, 8]) {
if (K > slots.length) break;
// fresh world for each count, so every module starts fully fuelled
run(`generateWorld(${seed}, ${w}, ${h}, null); for (let k=0;k<600;k++) simTick();`);
const modules = slots.slice(0, K);
for (const mx of modules) buildModule(mx, 8);
run('for (let k=0;k<300;k++) simTick();');
const moved0 = run('stats.moved');
const ms = measure(900);
const [a, ht] = awake();
const wheels = run('machines.filter(m => m && m.type === "wheel").map(m => m.out.toFixed(1)).join(" ")');
const sat = run('nets.slice(1).map(n => n.sat.toFixed(2)).join(" ")');
const furn = run(`[${modules.map(mx => `(() => { let t = 0; for (let y = 36; y < 44; y++) for (let x = ${(mx + 1) * 4}; x < ${(mx + 3) * 4}; x++) t = Math.max(t, temp[y*W+x]); return Math.round(t); })()`).join(',')}].join(' ')`);
const moved = run('stats.moved') - moved0;
const dist = Math.min(...modules.map(mx => Math.abs(mx * 4 + 40 - run('bot.x'))));
console.log(`\n${K} module${K > 1 ? 's' : ''}: ${ms.toFixed(2)} ms/tick (+${(ms - idle).toFixed(2)} over idle), ${a} moving areas, ${ht} warm areas`);
console.log(` nearest module ${Math.round(dist)} grains from the bot; wheel output ${wheels}; belt power satisfaction ${sat}`);
console.log(` furnace peak temps ${furn} °C; grains moved by machines in 15 s: ${moved}`);
report.push({ K, ms, extra: ms - idle, a, ht });
}
const per = report.map(r => (r.extra / r.K).toFixed(2));
console.log(`\nextra cost per module: ${per.join(' / ')} ms/tick`);
const budget = 9; // ms per tick left for simulation at 60 ticks/s after rendering
console.log(`at ~${budget} ms/tick of simulation budget, roughly ${Math.floor((budget - idle) / Math.max(0.01, report[report.length - 1].extra / report[report.length - 1].K))} such modules run at full speed`);