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
+154
View File
@@ -0,0 +1,154 @@
// Machines: the power network, footprints, placing and removing machines.
// ---------- Power & machines ----------
function markPow(i, on) {
if (on) { if (!isPow[i]) { isPow[i] = 1; pcells.add(i); } }
else if (isPow[i]) { isPow[i] = 0; pcells.delete(i); netId[i] = 0; }
powerDirty = true;
}
function partLost(i) { markPow(i, false); }
let nextPowerBuild = 0;
function rebuildPower() {
powerDirty = false;
for (const i of pcells) netId[i] = 0;
nets = [null]; beltL = []; beltR = []; tubeLists = [[], [], [], [], []];
const stack = [];
for (const s of pcells) {
if (netId[s]) continue;
const id = nets.length;
if (id >= 65535) break;
const net = { id, nb: 0, nt: 0, fans: [], prods: [], supply: 0, demand: 0, sat: 0 };
nets.push(net);
netId[s] = id; stack.push(s);
while (stack.length) {
const i = stack.pop();
const m = mat[i], p = part[i];
if (m === BELTL) { net.nb++; beltL.push(i); } else if (m === BELTR) { net.nb++; beltR.push(i); }
if (p >= P_TU && p <= P_TR) { net.nt++; tubeLists[p].push(i); }
const x = i % W;
let j;
if (x > 0) { j = i - 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
if (x < W - 1) { j = i + 1; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
if (i >= W) { j = i - W; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
if (i < N - W) { j = i + W; if (isPow[j] && !netId[j]) { netId[j] = id; stack.push(j); } }
}
}
for (const mm of machines) {
if (!mm) continue;
mm.net = netId[mm.cell0] || 0;
const net = nets[mm.net];
if (!net) continue;
if (mm.type === 'fan') net.fans.push(mm); else if (mm.type === 'wheel' || mm.type === 'engine') net.prods.push(mm);
}
beltL.sort((a, b) => a - b); beltR.sort((a, b) => b - a);
tubeLists[P_TU].sort((a, b) => a - b); tubeLists[P_TD].sort((a, b) => b - a);
tubeLists[P_TL].sort((a, b) => a - b); tubeLists[P_TR].sort((a, b) => b - a);
}
function netSat(i) { if (POWER_FREE) return 1; const n = nets[netId[i]]; return n ? n.sat : 0; }
// Move loose grains out of the given cells, to the nearest open cell above
// the footprint in the same column (they then fall back on top). Gas just vanishes.
function evictCells(cells, topY) {
for (const i of cells) bgm[i] = 0;
for (const i of cells) {
const m = mat[i], ph = PH[m];
if (m === EMPTY || ph === PH_SOLID) continue;
if (ph === PH_GAS) { mat[i] = EMPTY; life[i] = 0; continue; }
const x = i % W;
let moved = false;
for (let y = topY - 1; y >= Math.max(0, topY - 80); y--) {
const j = y * W + x;
if (mat[j] === EMPTY && !part[j] && !occ[j] && !mach[j]) { swapCells(i, j); wakeXY(x, y); moved = true; break; }
if (PH[mat[j]] === PH_SOLID) break;
}
if (!moved) { mat[i] = EMPTY; life[i] = 0; flags[i] = 0; }
wakeIdx(i);
}
}
function cellBuildable(i) { return PH[mat[i]] !== PH_SOLID && !part[i] && !mach[i] && !occ[i]; }
function machineFootprint(type, x, y) {
// [cx, cy, kind] where kind is 'hull', 'void' (inside the machine's area but open) or a part code
const out = [];
if (type === 'wheel') {
for (let dy = 0; dy < 12; dy++) for (let dx = 0; dx < 12; dx++) {
const ex = dx - 5.5, ey = dy - 5.5;
out.push([x + dx, y + dy, ex * ex + ey * ey <= 36.5 ? P_WHEEL : 'void']);
}
} else if (type === 'fan') {
for (let dy = 0; dy < 4; dy++) for (let dx = 0; dx < 4; dx++) out.push([x + dx, y + dy, 'hull']);
} else if (type === 'cutter') {
// a hopper 4 grains wide over the saw floor; bricks drop out underneath
for (let dy = 0; dy < 8; dy++) for (let dx = 0; dx < 8; dx++) out.push([x + dx, y + dy, dx < 2 || dx >= 6 || dy >= 5 ? 'hull' : 'void']);
} else if (type === 'silo') {
// roof with a hatch, the body, a cone narrowing to the gate, and two legs; the gaps beside
// the cone are left open
for (let dy = 0; dy < 24; dy++) for (let dx = 0; dx < 12; dx++) {
let k = null;
if (dy < 2) k = dx >= 3 && dx <= 8 ? 'void' : 'hull';
else if (dy < 20) k = 'hull';
else if (dx === 0 || dx === 11) k = 'hull'; // legs
else if (dy === 20) k = dx >= 1 && dx <= 10 ? 'hull' : null;
else if (dy === 21) k = dx >= 2 && dx <= 9 ? 'hull' : null;
else k = dx >= 4 && dx <= 7 ? 'void' : dx === 3 || dx === 8 ? 'hull' : null; // the gate
if (k) out.push([x + dx, y + dy, k]);
}
} else if (type === 'refiner') {
// an open hopper 8 grains wide over the grinding floor; what it lets out drops from under the floor
for (let dy = 0; dy < 12; dy++) for (let dx = 0; dx < 12; dx++) out.push([x + dx, y + dy, dx < 2 || dx >= 10 || dy >= 10 ? 'hull' : 'void']);
} else if (type === 'dock') {
// an open-topped hopper: 2-grain walls and floor around a 20x14 bin
for (let dy = 0; dy < 16; dy++) for (let dx = 0; dx < 24; dx++) out.push([x + dx, y + dy, dx < 2 || dx >= 22 || dy >= 14 ? 'hull' : 'void']);
} else if (type === 'engine') {
for (let dy = 0; dy < 8; dy++) for (let dx = 0; dx < 12; dx++) {
if (dy === 7 && dx >= 4 && dx <= 7) out.push([x + dx, y + dy, P_INTAKE]);
else out.push([x + dx, y + dy, 'hull']);
}
}
return out;
}
function canPlaceMachine(type, bx, by) {
const P = PIECE_BY_ID[type];
if (bx < 0 || by < 0 || bx + P.bw > BW || by + P.bh > BH) return 'Out of bounds';
for (const [cx, cy] of machineFootprint(type, bx * BS, by * BS)) {
const i = cy * W + cx;
if (mach[i] || part[i]) return 'Something is already built there';
if (PH[mat[i]] === PH_SOLID) return (flags[i] & F_NAT) ? 'Needs open space. Clear the rock first.' : 'Something is already built there';
if (occ[i]) return 'A bar is in the way';
}
for (let y = by; y < by + P.bh; y++) for (let x = bx; x < bx + P.bw; x++) if (blockP[y * BW + x]) return 'Something is already built there';
return null;
}
function addMachine(type, bx, by, dir) {
let id = machines.indexOf(null, 1);
if (id < 0) id = machines.length;
const P = PIECE_BY_ID[type];
const x = bx * BS, y = by * BS;
const mm = { id, type, x, y, w: P.w, h: P.h, dir: dir | 0, on: true, out: 0, flow: 0, energy: 0, angle: 0, net: 0, cell0: y * W + x, intake: [] };
machines[id] = mm;
const fp = machineFootprint(type, x, y);
evictCells(fp.filter(f => f[2] === 'hull').map(([cx, cy]) => cy * W + cx), y);
for (const [cx, cy, k] of fp) {
const i = cy * W + cx;
if (k === 'hull') { mat[i] = type === 'dock' ? DOCKW : HULL; flags[i] = 0; life[i] = 0; temp[i] = AMB[cy]; if (type !== 'dock') markPow(i, true); }
else if (k !== 'void') { part[i] = k; markPow(i, true); if (k === P_INTAKE) mm.intake.push(i); }
mach[i] = id;
wakeXY(cx, cy);
}
if (type === 'wheel') mm.cell0 = (y + 6) * W + x + 6;
return mm;
}
function removeMachine(id, refund, scrap) {
const mm = machines[id];
if (!mm) return;
for (const [cx, cy, k] of machineFootprint(mm.type, mm.x, mm.y)) {
const i = cy * W + cx;
if (mach[i] !== id) continue;
mach[i] = 0; markPow(i, false);
if (k === 'hull') { if (mat[i] === HULL || mat[i] === DOCKW) { mat[i] = scrap ? SCRCU : EMPTY; flags[i] = 0; } }
else if (k !== 'void') part[i] = 0;
wakeXY(cx, cy);
}
machines[id] = null;
if (refund) { refundAt(PIECE_BY_ID[mm.type].cost, 1, mm.x + mm.w / 2, mm.y + mm.h / 2); undoNote({ t: 'mach-', type: mm.type, bx: mm.x / BS, by: mm.y / BS, dir: mm.dir, on: mm.on }); }
if (scrap) emit({ t: 'toast', text: `${PIECE_BY_ID[mm.type].name} was destroyed by heat.` });
}