88 lines
3.6 KiB
JavaScript
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);
|