orbit/dev/faction-sweep.diag.mjs

88 lines
3.6 KiB
JavaScript

/**
* DIAG (throwaway): sweep many seeds — faction floor/cap invariants,
* determinism, capital placement.
*
* node dev/faction-sweep.diag.mjs [N]
*/
process.env.NODE_ENV = 'dev';
import { pathToFileURL, 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 { Rng } = await import(pathToFileURL(join(__dirname, '../js/utils/Rng.js')).href);
const { Galaxy } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Galaxy.js')).href);
const N = Number(process.argv[2] ?? 40);
const FACS = ['apex', 'mindweave', 'zenith'];
const minT = config.get('factions.balance.minTerritory', 5);
let bad = 0;
const small = [];
for (let i = 0; i < N; i++) {
const seed = Rng.randomSeedString ? Rng.randomSeedString(8) : 'SEED' + i;
const g = Galaxy.create(seed);
if (!g.factions) { console.log(` ${seed}: no factions (degenerate)`); continue; }
const counts = {};
for (const f of FACS) counts[f] = 0;
let owners = 0;
for (const r of g.records) {
if (!r.faction) continue;
owners++;
if (!FACS.includes(r.faction)) { console.log(` ${seed}: bad faction id ${r.faction}`); bad++; continue; }
counts[r.faction]++;
}
// Disjointness via factionTerritories.
const ft = g.factions.factionTerritories;
const seen = new Set();
let overlap = false;
for (const f of FACS) for (const id of ft[f] ?? []) {
if (seen.has(id)) overlap = true;
seen.add(id);
}
// Capitals inside their faction's systems.
let capOk = FACS.every((f) => {
const id = g.factions.capitals?.[f];
return id && g.byId.get(id)?.faction === f && g.byId.get(id)?.capital === f;
});
// Floor (best-effort bound: when total eligible >= 3*minT, all must reach it).
const branchOf = g.jumpNetwork.branchOf;
const depth = g.jumpNetwork.depth;
const branchMax = new Map();
for (const [id, b] of branchOf) branchMax.set(b, Math.max(branchMax.get(b) ?? 1, depth.get(id)));
const totalElig = [...branchOf.values()].reduce((acc, b) => acc, 0);
const eligCount = [...g.records]
.filter((r) => branchOf.has(r.id))
.filter((r) => { const d = depth.get(r.id) ?? 0; const b = branchOf.get(r.id); return d >= 2 && d <= (branchMax.get(b) ?? 1) - 1; })
.length;
const floorOk = eligCount >= 3 * minT ? FACS.every((f) => counts[f] >= minT) : true;
const capOk2 = Math.max(...FACS.map((f) => counts[f])) <= Math.max(g.factions.quota, minT);
if (overlap || !capOk || !floorOk || !capOk2) {
bad++;
console.log(` ${seed}: FAIL overlap=${overlap} capOk=${capOk} floor=${floorOk}(elig ${eligCount}) cap2=${capOk2} counts=${JSON.stringify(counts)} quota=${g.factions.quota}`);
}
if (Math.min(...FACS.map((f) => counts[f])) < 10) small.push(`${seed}:${JSON.stringify(counts)}`);
// Determinism
const g2 = Galaxy.create(seed);
if (JSON.stringify(g.factions) !== JSON.stringify(g2.factions) ||
g.records.some((r, i) => r.faction !== g2.records[i].faction || r.capital !== g2.records[i].capital)) {
bad++;
console.log(` ${seed}: DETERMINISM FAIL`);
}
}
console.log(`\n${N} seeds swept, ${bad} failures`);
console.log('smallest-faction galaxies (min count < 10):');
for (const s of small.slice(0, 10)) console.log(' ' + s);
process.exit(bad === 0 ? 0 : 1);