orbit/dev/decode.test.mjs

115 lines
4.9 KiB
JavaScript
Raw Permalink 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.

/**
* ScrambleDecode test (dev tool, run with Node — no browser):
*
* node dev/decode.test.mjs
*
* The shared "decode" effect (js/utils/Decode.js) — the type-out the
* menu uses on the Galaxy Seed and the game uses on the system dossier
* HUD. Checks the effect's contract:
* - '' before the start time, the exact target once finished;
* - the revealed prefix always matches the target's;
* - unrevealed slots are drawn from the decode alphabet;
* - the reveal never goes backwards over time;
* - display length is stable through the window;
* - reverse mode deconstructs (value → static → '') with the shrinking
* prefix never growing back;
* - decodeDur floors short lines, caps long ones, grows in between;
* - the default window is the menu's 620 ms.
*/
import { pathToFileURL } from 'node:url';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const { ScrambleDecode, DECODE_CHARS, DECODE_DURATION, decodeDur } = await import(
pathToFileURL(join(__dirname, '../js/utils/Decode.js')).href
);
let failures = 0;
const check = (label, cond) => {
console.log(`${cond ? '✔' : '✘ FAIL'} ${label}`);
if (!cond) failures++;
};
const VALUE = 'Kepler-9 · 17 planets';
const T0 = 1000;
const dur = DECODE_DURATION;
const dec = new ScrambleDecode(VALUE, T0, dur);
check('empty before the start time', dec.display(T0 - 1) === '');
check('exact target after the window', dec.display(T0 + dur + 1) === VALUE);
check('started()/finished() agree with display()', dec.started(T0 - 1) === false && dec.finished(T0 + dur) === true);
check('empty target stays empty', new ScrambleDecode('', T0, dur).display(T0 + dur / 2) === '');
// Through the window: length stable, the guaranteed-revealed prefix exact
// (per the documented curve), the rest from the decode alphabet, and the
// guaranteed reveal monotonically non-decreasing. (Slots that scramble to
// a coincidental match with the target are indistinguishable — and fine,
// so the checks are keyed on the curve, not on the measured common prefix.)
const forwardReveal = (t) =>
Math.floor(Math.max(0, Math.min(1, ((t - T0) / dur) * 1.15)) * VALUE.length);
let lenOk = true;
let prefixOk = true;
let charsOk = true;
let monotonic = true;
let prev = 0;
for (let t = T0; t <= T0 + dur; t += 16) {
const s = dec.display(t);
const r = forwardReveal(t);
if (s.length !== VALUE.length) lenOk = false;
for (let i = 0; i < r; i++) if (s[i] !== VALUE[i]) prefixOk = false; // guaranteed part is exact
for (let i = r; i < s.length; i++) {
if (s[i] !== VALUE[i] && DECODE_CHARS.indexOf(s[i]) === -1) charsOk = false;
}
if (r < prev) monotonic = false;
prev = r;
}
check('display length stays constant through the window', lenOk);
check('the guaranteed-revealed prefix is always exact', prefixOk);
check('unrevealed slots come from the decode alphabet', charsOk);
check('the guaranteed reveal never goes backwards', monotonic);
// decodeDur: floored, capped, growing in between.
check('decodeDur floors short lines', decodeDur(0) >= 420 && decodeDur(1) >= 420);
check('decodeDur caps long lines', decodeDur(500) <= 880);
check('decodeDur grows with length (in range)', decodeDur(4) < decodeDur(12) && decodeDur(12) < decodeDur(40));
// --- Reverse (deconstruct): the same reveal played backwards --------------
const rdec = new ScrambleDecode(VALUE, T0, dur, true);
check('reverse holds the value before the start time', rdec.display(T0 - 1) === VALUE);
check('reverse empties out after the window', rdec.display(T0 + dur + 1) === '');
check('reverse started()/finished() agree with display()', rdec.started(T0 - 1) === false && rdec.finished(T0 + dur) === true);
const reverseReveal = (t) =>
Math.floor(Math.max(0, Math.min(1, (1 - (t - T0) / dur) * 1.15)) * VALUE.length);
let rLenOk = true;
let rPrefixOk = true;
let rCharsOk = true;
let rMonotonic = true;
let rPrev = VALUE.length;
for (let t = T0; t <= T0 + dur; t += 16) {
const s = rdec.display(t);
const r = reverseReveal(t);
if (s.length !== VALUE.length) rLenOk = false;
for (let i = 0; i < r; i++) if (s[i] !== VALUE[i]) rPrefixOk = false; // still-stable part is exact
for (let i = r; i < s.length; i++) {
if (s[i] !== VALUE[i] && DECODE_CHARS.indexOf(s[i]) === -1) rCharsOk = false;
}
if (r > rPrev) rMonotonic = false;
rPrev = r;
}
check('reverse: display length stays constant through the window', rLenOk);
check('reverse: the still-stable prefix is always exact', rPrefixOk);
check('reverse: unrevealed slots come from the decode alphabet', rCharsOk);
check('reverse: the stable prefix only ever shrinks', rMonotonic);
// The default window is the menu's 620 ms (the canonical decode timing).
check('default window is the menus 620 ms', new ScrambleDecode('x', 0).dur === 620);
if (failures > 0) {
console.error(`\n${failures} decode test(s) FAILED`);
process.exit(1);
}
console.log('\ndecode: all checks passed');