orbit/dev/nav-reach-probe.mjs

133 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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(' ')}`);
}