/** * dev/nav-reach-probe.mjs — soft-lock probe (path-aware): which systems' * NAV charts can NEVER be completed under the game's actual progression? * * node dev/nav-reach-probe.mjs [seed ...] * * Model (mirrors GameScene + SystemCategory): * - To jump Y→X the player must have RESEARCHED Y's gate tech, so every * system jumped OUT of is researched; arrival at X from Y ⇒ Y (and * everything before it on the path, back to home) is researched. * - On entry, X's ACTIVE gates = the gates from X to any researched * system (those return gates were flipped by the neighbor's tech). * - The player's room to move = disc(star, 5120) ∪ disc(activeGate, 5120) * for each active gate. A NAV point is discoverable iff that union * comes within (radius + 540) of the point. * - A system is clearable iff SOME researched neighbor lets it chart. * Clear systems stay clear (they become researched and only add more * active gates for their neighbors). */ import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; import fs from 'node:fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); const manifest = JSON.parse(fs.readFileSync(join(root, 'data', 'manifest.json'), 'utf8')); const files = {}; for (const f of manifest.files) files[f.split('/').pop().replace('.json', '')] = JSON.parse(fs.readFileSync(join(root, 'data', f), 'utf8')); const { config } = await import(join(root, 'js', 'config', 'Config.js')); config.init(files); const { Galaxy } = await import(join(root, 'js', 'galaxy', 'Galaxy.js')); const { navPoints } = await import(join(root, 'js', 'research', 'SystemCategory.js')); const TETHER = config.get('tether.level1Radius', 5120); const DISC = config.get('game.discovery.distance', 540); const GATE_R = config.get('gates.size', 96); const STAR_R = config.get('planets.star.size', 1536) / 2; const homeTether = TETHER * Math.pow(config.get('tether.radiusGrowth', 2), Math.max(1, config.get('tether.homeLevel', 1)) - 1); function systemOf(galaxy, id) { const content = galaxy.ensureContent(id); const rec = galaxy.byId.get(id); const pts = navPoints(content).map((p) => { let pos = { x: 0, y: 0 }; let r = STAR_R; if (p.kind === 'planet') { const q = content.planets.find((x) => x.name === p.id); pos = q; r = (1024 * (config.get(`planets.classScale.${q.class}`, 1))) / 2; } else if (p.kind === 'gate') { pos = content.jumps.find((j) => j.id === p.id); r = GATE_R; } else if (p.kind === 'station') { pos = content.settlements.find((s) => s.id === p.id); r = 108; } return { id: p.id, kind: p.kind, x: pos.x, y: pos.y, r }; }); const gates = (content.jumps ?? []).filter((j) => typeof j.x === 'number'); return { id, name: rec.name, isHome: id === galaxy.homeSystemId, planets: content.planets.length, stations: (content.settlements ?? []).filter((s) => s.anchor?.type === 'space').length, gates, asteroids: (content.asteroids ?? []).length, pts, neighbors: gates.map((g) => g.to), }; } function chartable(sys, researched) { const active = sys.gates.filter((g) => researched.has(g.to)); const discs = [{ x: 0, y: 0, r: sys.isHome ? homeTether : TETHER }, ...active.map((g) => ({ x: g.x, y: g.y, r: TETHER }))]; const missing = sys.pts.filter((p) => { let best = Infinity; for (const c of discs) { const d = Math.hypot(p.x - c.x, p.y - c.y) - c.r; if (d < best) best = d; } return best > p.r + DISC; }); return { chartable: missing.length === 0, missing, activeCount: active.length }; } const seeds = process.argv.slice(2).length ? process.argv.slice(2) : ['probe1']; for (const seed of seeds) { const galaxy = Galaxy.create(seed, { systemCount: 200 }); const all = [...galaxy.byId.keys()].map((id) => systemOf(galaxy, id)); const byId = new Map(all.map((s) => [s.id, s])); // Progression: BFS of clearable systems, researching each as it clears. const researched = new Set([galaxy.homeSystemId]); let frontier = [galaxy.homeSystemId]; while (frontier.length) { const next = []; for (const id of frontier) { for (const s of all) { if (s.isHome || researched.has(s.id)) continue; if (!s.neighbors.includes(id)) continue; // entry neighbor const r2 = new Set(researched); if (!r2.has(id)) r2.add(id); if (chartable(s, r2).chartable) { researched.add(s.id); next.push(s.id); } } } frontier = next; } const stuck = all.filter((s) => !s.isHome && !researched.has(s.id)); console.log(`\n=== seed ${seed}: ${researched.size - 1}/${all.length - 1} non-home systems clearable, ${stuck.length} stuck ===`); for (const s of stuck.slice(0, 8)) { // best explanation: via which neighbor, which points remain missing let best = null; for (const n of new Set(s.neighbors)) { if (!byId.has(n)) continue; const r2 = new Set(researched); if (!r2.has(n)) r2.add(n); const c = chartable(s, r2); if (!best || c.missing.length < best.missing.length) best = { via: n, ...c }; } console.log( ` ${s.id}: planets=${s.planets} stations=${s.stations} gates=${s.gates.length} asteroids=${s.asteroids}` + ` → best entry via ${best?.via}: missing=[${best?.missing.map((m) => m.kind).join(',')}] activeGates=${best?.activeCount}` ); } // Also: systems matching "1 planet, 3 gates" (the user's shape) const one = all.filter((s) => !s.isHome && s.planets === 1 && s.stations === 0 && s.gates.length === 3); console.log(` systems with 1 planet + 3 gates: ${one.map((s) => `${s.id}(stuck=${!researched.has(s.id)})`).join(' ')}`); }