orbit/dev/research-builds.test.mjs

91 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.

/**
* Research & build data-layer test (dev tool, run with Node — no browser):
*
* node dev/research-builds.test.mjs
*
* The player's progression loop has two halves — research (time-based,
* one project at a time) and building (credits + minerals) — and the
* command deck that will host them. The RULES are not implemented yet;
* this test pins the data contract the future code will lean on:
* - the three config files are registered in data/manifest.json;
* - the rule knobs exist (timeUnit, maxConcurrent = 1, resources);
* - projects/builds are typed, empty maps (no content yet);
* - the `_template` entries document every required field;
* - the deck has exactly six slots in the right order.
*/
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import fs from 'node:fs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const dataDir = join(__dirname, '../data');
const read = (name) => JSON.parse(fs.readFileSync(join(dataDir, name), 'utf8'));
const manifest = read('manifest.json');
const research = read('research.json');
const builds = read('builds.json');
const actionbar = read('actionbar.json');
let failures = 0;
const check = (label, cond) => {
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
if (!cond) failures++;
};
// ----------------------------------------------------------------------
// 1. Manifest registration
// ----------------------------------------------------------------------
for (const f of ['research.json', 'builds.json', 'actionbar.json']) {
check(`manifest registers ${f}`, manifest.files.includes(f));
}
// ----------------------------------------------------------------------
// 2. Research — time-based, one at a time
// ----------------------------------------------------------------------
check('research: player researches one thing at a time (maxConcurrent === 1)', research.maxConcurrent === 1);
check('research: timeUnit is a named unit', typeof research.timeUnit === 'string' && research.timeUnit.length > 0);
check('research: projects is a map', !!research.projects && typeof research.projects === 'object' && !Array.isArray(research.projects));
check('research: no projects yet (empty map)', Object.keys(research.projects ?? {}).filter((k) => !k.startsWith('_')).length === 0);
const rt = research._template ?? {};
for (const k of ['label', 'description', 'duration', 'requires', 'unlocks', 'effects', 'theme']) {
check(`research._template documents "${k}"`, k in rt);
}
check('research._template.duration is a positive number', typeof rt.duration === 'number' && rt.duration > 0);
check('research._template.requires is an array', Array.isArray(rt.requires));
check('research._template.unlocks names builds[] + research[]', Array.isArray(rt.unlocks?.builds) && Array.isArray(rt.unlocks?.research));
// ----------------------------------------------------------------------
// 3. Builds — credits + minerals
// ----------------------------------------------------------------------
check('builds: credits resource defined', typeof builds.resources?.credits?.label === 'string');
check('builds: minerals resource defined', typeof builds.resources?.minerals?.label === 'string');
check('builds: builds is a map', !!builds.builds && typeof builds.builds === 'object' && !Array.isArray(builds.builds));
check('builds: no builds yet (empty map)', Object.keys(builds.builds ?? {}).filter((k) => !k.startsWith('_')).length === 0);
const bt = builds._template ?? {};
for (const k of ['label', 'description', 'category', 'cost', 'requires', 'repeatable', 'effects', 'theme']) {
check(`builds._template documents "${k}"`, k in bt);
}
check('builds._template.category is a known kind', ['ship', 'planet', 'station', 'general'].includes(bt.category));
check('builds._template.cost pays in credits + minerals', typeof bt.cost?.credits === 'number' && typeof bt.cost?.minerals === 'number');
check('builds._template.repeatable is a boolean', typeof bt.repeatable === 'boolean');
check('builds._template.requires is an array', Array.isArray(bt.requires));
// ----------------------------------------------------------------------
// 4. Command deck — six evenly spaced slots, right order
// ----------------------------------------------------------------------
const slots = actionbar.buttons ?? [];
check('actionbar: exactly six slots', slots.length === 6);
check('actionbar: slot ids in order (Research, Build, Ship, ·, ·, Menu)', JSON.stringify(slots.map((s) => s.id)) === JSON.stringify(['research', 'build', 'ship', null, null, 'menu']));
check('actionbar: labels (Research / Build / Ship / · / · / Menu)', JSON.stringify(slots.map((s) => s.label)) === JSON.stringify(['Research', 'Build', 'Ship', null, null, 'Menu']));
const hex = /^#[0-9a-fA-F]{6}$/;
check('actionbar: live slots carry hex accents', slots.filter((s) => s.id).every((s) => hex.test(s.accent ?? '')));
check('actionbar: reserved slots stay null', slots.filter((s) => s.id === null).every((s) => s.label === null));
check('actionbar: CRT scanlines configured (pitch + alpha)', typeof actionbar.scanline?.pitch === 'number' && typeof actionbar.scanline?.alpha === 'number');
check('actionbar: RGB pull-apart configured (offsets + alphas)', typeof actionbar.animation?.rgb?.idleOffset === 'number' && typeof actionbar.animation?.rgb?.burstOffset === 'number' && typeof actionbar.animation?.rgb?.idleAlpha === 'number' && typeof actionbar.animation?.rgb?.burstAlpha === 'number');
check('actionbar: sane geometry (height 40200 px)', typeof actionbar.height === 'number' && actionbar.height > 40 && actionbar.height < 200);
console.log(failures === 0 ? '\nall checks passed ✔' : `\n${failures} check(s) FAILED ✘`);
process.exit(failures === 0 ? 0 : 1);