449 lines
21 KiB
JavaScript
449 lines
21 KiB
JavaScript
/**
|
|
* World preview test (dev tool, run with Node — no browser needed):
|
|
*
|
|
* node dev/type-preview.test.mjs
|
|
*
|
|
* ?type=<spec> drops the player next to a landable world/station to
|
|
* judge its clips + surface loop (js/galaxy/TypeSystems.js):
|
|
*
|
|
* - parseTypeSpec: the spec syntax — <class>[-NN] planet faces (NN a
|
|
* 1-based ordinal into the class's data/planets.json → frames pool)
|
|
* and ss[-NN] station variants (data/stations.json → variants),
|
|
* case-insensitive, with out-of-range/unknown values → null;
|
|
* - pickPreviewSystem on the REAL seeded galaxy: every (class, face)
|
|
* and every station variant resolves to a system — exact (class,
|
|
* face) wins whenever the galaxy wears the face (cross-checked
|
|
* against the galaxy's actual content, lazy === eager), and the
|
|
* reskin fallback only fires for faces the galaxy wears nowhere
|
|
* (the home world — the galaxy's only terran — for terran faces it
|
|
* doesn't wear); an exact match ALWAYS beats a reskin, whatever the
|
|
* richness; the systemType filter (?fx= + ?type=) restricts the
|
|
* candidates; determinism (same call ⇒ same pick);
|
|
* - the RANKING on a crafted galaxy (same roll inputs, controlled
|
|
* frames/gates): richest-first within a tier, roster-order tiebreak,
|
|
* exact beats reskin.
|
|
*/
|
|
|
|
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 { rollSystemComposition, settlementDensity } = await import(
|
|
pathToFileURL(join(__dirname, '../js/galaxy/SystemGenerator.js')).href
|
|
);
|
|
const { parseTypeSpec, pickPreviewSystem } = await import(
|
|
pathToFileURL(join(__dirname, '../js/galaxy/TypeSystems.js')).href
|
|
);
|
|
|
|
let failures = 0;
|
|
const check = (label, cond, extra = '') => {
|
|
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}${cond ? '' : ' — ' + extra}`);
|
|
if (!cond) failures++;
|
|
};
|
|
|
|
const POOL = (k) => {
|
|
const p = config.get(k);
|
|
return Array.isArray(p) ? p : [];
|
|
};
|
|
const planetClasses = Object.keys(config.get('planets.frames', {}));
|
|
const stationVariants = POOL('stations.variants');
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 1. parseTypeSpec — the spec syntax
|
|
// ----------------------------------------------------------------------
|
|
check('parse: terran-01 = the FIRST terran face (pool [0,1,2] → frame 0)', (() => {
|
|
const s = parseTypeSpec('terran-01');
|
|
return s?.kind === 'planet' && s.cls === 'terran' && s.frame === POOL('planets.frames.terran')[0];
|
|
})());
|
|
check('parse: ice-03 = the THIRD ice face (pool [9,10,11] → frame 11)', (() => {
|
|
const pool = POOL('planets.frames.ice');
|
|
const s = parseTypeSpec('ice-03');
|
|
return pool.length >= 3 && s?.kind === 'planet' && s.cls === 'ice' && s.frame === pool[2];
|
|
})());
|
|
check('parse: ss-02 = the SECOND station variant (pool → variants[1])', (() => {
|
|
const s = parseTypeSpec('ss-02');
|
|
return stationVariants.length >= 2 && s?.kind === 'station' && s.frame === stationVariants[1];
|
|
})());
|
|
check('parse: bare class / bare ss = any face', (() => {
|
|
const a = parseTypeSpec('rocky');
|
|
const b = parseTypeSpec('ss');
|
|
return a?.kind === 'planet' && a.cls === 'rocky' && a.frame === null &&
|
|
b?.kind === 'station' && b.frame === null;
|
|
})());
|
|
check('parse: case-insensitive (ICE-03, SS-02, Terran)', (() => {
|
|
const a = parseTypeSpec('ICE-03');
|
|
const b = parseTypeSpec('SS-02');
|
|
const c = parseTypeSpec('Terran');
|
|
return a?.cls === 'ice' && a.frame === POOL('planets.frames.ice')[2] &&
|
|
b?.kind === 'station' && b.frame === stationVariants[1] &&
|
|
c?.cls === 'terran' && c.frame === null;
|
|
})());
|
|
check('parse: station aliases (station / spacestation) map like ss', (() => {
|
|
const a = parseTypeSpec('station-01');
|
|
const b = parseTypeSpec('spacestation');
|
|
return a?.kind === 'station' && a.frame === stationVariants[0] &&
|
|
b?.kind === 'station' && b.frame === null;
|
|
})());
|
|
check('parse: out-of-range / malformed / unknown → null', (() => {
|
|
return (
|
|
parseTypeSpec('terran-04') === null &&
|
|
parseTypeSpec('terran-0') === null &&
|
|
parseTypeSpec('terran-x') === null &&
|
|
parseTypeSpec('ss-9') === null &&
|
|
parseTypeSpec('jupiter') === null &&
|
|
parseTypeSpec('terran-') === null &&
|
|
parseTypeSpec('') === null &&
|
|
parseTypeSpec(null) === null
|
|
);
|
|
})());
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 2. pickPreviewSystem — the REAL seeded galaxy
|
|
// ----------------------------------------------------------------------
|
|
const SEED = 'type-preview-test';
|
|
const g = Galaxy.create(SEED);
|
|
const types = config.section('systems.types', {});
|
|
|
|
// A system's actual content, as the generator will produce it (lazy).
|
|
const contentOf = (id) => g.ensureContent(id);
|
|
|
|
// Verify a pick against the real content: the target world/station must
|
|
// EXIST with the spec's class/variant, and wear the spec's face unless
|
|
// the pick is a reskin (then the scene paints it — the CLASS must fit).
|
|
function verifyPick(spec, pick, why) {
|
|
if (!pick?.record) return `pick is null for ${why}`;
|
|
const c = contentOf(pick.record.id);
|
|
if (spec.kind === 'station') {
|
|
if (!pick.station) return `missing station flag on ${pick.record.id}`;
|
|
const s = c.settlements.find((x) => x.kind === 'deepSpaceStation');
|
|
if (!s) return `${pick.record.id} has no deepSpaceStation in content`;
|
|
if (!pick.reskin && spec.frame != null && s.stationFrame !== spec.frame) {
|
|
return `${pick.record.id} station wears ${s.stationFrame}, spec wants ${spec.frame}`;
|
|
}
|
|
return null;
|
|
}
|
|
if (typeof pick.ordinal !== 'number') return `missing ordinal on ${pick.record.id}`;
|
|
if (pick.ordinal === 0) {
|
|
if (pick.record.id !== g.homeSystemId) return `ordinal 0 (home) but ${pick.record.id} ≠ home ${g.homeSystemId}`;
|
|
if (spec.cls !== config.get('planets.homePlanet', 'terran')) return `home world is not a ${spec.cls}`;
|
|
if (!pick.reskin && spec.frame != null && c.homeFrame !== spec.frame) {
|
|
return `home world wears ${c.homeFrame}, spec wants ${spec.frame}`;
|
|
}
|
|
return null;
|
|
}
|
|
const p = c.planets[pick.ordinal - 1];
|
|
if (!p) return `${pick.record.id} has no planet #${pick.ordinal}`;
|
|
if (p.class !== spec.cls) return `planet #${pick.ordinal} is ${p.class}, spec wants ${spec.cls}`;
|
|
if (!pick.reskin && spec.frame != null && p.frame !== spec.frame) {
|
|
return `planet #${pick.ordinal} wears ${p.frame}, spec wants ${spec.frame}`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// 2a. Every (class, face) and every station variant resolves, and the
|
|
// pick is CORRECT against the content (exact ⇒ the world wears the face).
|
|
{
|
|
let ok = true;
|
|
let why = '';
|
|
const problems = [];
|
|
for (const cls of planetClasses) {
|
|
const pool = POOL(`planets.frames.${cls}`);
|
|
for (let i = 0; i < pool.length; i++) {
|
|
const spec = { kind: 'planet', cls, frame: pool[i] };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
const bad = verifyPick(spec, pick, `${cls} face ${i + 1}`);
|
|
if (bad) { ok = false; problems.push(bad); }
|
|
}
|
|
// bare class
|
|
const spec = { kind: 'planet', cls, frame: null };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
const bad = verifyPick(spec, pick, `${cls} (any face)`);
|
|
if (bad) { ok = false; problems.push(bad); }
|
|
}
|
|
for (const frame of stationVariants) {
|
|
const spec = { kind: 'station', frame };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
const bad = verifyPick(spec, pick, `ss variant ${frame}`);
|
|
if (bad) { ok = false; problems.push(bad); }
|
|
}
|
|
const spec = { kind: 'station', frame: null };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
const bad = verifyPick(spec, pick, 'ss (any variant)');
|
|
if (bad) { ok = false; problems.push(bad); }
|
|
check('every (class, face) + every station variant resolves and matches the content', ok, problems.slice(0, 3).join('; '));
|
|
}
|
|
|
|
// 2b. EXACT beats RESKIN whenever the galaxy wears the face somewhere.
|
|
{
|
|
let ok = true;
|
|
let why = '';
|
|
const problems = [];
|
|
for (const cls of planetClasses) {
|
|
const pool = POOL(`planets.frames.${cls}`);
|
|
for (const frame of pool) {
|
|
const wornSomewhere = g.records.some((rec) => {
|
|
const c = contentOf(rec.id);
|
|
if (c.planets.some((p) => p.class === cls && p.frame === frame)) return true;
|
|
if (rec.id === g.homeSystemId && cls === config.get('planets.homePlanet') && c.homeFrame === frame) return true;
|
|
return false;
|
|
});
|
|
const spec = { kind: 'planet', cls, frame };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
if (wornSomewhere) {
|
|
if (!pick || pick.reskin) { ok = false; problems.push(`${cls}/${frame} worn but pick is ${pick ? 'a reskin' : 'null'}`); }
|
|
} else if (pick && !pick.reskin) {
|
|
// an "exact" pick for a face nobody wears would be a lie
|
|
ok = false;
|
|
problems.push(`${cls}/${frame} unworn but pick claims exact`);
|
|
}
|
|
}
|
|
}
|
|
check('an exact match beats a reskin whenever the galaxy wears the face', ok, problems.slice(0, 3).join('; '));
|
|
}
|
|
|
|
// 2c. The RESKIN fallback exists for terran faces the home world doesn't
|
|
// wear (the home world is the galaxy's only terran).
|
|
{
|
|
const homeFrame = g.ensureContent(g.homeSystemId).homeFrame;
|
|
const pool = POOL('planets.frames.terran');
|
|
let reskins = 0;
|
|
let ok = true;
|
|
let why = '';
|
|
for (const frame of pool) {
|
|
const spec = { kind: 'planet', cls: 'terran', frame };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
if (!pick) { ok = false; why = `terran face ${frame}: null`; break; }
|
|
if (frame === homeFrame) {
|
|
if (pick.reskin) { ok = false; why = `terran face ${frame} (the home world's) reskinned`; break; }
|
|
if (pick.ordinal !== 0 || pick.record.id !== g.homeSystemId) {
|
|
// a genuine terran world elsewhere would be fine, but there is
|
|
// no other terran in the galaxy — the home world is THE terran
|
|
ok = false; why = `terran face ${frame}: expected the home world, got ${pick.record.id}#${pick.ordinal}`; break;
|
|
}
|
|
} else {
|
|
if (!pick.reskin) { ok = false; why = `terran face ${frame}: unworn face not reskinned`; break; }
|
|
reskins++;
|
|
}
|
|
}
|
|
check('terran faces the home world lacks are reskinned onto it (the only terran)', ok && reskins === pool.length - 1, why || `${reskins} reskinned of ${pool.length - 1}`);
|
|
}
|
|
|
|
// 2d. Determinism — the same spec always picks the same system.
|
|
{
|
|
const specs = [
|
|
{ kind: 'planet', cls: 'ice', frame: POOL('planets.frames.ice')[1] },
|
|
{ kind: 'planet', cls: 'gas', frame: null },
|
|
{ kind: 'station', frame: stationVariants[0] },
|
|
];
|
|
let ok = true;
|
|
for (const spec of specs) {
|
|
const a = pickPreviewSystem(g, spec);
|
|
const b = pickPreviewSystem(g, spec);
|
|
if (!a || a.record.id !== b?.record.id || a.ordinal !== b?.ordinal || a.station !== b?.station) { ok = false; break; }
|
|
}
|
|
check('deterministic (same spec ⇒ same pick, twice)', ok);
|
|
}
|
|
|
|
// 2e. The ?fx= FILTER — ?type= inside a ?fx= system type.
|
|
{
|
|
// Find a class/face that exists in SOME system type but NOT in
|
|
// another (both types present in the galaxy), then the filter must
|
|
// steer the pick (or give up) accordingly.
|
|
let ok = true;
|
|
let why = '';
|
|
const typeIds = Object.keys(types);
|
|
const found = [];
|
|
for (const cls of planetClasses) {
|
|
for (const frame of POOL(`planets.frames.${cls}`)) {
|
|
const byType = new Set();
|
|
for (const rec of g.records) {
|
|
const c = contentOf(rec.id);
|
|
const has = c.planets.some((p) => p.class === cls && p.frame === frame) ||
|
|
(rec.id === g.homeSystemId && cls === config.get('planets.homePlanet') && c.homeFrame === frame);
|
|
if (has) byType.add(rec.type);
|
|
}
|
|
if (byType.size >= 2) found.push({ cls, frame, byType });
|
|
}
|
|
}
|
|
if (found.length === 0) {
|
|
ok = false; why = 'no face found in ≥2 system types (test galaxy too small?)';
|
|
} else {
|
|
for (const { cls, frame, byType } of found.slice(0, 4)) {
|
|
const spec = { kind: 'planet', cls, frame };
|
|
const inside = [...byType].sort().join(',');
|
|
const outside = typeIds.filter((t) => !byType.has(t));
|
|
const pIn = pickPreviewSystem(g, spec, [...byType][0]);
|
|
const pOut = outside.length > 0 ? pickPreviewSystem(g, spec, outside[0]) : null;
|
|
if (!pIn || pIn.record.type !== [...byType][0]) { ok = false; why = `${cls}/${frame}: in-pick ${pIn?.record?.type}`; break; }
|
|
// OUTSIDE the wearing types: the face is unworn there, so a pick is
|
|
// only legal as a RESKIN (the class present, the face painted on);
|
|
// null when the class itself is absent from that type's systems.
|
|
if (pOut !== null) {
|
|
const c = contentOf(pOut.record.id);
|
|
const clsPresent = pOut.ordinal === 0
|
|
? pOut.record.id === g.homeSystemId
|
|
: c.planets[pOut.ordinal - 1]?.class === cls;
|
|
if (pOut.reskin !== true || !clsPresent) { ok = false; why = `${cls}/${frame}: out-pick ${pOut.record.id} (reskin=${pOut.reskin})`; break; }
|
|
}
|
|
}
|
|
if (ok) console.log(` (filter cases: ${found.slice(0, 4).map((f) => `${f.cls}/${f.frame}`).join(', ')})`);
|
|
}
|
|
check('systemType filter: exact inside the wearing types; reskin-or-null outside', ok, why);
|
|
}
|
|
|
|
// 2f. Stations: a variant worn somewhere ⇒ exact (no reskin).
|
|
{
|
|
const worn = new Set();
|
|
for (const rec of g.records) {
|
|
const c = contentOf(rec.id);
|
|
const s = c.settlements.find((x) => x.kind === 'deepSpaceStation');
|
|
if (s?.stationFrame != null) worn.add(s.stationFrame);
|
|
}
|
|
let ok = true;
|
|
let why = '';
|
|
for (const frame of stationVariants) {
|
|
const spec = { kind: 'station', frame };
|
|
const pick = pickPreviewSystem(g, spec);
|
|
if (!pick) { ok = false; why = `ss variant ${frame}: null`; continue; }
|
|
if (worn.has(frame) && pick.reskin) { ok = false; why = `ss variant ${frame} worn but reskinned`; }
|
|
if (!worn.has(frame) && !pick.reskin) { ok = false; why = `ss variant ${frame} unworn but exact`; }
|
|
}
|
|
check('station variants: exact where worn, reskin where unworn', ok && worn.size > 0, why || `${[...worn].join(',')} worn`);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 3. The RANKING — crafted galaxy (controlled frames + gates)
|
|
// ----------------------------------------------------------------------
|
|
// Reuse REAL record ids so rollSystemComposition (seed, id, type, d)
|
|
// agrees with the real galaxy's rolls — only the stamped frames and the
|
|
// gate counts are crafted.
|
|
{
|
|
// Two non-home systems whose roll gives an ICE planet (real rolls).
|
|
const iceRecs = g.records.filter((rec) => {
|
|
if (rec.id === g.homeSystemId) return false;
|
|
const c = contentOf(rec.id);
|
|
return c.planets.some((p) => p.class === 'ice');
|
|
});
|
|
const stRecs = g.records.filter((rec) => {
|
|
if (rec.id === g.homeSystemId) return false;
|
|
return contentOf(rec.id).settlements.some((x) => x.kind === 'deepSpaceStation');
|
|
});
|
|
const iceA = iceRecs[0];
|
|
const iceB = iceRecs[1];
|
|
const ok = iceA && iceB;
|
|
check('ranking fixture: two ice-world systems exist in the test galaxy', !!ok);
|
|
if (ok) {
|
|
const iceFrame = POOL('planets.frames.ice')[1]; // the 2nd ice face
|
|
const iceOrdinal = (rec) => contentOf(rec.id).planets.findIndex((p) => p.class === 'ice') + 1;
|
|
const mkGalaxy = (gateCounts) => ({
|
|
seed: g.seed,
|
|
records: [iceA, iceB],
|
|
homeSystemId: g.homeSystemId, // neither fixture record is home
|
|
homeWorldFrame: g.homeWorldFrame,
|
|
params: g.params,
|
|
planetFrames: new Map(),
|
|
stationFrames: new Map(),
|
|
jumpGatesFor: (id) => Array.from({ length: gateCounts[id] ?? 0 }, (_, i) => ({ id: `${id}-gate-${i}` })),
|
|
});
|
|
|
|
// 3a. EXACT beats a RICHER reskin: A is richer (3 gates) but wears a
|
|
// different ice face; B (1 gate) wears the requested face.
|
|
{
|
|
const other = POOL('planets.frames.ice').find((f) => f !== iceFrame) ?? POOL('planets.frames.ice')[0];
|
|
const gf = mkGalaxy({ [iceA.id]: 3, [iceB.id]: 1 });
|
|
gf.planetFrames.set(iceA.id, new Array(contentOf(iceA.id).planets.length).fill(other));
|
|
gf.planetFrames.set(iceB.id, (() => { const l = contentOf(iceB.id).planets.length; const a = new Array(l).fill(other); a[iceOrdinal(iceB) - 1] = iceFrame; return a; })());
|
|
const pick = pickPreviewSystem(gf, { kind: 'planet', cls: 'ice', frame: iceFrame });
|
|
check('ranking: an exact face beats a richer reskin (B wins despite A\u2019s 3 gates)', pick?.record?.id === iceB.id && pick.reskin === false, `got ${pick?.record?.id} reskin=${pick?.reskin}`);
|
|
}
|
|
// 3b. Within the RESKIN tier the richer system wins (A: 3 gates).
|
|
{
|
|
const gf = mkGalaxy({ [iceA.id]: 3, [iceB.id]: 1 });
|
|
const other = POOL('planets.frames.ice').find((f) => f !== iceFrame) ?? POOL('planets.frames.ice')[0];
|
|
gf.planetFrames.set(iceA.id, new Array(contentOf(iceA.id).planets.length).fill(other));
|
|
gf.planetFrames.set(iceB.id, new Array(contentOf(iceB.id).planets.length).fill(other));
|
|
const pick = pickPreviewSystem(gf, { kind: 'planet', cls: 'ice', frame: iceFrame });
|
|
check('ranking: reskin tier ranks richest-first (A wins)', pick?.record?.id === iceA.id && pick.reskin === true, `got ${pick?.record?.id} reskin=${pick?.reskin}`);
|
|
}
|
|
// 3c. Within the EXACT tier the richer system wins (flip the frames).
|
|
{
|
|
const gf = mkGalaxy({ [iceA.id]: 3, [iceB.id]: 1 });
|
|
const other = POOL('planets.frames.ice').find((f) => f !== iceFrame) ?? POOL('planets.frames.ice')[0];
|
|
gf.planetFrames.set(iceA.id, (() => { const l = contentOf(iceA.id).planets.length; const a = new Array(l).fill(other); a[iceOrdinal(iceA) - 1] = iceFrame; return a; })());
|
|
gf.planetFrames.set(iceB.id, new Array(contentOf(iceB.id).planets.length).fill(other));
|
|
const pick = pickPreviewSystem(gf, { kind: 'planet', cls: 'ice', frame: iceFrame });
|
|
check('ranking: exact tier ranks richest-first (A wins)', pick?.record?.id === iceA.id && pick.reskin === false, `got ${pick?.record?.id} reskin=${pick?.reskin}`);
|
|
}
|
|
// 3d. No ice world at all in the (filtered) candidates ⇒ null.
|
|
{
|
|
const noIce = iceRecs.filter((r) => r.id !== iceA.id && r.id !== iceB.id && !contentOf(r.id).planets.some((p) => p.class === 'ice'));
|
|
const victim = noIce[0];
|
|
if (victim) {
|
|
const gf = mkGalaxy({ [victim.id]: 5 });
|
|
const pick = pickPreviewSystem(gf, { kind: 'planet', cls: 'ice', frame: iceFrame });
|
|
check('ranking: a candidate without the class (and no home) ⇒ null', pick === null, `got ${JSON.stringify(pick)}`);
|
|
} else {
|
|
console.log(' (3d skipped — no ice-free fixture system)');
|
|
}
|
|
}
|
|
// 3e. Station ranking: exact beats a richer reskin.
|
|
{
|
|
const variant = stationVariants[1];
|
|
if (stRecs.length >= 2) {
|
|
const [sA, sB] = stRecs;
|
|
const gsf = {
|
|
seed: g.seed,
|
|
records: [sA, sB],
|
|
homeSystemId: g.homeSystemId,
|
|
homeWorldFrame: g.homeWorldFrame,
|
|
params: g.params,
|
|
planetFrames: new Map(),
|
|
stationFrames: new Map(),
|
|
jumpGatesFor: (id) => Array.from({ length: id === sA.id ? 3 : 1 }, (_, i) => ({ id: `${id}-gate-${i}` })),
|
|
};
|
|
const other = stationVariants.find((f) => f !== variant) ?? stationVariants[0];
|
|
gsf.stationFrames.set(sA.id, other);
|
|
gsf.stationFrames.set(sB.id, variant);
|
|
const pick = pickPreviewSystem(gsf, { kind: 'station', frame: variant });
|
|
check('ranking (station): exact beats a richer reskin (B wins)', pick?.record?.id === sB.id && pick.station === true && pick.reskin === false, `got ${pick?.record?.id} reskin=${pick?.reskin}`);
|
|
} else {
|
|
console.log(' (3e skipped — need ≥2 station systems in the test galaxy)');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// 4. Bad inputs
|
|
// ----------------------------------------------------------------------
|
|
check('picker: null/empty/foreign specs ⇒ null', (() => {
|
|
return (
|
|
pickPreviewSystem(g, null) === null &&
|
|
pickPreviewSystem(g, {}) === null &&
|
|
pickPreviewSystem(g, { kind: 'planet' }) === null &&
|
|
pickPreviewSystem(g, { kind: 'comet', frame: 0 }) === null &&
|
|
pickPreviewSystem({ records: [] }, { kind: 'station', frame: 0 }) === null
|
|
);
|
|
})());
|
|
|
|
if (failures > 0) {
|
|
console.error(`\n✘ type-preview: ${failures} check(s) FAILED`);
|
|
process.exit(1);
|
|
}
|
|
console.log('\n✔ type-preview: all checks passed');
|