/** * One-off audit: do generated galaxies obey the two tether rules? * Rule 1: every jump gate within 1 tether region (level1Radius) of a * planet or space station — unless the system is a dead end * (a barren system: no planets/stations, single gate). * Rule 2: every planet & space station within 2 tether lengths * (2 × level1Radius) of at least one other planet/station — * unless the system holds exactly one such object. */ 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 T1 = config.get('tether.level1Radius', 5120); const T2 = T1 * 2; const EPS = 1; for (const seed of ['audit-a', 'audit-b', 'audit-c']) { const g = Galaxy.create(seed); g.generateAll(); let r1Bad = 0, r1Checked = 0; let r2Bad = 0, r2Checked = 0; const r1Examples = []; const r2ByN = new Map(); // N → [checked, bad] for (const rec of g.records) { const c = g.contentOf(rec.id); const isHome = rec.id === g.homeSystemId; const objects = [...c.planets.map((p) => ({ x: p.x, y: p.y }))] .concat(c.settlements.filter((s) => s.anchor?.type === 'space').map((s) => ({ x: s.x, y: s.y }))); if (isHome) objects.push({ x: 0, y: 0 }); // the home world const isBarren = objects.length === 0; // Rule 1 for (const j of c.jumps) { r1Checked++; if (isBarren) continue; let best = Infinity; for (const o of objects) best = Math.min(best, Math.hypot(j.x - o.x, j.y - o.y)); if (best > T1 + EPS) { r1Bad++; if (r1Examples.length < 3) r1Examples.push(`${rec.id} gate→${j.to} d=${Math.round(best)}`); } } // Rule 2 if (objects.length >= 2) { for (const o of objects) { r2Checked++; let best = Infinity; for (const q of objects) { if (q === o) continue; best = Math.min(best, Math.hypot(o.x - q.x, o.y - q.y)); } const [cc, bb] = r2ByN.get(objects.length) ?? [0, 0]; r2ByN.set(objects.length, [cc + 1, bb + (best > T2 + EPS ? 1 : 0)]); if (best > T2 + EPS) r2Bad++; } } } console.log(`\nseed="${seed}" systems=${g.records.length} (tether L1=${T1}px, 2×L1=${T2}px)`); console.log(` Rule 1 (gates ≤ ${T1}px from a planet/station, barren exempt): ${r1Checked - r1Bad}/${r1Checked} OK${r1Bad ? ' — BAD: ' + r1Examples.join(', ') : ''}`); const byN = [...r2ByN.entries()].map(([n, [cc, bb]]) => `${n}-obj ${cc - bb}/${cc}`).join(', '); console.log(` Rule 2 (every object ≤ ${T2}px from another, N=1 exempt): ${r2Checked - r2Bad}/${r2Checked} OK — ${byN}`); }