/** * ROUTE test (dev tool, run with Node — no browser): * * node dev/route.test.mjs * * Pins the pure course-planning behind SET DESTINATION (js/galaxy/Route.js, * driven by GameScene.setDestination / routeNextGate / _routeForJump): * - the jump network is a spanning tree (data/gates.json → shortcuts:false), * so there is EXACTLY ONE route between any two systems — planRoute must * return a valid, connected path that honours the gate edges; * - from === to is already-there (path [id], next null, 0 hops); * - the path starts at `from`, ends at `to`, and every step is a real gate * link (both directions exist — the tree is bidirected); * - `next` is the first hop after `from` (what the compass points at) and is * always a gate neighbour of `from`; * - DETERMINISM: same seed + endpoints ⇒ same route; and the tree is * symmetric, so the from→to path reversed equals the to→from path; * - REACHABILITY: every ordered pair resolves (the network is strongly * connected) — no null where a route must exist. */ 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 { planRoute, nextSystem } = await import(pathToFileURL(join(__dirname, '../js/galaxy/Route.js')).href); let failures = 0; const check = (label, cond, extra = '') => { console.log(`${cond ? '✔' : '✘ FAIL'} ${label}${cond ? '' : ' — ' + extra}`); if (!cond) failures++; }; const SEED = 'route-test-seed'; const g = Galaxy.create(SEED); const ids = g.records.map((r) => r.id); const HOME = g.currentSystemId; const adj = (id) => new Set(g.jumpNetwork.gates.get(id) ?? []); console.log(`\ngalaxy: ${g.records.length} systems, home ${HOME}\n`); // ---------------------------------------------------------------------- // 1. Already there — from === to // ---------------------------------------------------------------------- { const r = planRoute(g, HOME, HOME); check('from===to → path [id]', Array.isArray(r?.path) && r.path.length === 1 && r.path[0] === HOME); check('from===to → next null, 0 hops', r?.next === null && r?.hops === 0); check('from===to → distance 0', r?.distance === 0); } // ---------------------------------------------------------------------- // 2. Path validity — endpoints, connectivity, gate edges // ---------------------------------------------------------------------- { // Pick a real destination a couple of hops out (not home, not a direct // neighbour, so the path is non-trivial). const homeAdj = adj(HOME); let dest = null; for (const id of ids) { if (id === HOME || homeAdj.has(id)) { dest = id; break; } } const r = planRoute(g, HOME, dest); check('resolves a route home → a non-neighbour system', !!r, `dest ${dest}`); check('path starts at from', r?.path?.[0] === HOME); check('path ends at to', r?.path?.[r.path.length - 1] === dest); check('hops = path.length - 1', r?.hops === r.path.length - 1); // Every step is a real gate link (tree edges run both ways). let connected = true; let why = ''; for (let i = 0; i + 1 < r.path.length; i++) { const a = r.path[i]; const b = r.path[i + 1]; if (!adj(a).has(b) || !adj(b).has(a)) { connected = false; why = `${a} !~ ${b}`; break; } } check('every step is a bidirected gate link', connected, why); // `next` is the first hop and a gate neighbour of from. check('next is path[1]', r?.next === r.path[1]); check('next is a gate neighbour of from', adj(HOME).has(r?.next)); check('distance is finite + non-negative', Number.isFinite(r?.distance) && r.distance >= 0); } // ---------------------------------------------------------------------- // 3. Determinism + tree symmetry (reverse path) // ---------------------------------------------------------------------- { const a = ids[0]; const b = ids[ids.length - 1]; const r1 = planRoute(g, a, b); const r2 = planRoute(g, a, b); check('deterministic: same seed ⇒ same route', JSON.stringify(r1) === JSON.stringify(r2)); const rb = planRoute(g, b, a); const reversed = [...(r1?.path ?? [])].reverse(); check( 'tree symmetry: a→b reversed === b→a', JSON.stringify(reversed) === JSON.stringify(rb?.path), `${JSON.stringify(r1?.path)} vs ${JSON.stringify(rb?.path)}`, ); } // ---------------------------------------------------------------------- // 4. Reachability — every ordered pair resolves (strongly connected) // ---------------------------------------------------------------------- { let unresolved = 0; let badNext = 0; // Sample a slice of ordered pairs (the full N² is large; cover a wide mix // including home and several far systems). const sample = [HOME, ...ids.filter((id) => id !== HOME).slice(0, 24)]; for (const from of sample) { for (const to of sample) { if (from === to) continue; const r = planRoute(g, from, to); if (!r || r.path[0] !== from || r.path[r.path.length - 1] !== to) { unresolved++; continue; } // next must be a gate neighbour of from (the compass's target gate). if (r.next !== null && !adj(from).has(r.next)) badNext++; } } check('every sampled ordered pair resolves (strong connectivity)', unresolved === 0, `${unresolved} unresolved`); check('every route\'s next is a gate neighbour of from', badNext === 0, `${badNext} bad`); } // ---------------------------------------------------------------------- // 5. nextSystem helper // ---------------------------------------------------------------------- { const dest = ids.find((id) => id !== HOME) ?? HOME; const r = planRoute(g, HOME, dest); check('nextSystem === planRoute.next', nextSystem(g, HOME, dest) === (r?.next ?? null)); check('nextSystem(from,from) === null', nextSystem(g, HOME, HOME) === null); } // ---------------------------------------------------------------------- // 6. Guard rails — unknown endpoints // ---------------------------------------------------------------------- { check('unknown from → null', planRoute(g, 'NOPE', HOME) === null); check('unknown to → null', planRoute(g, HOME, 'NOPE') === null); check('non-string → null', planRoute(g, null, HOME) === null); // No galaxy + a real destination (from ≠ to) → no route to compute. // (from === to is the "already there" short-circuit — valid without a // graph, so it is asserted in section 1, not here.) check('no galaxy + distinct endpoints → null', planRoute(null, HOME, 'S000001') === null); check('empty galaxy + distinct endpoints → null', planRoute({ byId: new Map(), jumpNetwork: { gates: new Map() } }, 'A', 'B') === null); } console.log(`\n${failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'}\n`); process.exit(failures === 0 ? 0 : 1);