115 lines
4.9 KiB
JavaScript
115 lines
4.9 KiB
JavaScript
/**
|
||
* 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 menu’s 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');
|