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
84 lines
4.5 KiB
JavaScript
84 lines
4.5 KiB
JavaScript
// Loads the simulation (the src/sim/ files listed in src/manifest.json, joined in order into one
|
|
// script, the same way build.py joins them) so scenarios can drive it headlessly in Node.
|
|
// Errors point at the real file and line (sim/pipes.js:42), not at the joined script.
|
|
const fs = require('fs');
|
|
const vm = require('vm');
|
|
const path = require('path');
|
|
|
|
const SRC = path.join(__dirname, '../src');
|
|
let starts = []; // [first line in the joined script, file]
|
|
function where(line) {
|
|
let f = starts[0];
|
|
for (const s of starts) if (s[0] <= line) f = s; else break;
|
|
return f ? `${f[1]}:${line - f[0] + 1}` : `sim:${line}`;
|
|
}
|
|
function fixStack(e) {
|
|
if (e && typeof e.stack === 'string') e.stack = e.stack.replace(/sim-bundle\.js:(\d+)/g, (_, n) => 'src/' + where(+n));
|
|
return e;
|
|
}
|
|
function load() {
|
|
const man = JSON.parse(fs.readFileSync(path.join(SRC, 'manifest.json'), 'utf8'));
|
|
const parts = []; let line = 1; starts = [];
|
|
for (const f of man.sim) {
|
|
const text = fs.readFileSync(path.join(SRC, f), 'utf8').replace(/\n+$/, '');
|
|
starts.push([line, f]); parts.push(text); line += text.split('\n').length;
|
|
}
|
|
try { vm.runInThisContext(parts.join('\n'), { filename: 'sim-bundle.js' }); } catch (e) { throw fixStack(e); }
|
|
const run = (src) => { try { return vm.runInThisContext(src); } catch (e) { throw fixStack(e); } };
|
|
return { run };
|
|
}
|
|
|
|
// A test suite's checks: check(name, ok, detail) prints PASS / FAIL; done() prints the total and
|
|
// sets a failing exit code, so test/run_all.py can tell.
|
|
function suite() {
|
|
let fails = 0;
|
|
const check = (name, cond, extra) => { console.log((cond ? 'PASS ' : 'FAIL ') + name + (extra !== undefined ? ` [${extra}]` : '')); if (!cond) fails++; };
|
|
const done = () => { console.log(fails ? `${fails} FAILED` : 'ALL PASS'); if (fails) process.exitCode = 1; };
|
|
return { check, done, get fails() { return fails; } };
|
|
}
|
|
|
|
// Build a flat test world: sky above row `ground`, stone below.
|
|
function flatWorld(run, w, h, ground, opts = {}) {
|
|
run(`
|
|
alloc(${w}, ${h});
|
|
worldSeed = 7;
|
|
for (let x = 0; x < W; x++) { surfY[x] = ${ground}; dirtY[x] = ${ground}+10; graniteY[x] = H; basaltY[x] = H; magmaY[x] = H; }
|
|
computeAmbient(); fillNoise();
|
|
for (let y = 0; y < H; y++) for (let x = 0; x < W; x++) {
|
|
const i = y*W+x; mat[i] = y >= ${ground} ? STONE : EMPTY; flags[i] = y >= ${ground} ? F_NAT : 0; temp[i] = AMB[y];
|
|
}
|
|
bot.x = (W>>1) - 8; bot.y = ${ground} - 30;
|
|
bag = new Int32Array(NMAT); bagTotal = 0;
|
|
player.bitTier = 0; player.tankTier = 3; player.lens = true; player.flask = true;
|
|
updateRoof(); resetWeather(); weather.next = 1e15; // no showers unless a test asks for one
|
|
`);
|
|
}
|
|
function put(run, x, y, m, t) { run(`setCell(${y}*W+${x}, ${m}, ${t === undefined ? `AMB[${y}]` : t}, 0)`); }
|
|
function rect(run, x0, y0, x1, y1, m) { run(`for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) setCell(y*W+x, ${m}, AMB[y], 0)`); }
|
|
function count(run, m, x0 = 0, y0 = 0, x1 = null, y1 = null) {
|
|
return run(`(()=>{let n=0; const X1=${x1 === null ? 'W-1' : x1}, Y1=${y1 === null ? 'H-1' : y1}; for (let y=${y0};y<=Y1;y++) for (let x=${x0};x<=X1;x++) if (mat[y*W+x]===${m}) n++; return n;})()`);
|
|
}
|
|
function maxTemp(run, x0, y0, x1, y1) {
|
|
return run(`(()=>{let t=-1e9; for (let y=${y0};y<=${y1};y++) for (let x=${x0};x<=${x1};x++) t=Math.max(t,temp[y*W+x]); return Math.round(t);})()`);
|
|
}
|
|
function ticks(run, n) { run(`for (let k=0;k<${n};k++) simTick();`); }
|
|
|
|
module.exports = { load, suite, flatWorld, put, rect, count, maxTemp, ticks };
|
|
|
|
// ASCII cross-section: one char per cell
|
|
const GLYPH = { 4: ':', 34: '<', 35: '>', 36: '-', 37: '|', 38: 'H', 0: ' ', 7: '#', 6: 'c', 20: 'B', 13: '.', 15: 'o', 17: 'f', 18: ',', 19: 's', 22: 'C', 23: 'I', 24: 'S', 27: '~', 28: '=', 29: '%', 25: 'w', 31: '"', 32: ';', 33: '^', 26: 'M', 21: 'g', 30: '*' };
|
|
function dump(run, x0, y0, x1, y1, withTemp) {
|
|
const rows = run(`(()=>{const r=[];for(let y=${y0};y<=${y1};y++){const row=[];for(let x=${x0};x<=${x1};x++){const i=y*W+x;row.push([mat[i],Math.round(temp[i]),flags[i]&F_BURN?1:0]);}r.push(row);}return r;})()`);
|
|
for (const row of rows) {
|
|
let a = '', b = '';
|
|
for (const [m, t, burn] of row) {
|
|
let g = GLYPH[m] !== undefined ? GLYPH[m] : '?';
|
|
if (burn) g = g === '.' ? '*' : g;
|
|
a += g;
|
|
b += t >= 1500 ? '9' : t >= 1300 ? '8' : t >= 1085 ? '7' : t >= 900 ? '6' : t >= 700 ? '5' : t >= 500 ? '4' : t >= 300 ? '3' : t >= 150 ? '2' : t >= 60 ? '1' : '0';
|
|
}
|
|
console.log(a + (withTemp ? ' ' + b : ''));
|
|
}
|
|
}
|
|
module.exports.dump = dump;
|