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:
@@ -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);
|
||||
@@ -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`);
|
||||
@@ -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));
|
||||
@@ -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]);
|
||||
@@ -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); }
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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'));
|
||||
@@ -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);
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate the file and symbol index at the end of docs/PROJECT_MAP.md.
|
||||
|
||||
python3 tools/map.py rewrite the index (everything after the INDEX marker)
|
||||
python3 tools/map.py --check exit 1 if the index is out of date (nothing is written)
|
||||
|
||||
The index lists every source file in build order with its line count, its header comment, and
|
||||
the top-level functions and variables it declares, so a name can be found without searching.
|
||||
Run it after adding, moving or renaming files or top-level functions. The hand-written part of
|
||||
the map above the marker is kept as it is.
|
||||
"""
|
||||
import json, pathlib, re, sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
MAP = ROOT / 'docs' / 'PROJECT_MAP.md'
|
||||
MARK = '<!-- INDEX: generated by tools/map.py, do not edit below this line -->'
|
||||
|
||||
|
||||
def header(text):
|
||||
"""The file's leading comment (// lines or one /* block */), as one line."""
|
||||
out, block = [], False
|
||||
for line in text.split('\n'):
|
||||
s = line.strip()
|
||||
if block:
|
||||
done = s.endswith('*/')
|
||||
s = s.rstrip('/').rstrip('*').strip(' =')
|
||||
if s: out.append(s)
|
||||
if done: break
|
||||
continue
|
||||
if s in ("'use strict';", '') and not out: continue
|
||||
if s.startswith('/*'):
|
||||
block = not s.endswith('*/')
|
||||
s = s.lstrip('/').lstrip('*').rstrip('/').rstrip('*').strip(' =')
|
||||
if s: out.append(s)
|
||||
if not block: break
|
||||
elif s.startswith('//') and not s.startswith('// ----'):
|
||||
out.append(s[2:].strip())
|
||||
else:
|
||||
break
|
||||
return re.sub(r'\s+', ' ', ' '.join(out)).strip()
|
||||
|
||||
|
||||
def symbols(text):
|
||||
funcs = re.findall(r'^(?:async\s+)?function\s+([A-Za-z0-9_$]+)', text, re.M)
|
||||
names = []
|
||||
for m in re.finditer(r'^(?:const|let|var)\s+(.+)$', text, re.M):
|
||||
decl = m.group(1)
|
||||
# names at the top level of the declaration list (skip what is inside brackets)
|
||||
depth, cur, parts = 0, '', []
|
||||
for ch in decl:
|
||||
if ch in '([{': depth += 1
|
||||
elif ch in ')]}': depth -= 1
|
||||
if ch == ',' and depth == 0: parts.append(cur); cur = ''
|
||||
else: cur += ch
|
||||
if ch == ';' and depth == 0: break
|
||||
parts.append(cur)
|
||||
for p in parts:
|
||||
n = re.match(r'\s*([A-Za-z_$][A-Za-z0-9_$]*)', p)
|
||||
if n and n.group(1) not in names: names.append(n.group(1))
|
||||
return funcs, names
|
||||
|
||||
|
||||
def index():
|
||||
man = json.loads((ROOT / 'src' / 'manifest.json').read_text())
|
||||
lines = [MARK, '', '## Index of source files (build order)', '']
|
||||
for f in man['sim'] + man['client']:
|
||||
text = (ROOT / 'src' / f).read_text()
|
||||
funcs, names = symbols(text)
|
||||
lines.append(f'### src/{f} ({text.count(chr(10))} lines)')
|
||||
h = header(text)
|
||||
if h: lines.append(h)
|
||||
if funcs: lines.append('- functions: ' + ', '.join(funcs))
|
||||
if names: lines.append('- variables: ' + ', '.join(names))
|
||||
lines.append('')
|
||||
lines += ['## Tests and tools', '']
|
||||
for d in ['test', 'tools', 'tools/experiments', 'tools/scenes']:
|
||||
for p in sorted((ROOT / d).glob('*')):
|
||||
if p.suffix not in ('.js', '.py') : continue
|
||||
text = p.read_text()
|
||||
m = re.search(r'"""(.+?)(?:\n\n|""")', text, re.S) if p.suffix == '.py' else None
|
||||
h = re.sub(r'\s+', ' ', m.group(1)).strip() if m else header(text)
|
||||
lines.append(f'- `{d}/{p.name}`: {h}')
|
||||
return '\n'.join(lines).rstrip() + '\n'
|
||||
|
||||
|
||||
def main():
|
||||
cur = MAP.read_text() if MAP.exists() else '# Project map\n\n'
|
||||
head = cur.split(MARK)[0].rstrip() + '\n\n'
|
||||
new = head + index()
|
||||
if '--check' in sys.argv:
|
||||
ok = new == cur
|
||||
print('index is up to date' if ok else 'index is out of date: run python3 tools/map.py')
|
||||
sys.exit(0 if ok else 1)
|
||||
MAP.parent.mkdir(exist_ok=True)
|
||||
MAP.write_text(new)
|
||||
print(f'wrote {MAP.relative_to(ROOT)}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Times the renderer and the simulation in a headless browser: light, drawWorld (with and without
|
||||
light and glow), the pixel upload and blit, and one simulation tick, in ms. Rebuild first.
|
||||
|
||||
python3 tools/perf.py
|
||||
"""
|
||||
import sys, pathlib
|
||||
from playwright.sync_api import sync_playwright
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
url = (root / 'dist' / 'index.html').as_uri()
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=['--enable-gpu-rasterization','--ignore-gpu-blocklist'])
|
||||
pg = b.new_page(viewport={'width': 1440, 'height': 860})
|
||||
pg.goto(url); pg.wait_for_timeout(300)
|
||||
pg.evaluate("startNewWorld(424242, 1024, 640)")
|
||||
pg.wait_for_function('GAME.running === true', timeout=20000)
|
||||
pg.wait_for_timeout(500)
|
||||
res = pg.evaluate('''() => {
|
||||
const t = (f, n) => { const a = performance.now(); for (let k=0;k<n;k++) f(); return ((performance.now()-a)/n).toFixed(2); };
|
||||
const out = {};
|
||||
out.light = t(() => computeLight(), 20);
|
||||
out.drawWorld = t(() => drawWorld(), 20);
|
||||
SET.lighting = 'off'; out.drawWorldNoLight = t(() => drawWorld(), 20); SET.lighting='full';
|
||||
SET.glow = 'off'; out.drawWorldNoGlow = t(() => drawWorld(), 20); SET.glow='low';
|
||||
out.put = t(() => { R.bctx.putImageData(R.img,0,0); }, 20);
|
||||
out.blit = t(() => { R.ctx.drawImage(R.buf, 0, 0, R.bw, R.bh, 0, 0, R.bw*R.zp, R.bh*R.zp); }, 20);
|
||||
out.sim = t(() => simTick(), 60);
|
||||
out.view = [R.bw, R.bh, R.zp, R.lw, R.lh];
|
||||
return out;
|
||||
}''')
|
||||
print(res)
|
||||
b.close()
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Headless browser run: load the page, start a world, play a little, take screenshots."""
|
||||
import sys, time, pathlib
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parent.parent
|
||||
out = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else root / 'shots' / 'playthrough')
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
url = (root / 'dist' / 'index.html').as_uri()
|
||||
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=['--autoplay-policy=no-user-gesture-required'])
|
||||
pg = b.new_page(viewport={'width': 1440, 'height': 860})
|
||||
logs = []
|
||||
pg.on('console', lambda m: logs.append(f'{m.type}: {m.text}'))
|
||||
pg.on('pageerror', lambda e: logs.append(f'PAGEERROR: {e}'))
|
||||
pg.goto(url)
|
||||
pg.wait_for_timeout(800)
|
||||
pg.screenshot(path=str(out / '01-title.png'))
|
||||
pg.click('#btn-new')
|
||||
pg.fill('#nw-seed', '424242')
|
||||
pg.click('#btn-generate')
|
||||
pg.wait_for_function('GAME.running === true', timeout=20000)
|
||||
pg.wait_for_timeout(1500)
|
||||
pg.screenshot(path=str(out / '02-spawn.png'))
|
||||
# dig straight down for a while
|
||||
pg.mouse.move(720, 560)
|
||||
pg.mouse.down()
|
||||
pg.keyboard.down('s')
|
||||
pg.wait_for_timeout(2500)
|
||||
pg.keyboard.up('s')
|
||||
pg.mouse.up()
|
||||
pg.wait_for_timeout(400)
|
||||
pg.screenshot(path=str(out / '03-digging.png'))
|
||||
print('bag', pg.evaluate('bagTotal'), pg.evaluate('JSON.stringify(BAG_ORDER.filter(m=>bag[m]>0).map(m=>[MD[m].name,bag[m]]))'))
|
||||
# build a coal fire scenario directly through the core API near the bot, then ignite with the tool
|
||||
pg.evaluate('''() => {
|
||||
const bx = Math.floor(bot.x)+12, by = Math.floor(bot.y)+3;
|
||||
bag[COAL] += 300; bag[CUORE] += 120; bag[STONE] += 400; bagTotal += 820;
|
||||
for (let y=by-14; y<=by+6; y++) for (let x=bx-2; x<=bx+26; x++) setCell(y*W+x, EMPTY, AMB[y], 0);
|
||||
for (let y=by-12; y<=by+6; y++) { setCell(y*W+bx-2, STONE, AMB[y], 0); setCell(y*W+bx-1, STONE, AMB[y], 0); setCell(y*W+bx+25, STONE, AMB[y], 0); setCell(y*W+bx+26, STONE, AMB[y], 0); }
|
||||
for (let x=bx-2; x<=bx+26; x++) { if (x < bx+11 || x > bx+13) { setCell((by-12)*W+x, STONE, AMB[by-12], 0); setCell((by-13)*W+x, STONE, AMB[by-13], 0);} setCell((by+6)*W+x, STONE, AMB[by+6], 0); }
|
||||
const r = mulberry32(4);
|
||||
for (let y=by-4; y<=by+5; y++) for (let x=bx; x<=bx+24; x++) setCell(y*W+x, r()<0.3 ? CUORE : COAL, AMB[y], 0);
|
||||
for (let k=0;k<50;k++){ toolIgnite(bx+3, by-5); toolIgnite(bx+20, by-5); simTick(); }
|
||||
window.__fx = {bx, by};
|
||||
}''')
|
||||
pg.wait_for_timeout(9000)
|
||||
pg.screenshot(path=str(out / '04-furnace.png'))
|
||||
pg.keyboard.press('h')
|
||||
pg.wait_for_timeout(300)
|
||||
pg.screenshot(path=str(out / '05-lens.png'))
|
||||
pg.keyboard.press('h')
|
||||
pg.keyboard.press('3')
|
||||
pg.wait_for_timeout(200)
|
||||
pg.screenshot(path=str(out / '06-parts.png'))
|
||||
pg.keyboard.press('n')
|
||||
pg.wait_for_timeout(300)
|
||||
pg.screenshot(path=str(out / '07-notebook.png'))
|
||||
pg.keyboard.press('Escape')
|
||||
pg.keyboard.press('Escape')
|
||||
pg.wait_for_timeout(200)
|
||||
pg.click('#pm-settings')
|
||||
pg.wait_for_timeout(200)
|
||||
pg.screenshot(path=str(out / '08-settings.png'))
|
||||
perf = pg.evaluate('JSON.stringify({frameMs, simMs, fps: fpsSm, view: [R.bw, R.bh]})')
|
||||
print('perf', perf)
|
||||
print('molten copper', pg.evaluate('(()=>{let n=0,c=0;for(let i=0;i<N;i++){if(mat[i]===MCU)n++; if(mat[i]===COPPER)c++;} return [n,c]})()'))
|
||||
print('\n'.join(l for l in logs if 'error' in l.lower() or 'PAGEERROR' in l) or 'no errors')
|
||||
b.close()
|
||||
@@ -0,0 +1,18 @@
|
||||
// A clear work area left of the bot's starting point (the trade dock is to its right): open
|
||||
// sky 160 grains wide and 40 tall, over a flat stone floor. Money and a bag of building
|
||||
// stuff to build with. Sets FLOOR (the first row of the floor), BX, BY (the leftmost block
|
||||
// of the area that stands on the floor) and CAM (the middle of the area).
|
||||
money = 50000;
|
||||
const x0 = Math.max(4, SX - 170), x1 = SX - 10, y0 = SY - 40;
|
||||
window.FLOOR = SY + 40;
|
||||
for (let y = y0; y < FLOOR + 8; y++) for (let x = x0; x < x1; x++) {
|
||||
const i = y * W + x;
|
||||
bgm[i] = 0;
|
||||
setCell(i, y >= FLOOR ? STONE : EMPTY, AMB[y], y >= FLOOR ? F_NAT : 0);
|
||||
}
|
||||
// open sky down to the floor (the renderer draws cave wall behind empty cells below surfY)
|
||||
for (let x = x0; x < x1; x++) { surfY[x] = FLOOR; dirtY[x] = Math.max(dirtY[x], FLOOR + 8); }
|
||||
updateRoof();
|
||||
window.BX = Math.ceil(x0 / 4) + 1; window.BY = Math.floor(FLOOR / 4) - 1;
|
||||
bag[RSTONE] = 400; bag[RBRICK] = 200; bagTotal = 600;
|
||||
window.CAM = [(x0 + x1) / 2, SY + 20];
|
||||
@@ -0,0 +1,14 @@
|
||||
// The stone and sand machines: a half-full silo with its gate open over a belt, a stone cutter
|
||||
// fed with rubble, and cut stone and glass brick walls. Run clear-sky.js first, e.g.
|
||||
// python3 tools/shot.py /tmp/stone.png --setup tools/scenes/clear-sky.js --js "$(cat tools/scenes/stone-uses.js)" --ticks 240 --zoom 7
|
||||
const bx = BX + 1, by = BY - 7; // the silo is 6 blocks tall; its gate pours onto a belt a block below
|
||||
placeMachineAt('silo', 0, bx, by);
|
||||
const silo = machines.find(m => m && m.type === 'silo'); silo.sm = RSTONE; silo.sn = 2300; silo.open = true;
|
||||
for (let k = bx - 2; k <= bx + 10; k++) placeBlock(BP_BELT, 1, k, BY, true);
|
||||
placeMachineAt('cutter', 0, bx + 14, BY - 3);
|
||||
for (let k = bx + 12; k <= bx + 22; k++) placeBlock(BP_BELT, 1, k, BY - 1, true);
|
||||
for (let y = (BY - 3) * 4; y < (BY - 3) * 4 + 5; y++) for (let x = (bx + 14) * 4 + 2; x < (bx + 14) * 4 + 6; x++) setCell(y * W + x, RSTONE, AMB[y], 0);
|
||||
bag[BARSB] = 40; bag[BARGL] = 20; bagTotal += 60;
|
||||
for (let k = 0; k < 4; k++) for (let j = 0; j < 3; j++) placeBlock(BP_WALL, BARSB, bx + 26 + k, BY - j, false);
|
||||
for (let k = 0; k < 2; k++) placeBlock(BP_WALL, BARGL, bx + 31 + k, BY, false);
|
||||
window.CAM = [(bx + 16) * 4, (BY - 4) * 4];
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Screenshot the game in a headless browser, after setting up a scene with JavaScript.
|
||||
|
||||
python3 tools/shot.py OUT.png [options]
|
||||
|
||||
Starts a new world in dist/index.html (run python3 build.py first), runs your setup code in the
|
||||
page (every game global is available: setCell, placeBlock, placeMachineAt, bot, SET, GAME ...),
|
||||
runs the simulation for --ticks steps (it is paused otherwise, so the same options give the same
|
||||
picture), then saves a PNG. Open the PNG to look at it.
|
||||
|
||||
The camera follows the bot. To frame something else, set CAM = [x, y] (in grains) in your setup
|
||||
code and the bot is moved there before the shot. SX, SY are the bot's starting position.
|
||||
|
||||
Options:
|
||||
--look pixel|smooth which look to draw (default: pixel; use smooth only to check graphics)
|
||||
--seed N world seed (default 424242)
|
||||
--size WxH world size in grains (default 2048x1024)
|
||||
--setup FILE.js code to run once the world is ready (see tools/scenes/ for examples)
|
||||
--js CODE code to run after --setup (both may be given)
|
||||
--ticks N simulation ticks to run after the setup (default 0)
|
||||
--zoom Z screen pixels per grain (default 6)
|
||||
--hud keep the HUD and build menu (hidden by default)
|
||||
--previews keep the cursor previews (drill aim, build ghost); hidden by default
|
||||
--viewport WxH browser window size (default 1280x760)
|
||||
--clip X,Y,W,H save only this part of the window
|
||||
--no-light switch off lighting (shows true colours, as in daylight)
|
||||
--freeze draw the last frame at a fixed time and frame count, so animation (belts,
|
||||
saws, the bot's bob) is caught at the same moment every time: two shots
|
||||
of the same scene then match pixel for pixel, and a before / after pair
|
||||
shows only what really changed
|
||||
--page PATH another built page to load (default dist/index.html)
|
||||
|
||||
Exit status 1 if the page threw an error (they are printed).
|
||||
|
||||
Example: a belt carrying a heap of sand, in the smooth look:
|
||||
python3 tools/shot.py /tmp/belt.png --look smooth --setup tools/scenes/clear-sky.js \\
|
||||
--js "placeBlocks(BP_BELT, 1, blockLine(SX/4+2, SY/4+6, SX/4+12, SY/4+6), undefined, false)" --ticks 30
|
||||
"""
|
||||
import argparse, pathlib, sys
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
from where import translate
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
# Headless Chromium has no GPU. These flags give it WebGL2 through SwiftShader (software), and
|
||||
# window.__allowSoftwareGL tells the game to use the smooth look anyway (players on software
|
||||
# rendering get the pixel look instead).
|
||||
GL_ARGS = ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader']
|
||||
FREEZE = """(() => { const real = performance.now.bind(performance); window.__frozenNow = null;
|
||||
performance.now = () => window.__frozenNow === null ? real() : window.__frozenNow; })();"""
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument('out')
|
||||
ap.add_argument('--look', choices=['pixel', 'smooth'], default='pixel')
|
||||
ap.add_argument('--seed', type=int, default=424242)
|
||||
ap.add_argument('--size', default='2048x1024')
|
||||
ap.add_argument('--setup')
|
||||
ap.add_argument('--js')
|
||||
ap.add_argument('--ticks', type=int, default=0)
|
||||
ap.add_argument('--zoom', type=float, default=6)
|
||||
ap.add_argument('--hud', action='store_true')
|
||||
ap.add_argument('--previews', action='store_true')
|
||||
ap.add_argument('--viewport', default='1280x760')
|
||||
ap.add_argument('--clip')
|
||||
ap.add_argument('--freeze', action='store_true')
|
||||
ap.add_argument('--no-light', action='store_true')
|
||||
ap.add_argument('--page', default=str(ROOT / 'dist' / 'index.html'))
|
||||
a = ap.parse_args()
|
||||
ww, wh = (int(v) for v in a.size.split('x'))
|
||||
vw, vh = (int(v) for v in a.viewport.split('x'))
|
||||
errors = []
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch(args=GL_ARGS)
|
||||
pg = b.new_page(viewport={'width': vw, 'height': vh})
|
||||
pg.on('pageerror', lambda e: errors.append(translate(e.stack or str(e))))
|
||||
pg.on('console', lambda m: errors.append('console: ' + m.text) if m.type == 'error' else None)
|
||||
pg.add_init_script('window.__allowSoftwareGL = true;' + (FREEZE if a.freeze else ''))
|
||||
pg.goto(pathlib.Path(a.page).resolve().as_uri())
|
||||
pg.wait_for_timeout(300)
|
||||
# pause the moment the world exists, so the shot depends only on the seed, the setup
|
||||
# code and --ticks (simTick is deterministic), not on how fast the browser ran
|
||||
# (wrapped in a function: Playwright calls a string that evaluates to a function)
|
||||
pg.evaluate("() => { const enter = enterGame; enterGame = function () { enter(); GAME.paused = true; }; }")
|
||||
pg.evaluate(f"SET.look = '{a.look}'; {"SET.lighting = 'off';" if a.no_light else ''} startNewWorld({a.seed}, {ww}, {wh})")
|
||||
pg.wait_for_function('GAME.running === true', timeout=60000)
|
||||
pg.evaluate("document.querySelectorAll('.card, .toast').forEach(e => e.remove())")
|
||||
# SX, SY: the bot's position when the world starts, handy for placing things near it
|
||||
pg.evaluate(f"window.SX = Math.floor(bot.x); window.SY = Math.floor(bot.y); SET.zoom = {a.zoom}; resizeRender();")
|
||||
for code in [pathlib.Path(a.setup).read_text() if a.setup else None, a.js]:
|
||||
if code:
|
||||
pg.evaluate(f'() => {{ {code}\n}}')
|
||||
if a.ticks:
|
||||
pg.evaluate(f'for (let k = 0; k < {a.ticks}; k++) simTick();')
|
||||
pg.evaluate(f"""GAME.paused = true; $('hud').hidden = {'false' if a.hud else 'true'}; GAME.hidePreviews = {'false' if a.previews else 'true'};
|
||||
if (window.CAM) {{ bot.x = CAM[0] - bot.w / 2; bot.y = CAM[1] - bot.h / 2; }}
|
||||
snapCamera(); R.snapNext = true;""")
|
||||
pg.wait_for_timeout(600)
|
||||
if a.freeze:
|
||||
# stop the frame loop, then draw one last frame at a fixed time and frame count
|
||||
pg.evaluate("() => { window.__frozenNow = 5000; window.requestAnimationFrame = () => 0; }")
|
||||
pg.wait_for_timeout(100)
|
||||
pg.evaluate("() => { R.frame = 999; if (typeof SM !== 'undefined') SM.t0 = 0; frame(lastRender + 1000); }")
|
||||
kw = {}
|
||||
if a.clip:
|
||||
x, y, w, h = (float(v) for v in a.clip.split(','))
|
||||
kw['clip'] = {'x': x, 'y': y, 'width': w, 'height': h}
|
||||
pathlib.Path(a.out).parent.mkdir(parents=True, exist_ok=True)
|
||||
pg.screenshot(path=a.out, **kw)
|
||||
note = pg.evaluate("SET.look === 'smooth' ? (SM.ok ? 'smooth look' : 'smooth look FAILED: ' + SM.why) : 'pixel look'")
|
||||
b.close()
|
||||
print(f'saved {a.out} ({note})')
|
||||
for e in errors:
|
||||
print('ERROR', e)
|
||||
sys.exit(1 if errors else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Turn a line number in dist/index.html into the source file and line it came from.
|
||||
|
||||
python3 tools/where.py 4211 -> src/sim/pipes.js:57
|
||||
python3 tools/where.py < error.txt -> the text with every index.html:LINE rewritten
|
||||
|
||||
Browser errors point into the built page. build.py writes dist/bundle-lines.json, which says where
|
||||
each source file starts in it. Rebuild first if the sources changed since the error.
|
||||
"""
|
||||
import json, pathlib, re, sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load():
|
||||
return json.loads((ROOT / 'dist' / 'bundle-lines.json').read_text())['files']
|
||||
|
||||
|
||||
def where(line, files=None):
|
||||
files = files or load()
|
||||
best = None
|
||||
for start, f in files:
|
||||
if start <= line: best = (start, f)
|
||||
else: break
|
||||
return f'src/{best[1]}:{line - best[0] + 1}' if best else f'index.html:{line} (before the script)'
|
||||
|
||||
|
||||
def translate(text, files=None):
|
||||
files = files or load()
|
||||
return re.sub(r'(?:file://)?[^\s()]*index\.html:(\d+)(:\d+)?', lambda m: where(int(m.group(1)), files) + (m.group(2) or ''), text)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
for a in sys.argv[1:]: print(where(int(a)))
|
||||
else:
|
||||
print(translate(sys.stdin.read()), end='')
|
||||
Reference in New Issue
Block a user