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
+46
View File
@@ -0,0 +1,46 @@
// The title screen art.
// ---------- Title art ----------
const FONT5 = {
D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], G: ['01111', '10000', '10000', '10011', '10001', '10001', '01111'],
R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'],
F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
U: ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'],
};
const TITLE = { cells: [], grains: [], t: 0, raf: 0 };
function buildTitleCells() {
const lines = [['DEEPGRAIN', 6, 'hot'], ['FOUNDRY', 44, 'copper']];
for (const [word, y0, kind] of lines) {
const sc = 3, wpx = word.length * 6 * sc - sc;
const x0 = Math.round((320 - wpx) / 2);
[...word].forEach((ch, k) => {
const g = FONT5[ch];
for (let r = 0; r < 7; r++) for (let c = 0; c < 5; c++) if (g[r][c] === '1')
for (let a = 0; a < sc; a++) for (let b = 0; b < sc; b++) TITLE.cells.push({ x: x0 + (k * 6 + c) * sc + a, y: y0 + r * sc + b, kind, n: Math.random() });
});
}
}
function drawTitle() {
const c = $('titleart'); if (!c || $('title').hidden) { TITLE.raf = 0; return; }
const ctx = c.getContext('2d'); const t = (TITLE.t += 1);
const img = ctx.createImageData(320, 80), px = new Uint32Array(img.data.buffer);
px.fill(0x00000000);
for (const g of TITLE.cells) {
const w = 0.55 + 0.45 * Math.sin(t * 0.045 + g.x * 0.09 + g.n * 6);
let r, gg, b;
if (g.kind === 'hot') { const T = 1050 + w * 650 + g.n * 120; const i = glowIndex(T); r = GLOW_R[i]; gg = GLOW_G[i]; b = GLOW_B[i]; }
else { const v = (g.n - 0.5) * 30; r = 200 + v + w * 20; gg = 122 + v + w * 12; b = 76 + v; }
px[g.y * 320 + g.x] = 0xff000000 | (Math.max(0, Math.min(255, b)) << 16) | (Math.max(0, Math.min(255, gg)) << 8) | Math.max(0, Math.min(255, r));
}
// falling grains under the words
if (Math.random() < 0.6) TITLE.grains.push({ x: 60 + Math.random() * 200, y: 28, v: 0.3 + Math.random() * 0.4, hot: Math.random() < 0.6 });
for (let k = TITLE.grains.length - 1; k >= 0; k--) {
const g = TITLE.grains[k]; g.y += g.v; g.v += 0.02;
if (g.y > 79) { TITLE.grains.splice(k, 1); continue; }
if (g.y > 41 && g.y < 66 && px[(g.y | 0) * 320 + (g.x | 0)]) { TITLE.grains.splice(k, 1); continue; }
px[(g.y | 0) * 320 + (g.x | 0)] = g.hot ? 0xff3aa0ff : 0xff5a8cc8;
}
ctx.putImageData(img, 0, 0);
TITLE.raf = requestAnimationFrame(drawTitle);
}