162 lines
6.9 KiB
JavaScript
162 lines
6.9 KiB
JavaScript
/**
|
|
* Station-variant-spread test (dev tool, run with Node — no browser):
|
|
*
|
|
* node dev/station-frames.test.mjs
|
|
*
|
|
* Asserts the galaxy-wide deep-space-station variant pass
|
|
* (js/galaxy/StationFrames.js, wired in js/galaxy/Galaxy.js,
|
|
* data/stations.json → variants):
|
|
* - every deep-space station carries a sheet frame inside the variant
|
|
* pool (settlement.stationFrame, stamped by the content generator);
|
|
* - the pass beats a naive random pick: the variant collision rate
|
|
* among each station's 8-nearest stars is LOWER than the same metric
|
|
* for an independent random-per-station assignment;
|
|
* - determinism: same seed ⇒ identical variant map, different seed ⇒
|
|
* different assignment (spot check);
|
|
* - lazy === eager: the stamped settlement frames equal a fresh
|
|
* galaxy's pass output.
|
|
*/
|
|
|
|
process.env.NODE_ENV = 'dev';
|
|
|
|
import { pathToFileURL } from 'node:url';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const { config } = await import(pathToFileURL(join(__dirname, '../js/config/Config.js')).href);
|
|
const fs = await import('node:fs');
|
|
const dataDir = join(__dirname, '../data');
|
|
const configData = {};
|
|
for (const f of fs.readdirSync(dataDir)) {
|
|
if (!f.endsWith('.json') || f === 'manifest.json') continue;
|
|
configData[f.replace(/\.json$/i, '')] = JSON.parse(fs.readFileSync(join(dataDir, f), 'utf8'));
|
|
}
|
|
config.init(configData);
|
|
|
|
const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href);
|
|
const { Rng } = await import(pathToFileURL(join(__dirname, '../js/utils/Rng.js')).href);
|
|
|
|
let failures = 0;
|
|
const check = (label, cond, extra = '') => {
|
|
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}${cond ? '' : ' — ' + extra}`);
|
|
if (!cond) failures++;
|
|
};
|
|
|
|
const SEED = 'station-frames-test-seed';
|
|
const g = Galaxy.create(SEED);
|
|
const POOL = Math.max(1, Math.floor(g.params.neighbors ?? 8));
|
|
const POOLV = (config.get('stations.variants') ?? []).filter((f) => Number.isInteger(f) && f >= 0);
|
|
check('data/stations.json → variants is a non-empty integer pool', POOLV.length >= 1, JSON.stringify(config.get('stations.variants')));
|
|
|
|
// The station-bearing systems and their stamped frame (via content).
|
|
const stations = new Map(); // id → { rec, frame, name }
|
|
for (const rec of g.records) {
|
|
const c = g.ensureContent(rec.id);
|
|
for (const s of c.settlements) {
|
|
if (s.kind === 'deepSpaceStation') {
|
|
stations.set(rec.id, { rec, frame: s.stationFrame, name: s.name });
|
|
}
|
|
}
|
|
}
|
|
console.log(` (galaxy: ${g.records.length} systems, ${stations.size} deep-space stations, pool [${POOLV}])`);
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 1. Frames are present and in-pool
|
|
// ----------------------------------------------------------------------
|
|
{
|
|
let inPool = true;
|
|
let why = '';
|
|
for (const [id, st] of stations) {
|
|
if (typeof st.frame !== 'number' || !Number.isInteger(st.frame) || !POOLV.includes(st.frame)) {
|
|
inPool = false;
|
|
if (!why) why = `${id}: frame ${st.frame} ∉ [${POOLV}]`;
|
|
}
|
|
}
|
|
check('every deep-space station wears a pool variant (settlement.stationFrame)', inPool, why);
|
|
check('the galaxy pass covers exactly the station-bearing systems',
|
|
stations.size === g.stationFrames.size,
|
|
`${stations.size} settlements vs ${g.stationFrames.size} pass entries`);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 2. The pass beats a naive random assignment
|
|
// ----------------------------------------------------------------------
|
|
{
|
|
// The pass's variant collision count: for each station, count its
|
|
// POOL-nearest stars whose station wears the SAME frame (directed).
|
|
// Aggregated over THREE seeds — a single galaxy is sparse in
|
|
// station adjacency (most stars carry no station), and the naive
|
|
// random baseline can be 0 in any given seed; summed, it is a
|
|
// stable floor the pass must beat.
|
|
const SEEDS = [SEED, 'sf-seed-b', 'sf-seed-c'];
|
|
let passColl = 0;
|
|
let naiveColl = 0;
|
|
let checks = 0;
|
|
for (const sd of SEEDS) {
|
|
const gg = Galaxy.create(sd);
|
|
const sts = new Map(); // id → stamped frame
|
|
for (const rec of gg.records)
|
|
for (const s of gg.ensureContent(rec.id).settlements)
|
|
if (s.kind === 'deepSpaceStation') sts.set(rec.id, s.stationFrame);
|
|
// Naive: independent random pool picks (seeded, per station).
|
|
const naiveF = {};
|
|
for (const id of sts.keys())
|
|
naiveF[id] = POOLV.length === 1
|
|
? POOLV[0]
|
|
: POOLV[Math.floor(Rng.derive(sd, 'naive-station', id).next() * POOLV.length)];
|
|
for (const [id, fr] of sts) {
|
|
for (const nb of gg.neighborsOf(id, POOL)) {
|
|
checks++;
|
|
const nfr = sts.get(nb.id);
|
|
if (nfr !== undefined && nfr === fr) passColl++;
|
|
const nnfr = naiveF[nb.id];
|
|
if (nnfr !== undefined && nnfr === fr) naiveColl++;
|
|
}
|
|
}
|
|
}
|
|
check(
|
|
`variant collisions vs the ${POOL}-nearest pool (3 seeds): pass ${passColl} < naive ${naiveColl}`,
|
|
passColl < naiveColl && naiveColl >= 1,
|
|
`${passColl}/${checks} vs ${naiveColl}/${checks} (station density sets the floor — most stars carry none)`,
|
|
);
|
|
|
|
// The spread should also produce ALL variants across the galaxy
|
|
// (with 3 stations' worth of pool breadth and this many stations,
|
|
// pigeonhole is generous) — the point is variety, not just local spread.
|
|
const used = new Set(stations.size === 0 ? [] : [...stations.values()].map((s) => s.frame));
|
|
check('the galaxy wears more than one station type (the pool shows variety)',
|
|
used.size >= Math.min(2, POOLV.length, stations.size),
|
|
`used [${[...used].sort()}] of pool [${POOLV}]`);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 3. Determinism + lazy === eager
|
|
// ----------------------------------------------------------------------
|
|
{
|
|
const g2 = Galaxy.create(SEED);
|
|
const same = JSON.stringify([...g.stationFrames.entries()].sort()) ===
|
|
JSON.stringify([...g2.stationFrames.entries()].sort());
|
|
check('same seed ⇒ same station-variant assignment galaxy-wide', same);
|
|
|
|
// Content stamp === galaxy pass (the stamping contract).
|
|
let stamped = true;
|
|
let why = '';
|
|
for (const [id, st] of stations) {
|
|
if (g2.stationFrames.get(id) !== st.frame) {
|
|
stamped = false;
|
|
if (!why) why = `${id}: content ${st.frame} vs pass ${g2.stationFrames.get(id)}`;
|
|
}
|
|
}
|
|
check('content settlement frames === the galaxy pass (lazy === eager stamping)', stamped, why);
|
|
|
|
const g3 = Galaxy.create('station-frames-OTHER');
|
|
const diff = JSON.stringify([...g.stationFrames.entries()].sort()) !==
|
|
JSON.stringify([...g3.stationFrames.entries()].sort());
|
|
check('different seed ⇒ different assignment (spot check)', diff || g3.records[0].id !== g.records[0].id);
|
|
}
|
|
|
|
console.log(failures === 0 ? '\nAll station-variant tests passed ✔' : `\n${failures} test(s) FAILED ✘`);
|
|
process.exit(failures === 0 ? 0 : 1);
|