115 lines
5.9 KiB
JavaScript
115 lines
5.9 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;
|
|
|
|
// Node uses exactly the browser worker entry point and scheduler.
|
|
async function enableThreads(run, count) {
|
|
const { Worker } = require('worker_threads');
|
|
const entry = `const {parentPort}=require('worker_threads');
|
|
require(${JSON.stringify(__filename)}).load();
|
|
parentPort.on('message', data=>threadMessage(data));`;
|
|
const pool = { world: run('mat'), workers: [], slots: [], force: true };
|
|
try {
|
|
const ready = [];
|
|
for (let k = 0; k < count; k++) {
|
|
const worker = new Worker(entry, {eval:true}), slot = run('threadSlot()');
|
|
pool.workers.push(worker); pool.slots.push(slot);
|
|
ready.push(new Promise((resolve,reject) => {
|
|
const deadline = Date.now()+10000;
|
|
worker.once('error',reject);
|
|
const poll = () => {
|
|
if (Atomics.load(slot.control,0)) resolve();
|
|
else if (Date.now()>deadline) reject(Error('Worker startup timed out'));
|
|
else setTimeout(poll,1);
|
|
};
|
|
poll();
|
|
}));
|
|
worker.postMessage({world:run('threadWorld()'),slot});
|
|
}
|
|
await Promise.all(ready);
|
|
globalThis.__testPool = pool; run('simPool = globalThis.__testPool;'); delete globalThis.__testPool;
|
|
return async () => { run('simPool=null;'); await Promise.all(pool.workers.map(w=>w.terminate())); };
|
|
} catch (e) { await Promise.all(pool.workers.map(w=>w.terminate())); throw e; }
|
|
}
|
|
module.exports.enableThreads = enableThreads;
|