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