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
+24
View File
@@ -0,0 +1,24 @@
// Block-sized bit mold sunk into the ground with a stone furnace built over its funnel.
const { load, flatWorld, rect, ticks, dump } = require('../../test/harness');
const { run } = load();
const C = n => run(n);
const kind = process.argv[2] || 'bit', wallName = process.argv[3] || 'STONE';
flatWorld(run, 220, 160, 100);
run('bot.x = 90; bot.y = 60;');
// pit for the mold: x 80..103, y 100..111
run(`for (let y=100;y<=111;y++) for (let x=80;x<=103;x++) setCell(y*W+x, EMPTY, AMB[y], 0)`);
run(`bag[R${wallName === 'STONE' ? 'STONE' : 'BRICK'}] = 500; bagTotal = 500;`);
console.log('mold', JSON.stringify(run(`placeMoldAt('${kind}', 21, 25, ${wallName === 'STONE' ? 'RSTONE' : 'RBRICK'})`)));
const W_ = C(wallName);
rect(run, 80, 100, 83, 111, W_); rect(run, 100, 100, 103, 111, W_); rect(run, 80, 108, 103, 111, W_);
if (process.env.GRATE) { run('bag[LCLAY] = 100; bagTotal += 100;'); console.log('grate', JSON.stringify(run('placeBlocks(BP_GRATE, 0, blockLine(21, 24, 24, 24))'))); }
// furnace walls above, floor at ground level except over the mold
rect(run, 74, 74, 75, 99, W_); rect(run, 108, 74, 109, 99, W_); rect(run, 74, 74, 109, 75, W_);
run(`for (let y=74;y<76;y++) for (let x=91;x<93;x++) setCell(y*W+x, EMPTY, AMB[y], 0)`);
run(`{ let r=mulberry32(9); for (let y=80;y<=99;y++) for (let x=76;x<=107;x++) { const i=y*W+x; if (mat[i]===EMPTY && !part[i]) setCell(i, r()<0.32 ? CUORE : COAL, AMB[y], 0); } }`);
run(`for (let k=0;k<40;k++){ toolIgnite(80,79); toolIgnite(103,79); simTick(); }`);
for (const t of [1200, 1200, 1200, 1800]) {
ticks(run, t);
console.log('--- +' + t, run(`JSON.stringify(molds.map(m=>({state:m.state, fill:+m.fill.toFixed(2), ready: m.ready.length, flaw:m.flaw})))`));
}
if (process.argv[4]) dump(run, 72, 70, 112, 112, true);
+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`);
+33
View File
@@ -0,0 +1,33 @@
// Throughput of belts and screw tubes: grains delivered per second. Whatever lands
// past the outlet is taken away, so the output pile never backs up into the line.
const { load, flatWorld, rect, ticks, count } = require('../../test/harness');
const { run } = load();
const res = {};
function fresh() { flatWorld(run, 320, 200, 120); run('bot.x = 20; bot.y = 40; money = 1e6; globalThis.__got = 0;'); }
function rate(x0, x1, y0, y1) {
const sink = `for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) { const i=y*W+x; if (mat[i]===SAND) { mat[i]=EMPTY; __got++; } }`;
for (let k = 0; k < 90; k++) run(`simTick(); ${sink}`);
run('__got = 0');
for (let k = 0; k < 300; k++) run(`simTick(); ${sink}`);
return run('__got') / 5;
}
// sideways tube fed by a walled hopper sitting on its first two blocks
fresh();
run('for (let bx = 20; bx < 30; bx++) placeBlock(BP_TUBE, 1, bx, 20, true);');
run('for (let y = 0; y < 80; y++) { setCell(y*W+79, STONE, AMB[y], 0); setCell(y*W+96, STONE, AMB[y], 0); }');
rect(run, 80, 0, 95, 79, run('SAND'));
res.tubeSideways = rate(120, 319, 84, 119);
res.tubeLanes = run('(() => { const s = new Set(); for (let y = 80; y < 84; y++) for (let x = 100; x < 120; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
// lift: a column of up-tubes in a pit of sand, turned sideways at the top
fresh();
run('for (let y = 120; y < 196; y++) for (let x = 90; x < 130; x++) setCell(y*W+x, EMPTY, AMB[y], 0)');
rect(run, 90, 130, 129, 195, run('SAND'));
run('for (let by = 20; by <= 37; by++) placeBlock(BP_TUBE, 0, 26, by, true); for (let bx = 26; bx <= 40; bx++) placeBlock(BP_TUBE, 1, bx, 19, true);');
res.lift = rate(164, 319, 80, 119);
res.liftLanes = run('(() => { const s = new Set(); for (let y = 76; y < 80; y++) for (let x = 120; x < 160; x++) if (mat[y*W+x] === SAND) s.add(y); return s.size; })()');
// belt: 12 blocks, a pile dropped on its back end
fresh();
run('for (let bx = 20; bx < 32; bx++) placeBlock(BP_BELT, 1, bx, 25, true);');
rect(run, 80, 20, 95, 99, run('SAND'));
res.belt = rate(128, 319, 104, 119);
console.log(JSON.stringify(res));
+48
View File
@@ -0,0 +1,48 @@
// Furnace scenarios on a flat test world: builds a chamber, charges it with ore and coal, lights it
// and prints temperatures, burning cells and what has melted or set at intervals.
const { load, flatWorld, rect, count, maxTemp, ticks } = require('../../test/harness');
const { run } = load();
const C = n => run(n);
function report(t, region) {
const [x0, y0, x1, y1] = region;
const burning = run(`(()=>{let n=0;for(let y=${y0};y<=${y1};y++)for(let x=${x0};x<=${x1};x++)if(flags[y*W+x]&F_BURN)n++;return n})()`);
const oreT = run(`(()=>{let s=0,n=0;for(let y=${y0};y<=${y1};y++)for(let x=${x0};x<=${x1};x++)if(mat[y*W+x]===CUORE||mat[y*W+x]===FEORE){s+=temp[y*W+x];n++;}return n?Math.round(s/n):0})()`);
const airT = run(`(()=>{let s=0,n=0;for(let y=${y0};y<=${y1};y++)for(let x=${x0};x<=${x1};x++)if(mat[y*W+x]===EMPTY){s+=temp[y*W+x];n++;}return n?Math.round(s/n):0})()`);
console.log(`t=${String(t).padStart(5)} maxT=${String(maxTemp(run, x0, y0, x1, y1)).padStart(5)} oreT=${String(oreT).padStart(5)} airT=${String(airT).padStart(4)} burn=${String(burning).padStart(3)} coal=${count(run, C('COAL'), x0, y0, x1, y1)} cuOre=${count(run, C('CUORE'), x0, y0, x1, y1)} feOre=${count(run, C('FEORE'), x0, y0, x1, y1)} mCu=${count(run, C('MCU'), x0, y0, x1, y1)} Cu=${count(run, C('COPPER'), x0, y0, x1, y1)} mFe=${count(run, C('MFE'), x0, y0, x1, y1)} Fe=${count(run, C('IRON'), x0, y0, x1, y1)} slag=${count(run, C('SLAG'), x0, y0, x1, y1)} ash=${count(run, C('ASH'), x0, y0, x1, y1)} magma=${count(run, C('MAGMA'), x0, y0, x1, y1)} brick=${count(run, C('BRICK'), x0, y0, x1, y1)} smoke=${count(run, C('SMOKE'), x0, 0, x1, y1)}`);
}
function scenario(name, build, total, every, region) {
flatWorld(run, 200, 140, 90);
build();
console.log(`\n=== ${name} ===`);
for (let t = 0; t <= total; t += every) { if (t) ticks(run, every); report(t, region); }
}
function ignite(x, y, n = 40) { run(`for (let k=0;k<${n};k++){ toolIgnite(${x},${y}); simTick(); }`); }
// mixed charge: coal with ore grains sprinkled through it
function charge(x0, y0, x1, y1, ore, frac) {
run(`{ let r=mulberry32(5); for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) setCell(y*W+x, r()<${frac} ? ${ore} : COAL, AMB[y], 0); }`);
}
function furnace(x0, y0, x1, y1, wall, thick, vent) {
rect(run, x0, y0, x0 + thick - 1, y1, wall); rect(run, x1 - thick + 1, y0, x1, y1, wall);
rect(run, x0, y0, x1, y0 + thick - 1, wall);
const cx = (x0 + x1) >> 1;
run(`for (let y=${y0};y<${y0 + thick};y++) for (let x=${cx - (vent >> 1)};x<${cx - (vent >> 1) + vent};x++) setCell(y*W+x, EMPTY, AMB[y], 0)`);
}
const which = process.argv[2] || 'all';
if (which === 'all' || which === 'open') scenario('Open mixed pile (coal + 30% copper ore)', () => {
charge(80, 80, 103, 89, C('CUORE'), 0.3);
ignite(85, 79); ignite(98, 79);
}, 3600, 600, [70, 60, 113, 89]);
if (which === 'all' || which === 'stone') scenario('Stone furnace, mixed charge, 2-wide vent', () => {
furnace(77, 66, 106, 89, C('STONE'), 2, 2);
charge(79, 76, 104, 89, C('CUORE'), 0.3);
ignite(84, 75); ignite(99, 75);
}, 4800, 600, [77, 64, 106, 89]);
if (which === 'all' || which === 'clay') scenario('Clay furnace, mixed charge (walls fire to brick)', () => {
furnace(77, 66, 106, 89, C('CLAY'), 2, 2);
charge(79, 76, 104, 89, C('CUORE'), 0.3);
ignite(84, 75); ignite(99, 75);
}, 4800, 600, [77, 64, 106, 89]);
+14
View File
@@ -0,0 +1,14 @@
// ASCII cross-section of a stone furnace firing, with a temperature digit per cell (0-9), printed
// at intervals. Usage: node tools/experiments/section.js [WALL_MATERIAL] (default STONE).
const { load, flatWorld, rect, ticks, dump } = require('../../test/harness');
const { run } = load();
const C = n => run(n);
flatWorld(run, 200, 140, 90);
const wall = C(process.argv[2] || 'STONE');
rect(run, 77, 66, 78, 95, wall); rect(run, 105, 66, 106, 95, wall); rect(run, 77, 66, 106, 67, wall);
run(`for (let y=66;y<68;y++) for (let x=91;x<93;x++) setCell(y*W+x, EMPTY, AMB[y], 0)`);
// dig a basin into the ground so the furnace has a floor of the wall material
run(`for (let y=90;y<=95;y++) for (let x=79;x<=104;x++) setCell(y*W+x, y>=94? ${wall} : EMPTY, AMB[y], 0)`);
run(`{ let r=mulberry32(5); for (let y=76;y<=93;y++) for (let x=79;x<=104;x++) setCell(y*W+x, r()<0.3 ? CUORE : COAL, AMB[y], 0); }`);
run(`for (let k=0;k<40;k++){ toolIgnite(84,75); toolIgnite(99,75); simTick(); }`);
for (const t of [600, 1200, 1200, 1200]) { ticks(run, t); console.log('--- +' + t); dump(run, 75, 64, 108, 97, true); }
+66
View File
@@ -0,0 +1,66 @@
// Smelter design experiments, built with the same block pieces a player uses.
// Layout (block rows, 4x4 grains each), on flat ground at row 25:
// roof with a vent ........ row g-CH-1
// chamber, CH blocks tall .. charge of coal + copper ore inside
// grate floor ............. row g = 23 (liquids pass, powders do not)
// ingot mold .............. row 24, sitting on the ground
// side walls run from the roof down past the mold.
// Usage: node t_smelter.js [key=value ...]
const { load, flatWorld, ticks } = require('../../test/harness');
const opt = Object.assign({ wall: 'RSTONE', ratio: 0.33, ch: 4, vent: 1, grate: 1, fill: 0.65, mix: 'mix', ign: 'top', mold: 'ingot', T: 7200, dump: 0, lid: 0, base: 'RSTONE', cw: 4 },
Object.fromEntries(process.argv.slice(2).map(a => a.split('=')).map(([k, v]) => [k, isNaN(+v) ? v : +v])));
const { run } = load();
flatWorld(run, 200, 160, 100);
run('bot.x = 90; bot.y = 50;');
const bx = 23, g = 23, CH = opt.ch, top = g - CH - 1, CWB = opt.cw;
const W_ = opt.wall;
const B = (code, v, x, y) => run(`placeBlock(${code}, ${v}, ${x}, ${y}, true)`);
// mold on the ground
for (let k = 0; k < CWB / 4; k++) {
const moldErr = run(`addMold('${opt.mold}', ${bx + k * 4}, 24, ${opt.base}, true)`);
if (typeof moldErr === 'string') { console.log('mold error', moldErr); process.exit(1); }
}
// floor: grate (or plain walls with nothing) over the mold
for (let x = bx; x < bx + CWB; x++) if (opt.grate) B('BP_GRATE', 0, x, g);
// side walls from roof down to the mold row, roof with a vent in the middle
if (W_ !== 'none') {
for (let y = top; y <= 24; y++) { B('BP_WALL', W_, bx - 1, y); B('BP_WALL', W_, bx + CWB, y); }
const mid = bx + (CWB >> 1);
const ventX = opt.vent === 2 ? [mid - 1, mid] : opt.vent === 1 ? [mid] : [];
for (let x = bx; x < bx + CWB; x++) if (!ventX.includes(x)) B('BP_WALL', W_, x, top);
}
// charge
const x0 = bx * 4, x1 = (bx + CWB) * 4 - 1, yBot = g * 4 - 1, yTop = Math.round(yBot - CH * 4 * opt.fill) + 1;
run(`{ const r = mulberry32(11); for (let y = ${yTop}; y <= ${yBot}; y++) for (let x = ${x0}; x <= ${x1}; x++) {
const i = y*W+x; if (mat[i] !== EMPTY || part[i]) continue;
let ore;
if ('${opt.mix}' === 'layers') ore = ((${yBot} - y) >> 1) % 3 === 1;
else if ('${opt.mix}' === 'coalbottom') ore = y < ${yTop} + (${yBot} - ${yTop}) * ${opt.ratio};
else ore = r() < ${opt.ratio};
setCell(i, ore ? CUORE : COAL, AMB[y], 0);
} }`);
const ore0 = run(`(()=>{let n=0;for(let i=0;i<N;i++) if(mat[i]===CUORE) n++; return n})()`);
const coal0 = run(`(()=>{let n=0;for(let i=0;i<N;i++) if(mat[i]===COAL) n++; return n})()`);
// light it like a player would: igniter on the top surface (or bottom corner)
const iy = opt.ign === 'top' ? yTop : yBot - 1;
run(`for (let k=0;k<45;k++){ ${[x0 + 2, x0 + 8, x1 - 2].map(x => `toolIgnite(${x}, ${iy});`).join('')} simTick(); }`);
const stat = () => run(`(() => {
let mcu = 0, cu = 0, scr = 0, ore = 0, coal = 0, inCav = 0, peak = 0, slag = 0;
const mo = molds[0], cav = new Set(molds.flatMap(m => m.cavs.flat()));
for (let i = 0; i < N; i++) { const m = mat[i];
if (m === MCU) mcu++; else if (m === COPPER) cu++; else if (m === SCRCU) scr++; else if (m === CUORE) ore++; else if (m === COAL) coal++; else if (m === SLAG) slag++;
if ((m === MCU || m === COPPER) && cav.has(i)) inCav++; }
for (let y = ${top * 4}; y < ${g * 4}; y++) for (let x = ${x0}; x <= ${x1}; x++) peak = Math.max(peak, temp[y*W+x]);
return { mcu, cu, scr, ore, coal, slag, inCav, cav: cav.size, peak: Math.round(peak), state: molds.map(m => m.state).join('+'), ready: molds.reduce((a, m) => a + m.ready.length, 0), fill: +mo.fill.toFixed(2) };
})()`);
console.log(JSON.stringify(opt), `ore ${ore0} coal ${coal0}`);
let el = 0;
for (const dt of [900, 900, 1800, 1800, 1800, 1800, 1800, 1800].filter((_, k, a) => a.slice(0, k + 1).reduce((p, q) => p + q, 0) <= opt.T)) {
ticks(run, dt); el += dt;
const s = stat();
console.log(`${String(el / 60).padStart(4)}s peak ${String(s.peak).padStart(4)}°C ore ${String(s.ore).padStart(3)} coal ${String(s.coal).padStart(3)} molten ${String(s.mcu).padStart(3)} set ${String(s.cu).padStart(3)} in mold ${s.inCav}/${s.cav} mold ${s.state}${s.ready ? ' ' + s.ready + ' bars' : ''}`);
}
if (opt.dump) {
const ch = { [run('EMPTY')]: '.', [run('COAL')]: 'c', [run('CUORE')]: 'o', [run('MCU')]: 'M', [run('COPPER')]: '=', [run('SLAG')]: 's', [run('ASH')]: 'a', [run('FIRE')]: '^', [run('SMOKE')]: '~', [run('STONE')]: '#', [run('BRICK')]: 'B', [run('CLAY')]: 'C' };
for (let y = top * 4 - 2; y < 104; y++) { let l = ''; for (let x = x0 - 6; x <= x1 + 6; x++) { const i = run(`${y}*W+${x}`); const m = run(`mat[${i}]`); l += run(`part[${i}]`) === 5 && m === 0 ? '+' : (ch[m] || '?'); } console.log(l); }
}
+49
View File
@@ -0,0 +1,49 @@
// Plays a starter design the way a player would: paste the blueprint, let the
// bot build it from the bag, blow an even ore/coal mix inside, light it, wait,
// knock out the castings. Usage: node tools/experiments/starter.js [design id] [ore] [coal]
const { load, flatWorld, ticks } = require('../../test/harness');
const { run } = load();
const id = process.argv[2] || 'smelter1', ore = +(process.argv[3] || 40), coal = +(process.argv[4] || 40);
flatWorld(run, 240, 160, 100);
for (let k = 0; k < +(process.argv[5] || 0); k++) run('rnd()'); // shake the dice: how robust is the design?
run(`bag[RSTONE] = 900; bag[LCLAY] = 200; bag[COAL] = ${coal}; bag[CUORE] = ${ore}; bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];`);
const d = run(`JSON.stringify(STARTER_DESIGNS.find(d => d.id === '${id}').bp())`);
const bp = JSON.parse(d);
// sit it on the ground: bottom row at block 24 (ground starts at block 25)
const ox = 24, oy = 25 - bp.bh;
run(`bot.x = ${ox * 4 + bp.bw * 2 - 2}; bot.y = ${oy * 4 - 14}; pasteGhost(${d}, ${ox}, ${oy});`);
for (let k = 0; k < 40 && run('ghostBlocks.size + ghostMolds.length') > 0; k++) ticks(run, 30);
console.log(id, `${bp.bw}x${bp.bh} blocks;`, 'ghosts left', run('ghostBlocks.size + ghostMolds.length'), 'molds', run('molds.length'), 'stone used', 900 - run('bag[RSTONE]'), 'clay used', 200 - run('bag[LCLAY]'));
// fly inside (the bot passes through walls) and blow the mix across the chamber floor
const cx0 = (ox + 1) * 4, cx1 = (ox + bp.bw - 1) * 4 - 1, floorY = (oy + 4) * 4 - 1;
run(`bot.x = ${(cx0 + cx1) / 2 - 2}; bot.y = ${oy * 4};`);
run(`GCOL = [CUORE, COAL];`);
for (let k = 0; k < 400 && run('bag[CUORE] + bag[COAL]') > 0; k++) {
const tx = cx0 + 1 + (k * 5) % (cx1 - cx0 - 2);
run(`toolBlow(${tx}, ${floorY - 1}, 0, CUORE, GCOL); simTick();`);
}
ticks(run, 60);
const inside = run(`(()=>{let o=0,c=0;for(let y=${oy * 4};y<=${floorY};y++)for(let x=${cx0};x<=${cx1};x++){const m=mat[y*W+x];if(m===CUORE)o++;else if(m===COAL)c++;}return [o,c]})()`);
console.log('charge inside', inside, 'left in bag', run('[bag[CUORE], bag[COAL]]'));
// light the top of the pile from inside, then leave
// like a player: hold the igniter on a few coal grains that touch air
const spots = run(`(()=>{const out=[];for(let y=${oy * 4 + 4};y<=${floorY};y++)for(let x=${cx0};x<=${cx1};x++){const i=y*W+x;if(mat[i]===COAL&&mat[i-W]===EMPTY)out.push([x,y]);}return out})()`);
const pick = [spots[0], spots[Math.floor(spots.length / 2)], spots[spots.length - 1]].filter(Boolean);
run(`for (let k=0;k<50;k++){ ${pick.map(([x, y]) => `toolIgnite(${x}, ${y});`).join('')} simTick(); }`);
run(`bot.x = ${ox * 4 - 30}; bot.y = ${oy * 4 - 20};`);
let t = 0;
for (const dt of [900, 900, 1800, 1800]) {
ticks(run, dt); t += dt;
console.log(`${t / 60}s`, run(`JSON.stringify(molds.map(m => m.state + (m.ready.length ? ':' + m.ready.length : '') + (m.flaw ? ':' + m.flaw : '')))`),
'ore left', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===CUORE)n++;return n})()`), 'copper set outside molds', run(`(()=>{let n=0;const cav=new Set(molds.flatMap(m=>m.cavs.flat()));for(let i=0;i<N;i++)if(mat[i]===COPPER&&!cav.has(i))n++;return n})()`),
'in molds', run(`(()=>{let n=0;for(const i of molds.flatMap(m=>m.cavs.flat()))if(mat[i]===COPPER||mat[i]===MCU)n++;return n})()`), 'slag', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===SLAG)n++;return n})()`), 'molten', run(`(()=>{let n=0;for(let i=0;i<N;i++)if(mat[i]===MCU)n++;return n})()`));
}
function dump(title) {
console.log('--', title);
const ch = { [run('EMPTY')]: '.', [run('COAL')]: 'c', [run('CUORE')]: 'o', [run('MCU')]: 'M', [run('COPPER')]: '=', [run('SLAG')]: 's', [run('ASH')]: 'a', [run('FIRE')]: '^', [run('SMOKE')]: '~', [run('STONE')]: '#', [run('SCRCU')]: '%' };
const rows = run(`(()=>{const out=[];for (let y = ${oy * 4 - 2}; y < ${(oy + bp.bh) * 4 + 2}; y++) { const r = []; for (let x = ${ox * 4 - 2}; x < ${(ox + bp.bw) * 4 + 2}; x++) { const i = y * W + x; r.push([mat[i], part[i], Math.round(temp[i])]); } out.push(r); } return out; })()`);
for (const r of rows) console.log(r.map(([m, p]) => p === 5 && m === 0 ? '+' : p === 5 ? (ch[m] || '?').toUpperCase() : (ch[m] || '?')).join(''));
}
if (process.env.DUMP) dump('before knocking out');
const msgs = run(`JSON.stringify(molds.map(m => collectMold(m)))`);
console.log('knock out:', msgs, '| bars on the ground', run('ents.length'), '| bit tier', run('player.bitTier'));
+25
View File
@@ -0,0 +1,25 @@
// World generation report: time to generate, how many cells of each material, where the bot
// starts, and what idle ticks and ticks after digging cost.
const { load, ticks } = require('../../test/harness');
const { run } = load();
const t0 = Date.now();
run(`generateWorld(12345, 1024, 640)`);
console.log('worldgen ms', Date.now() - t0);
const names = run(`MD.map(d=>d.name)`);
const counts = run(`(()=>{const c=new Array(NMAT).fill(0); for(let i=0;i<N;i++) c[mat[i]]++; return c;})()`);
counts.forEach((c, i) => { if (c) console.log(String(i).padStart(2), names[i].padEnd(16), c); });
console.log('bot', run(`JSON.stringify(bot)`), 'surf at spawn', run(`surfY[W>>1]`));
// settle cost: run 600 ticks idle
let t1 = Date.now();
ticks(run, 600);
console.log('600 idle ticks ms', Date.now() - t1, 'awake chunks', run(`awake.reduce((a,b)=>a+(b>0),0)`), 'hot', run(`hot.reduce((a,b)=>a+(b>0),0)`));
// dig a big hole near spawn and measure active cost
run(`player.bitTier=3; for (let k=0;k<60;k++) { toolDig(W>>1, surfY[W>>1]+20+k, 6); }`);
t1 = Date.now();
ticks(run, 300);
console.log('300 ticks after digging ms', Date.now() - t1);
// worst case: wake everything
run(`awake.fill(3); hot.fill(6); hotLvl.fill(2)`);
t1 = Date.now();
ticks(run, 60);
console.log('60 ticks all-awake ms', Date.now() - t1);