133 lines
5.6 KiB
JavaScript
133 lines
5.6 KiB
JavaScript
/**
|
||
* 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(' ')}`);
|
||
}
|