// 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;