84 lines
4.5 KiB
TypeScript
84 lines
4.5 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
nativeStormDialOpportunity, parseStormDialClaim, parseStormDialSource,
|
|
stormDialMatches, type StormDialGame,
|
|
} from '../game/storm-dial-question';
|
|
|
|
const first: StormDialGame = {
|
|
status: 'playing', advanced: false, phase: 0, turn: 1,
|
|
players: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
|
|
stormDialers: ['a', 'b'], stormPending: null, stormDials: {},
|
|
};
|
|
const source = (turn: number, player: string) => JSON.stringify(['stormDial', turn, player]);
|
|
const claim = (turn: number, player: string, value: number) =>
|
|
({ kind: 'stormDial' as const, source: source(turn, player), value });
|
|
|
|
void test('public opportunity selects the real Basic unsubmitted dialer and exact turn', () => {
|
|
assert.deepEqual(nativeStormDialOpportunity(first, 'a'), {
|
|
turn: 1, player: 'a', source: source(1, 'a'), min: 0, max: 20,
|
|
});
|
|
assert.equal(nativeStormDialOpportunity(first, 'c'), null);
|
|
assert.equal(nativeStormDialOpportunity(first, 'a', source(2, 'a')), null);
|
|
assert.equal(nativeStormDialOpportunity({ ...first, phase: 1 }, 'a'), null);
|
|
assert.equal(nativeStormDialOpportunity({ ...first, status: 'finished' }, 'a'), null);
|
|
assert.equal(nativeStormDialOpportunity({ ...first, advanced: true }, 'a'), null);
|
|
assert.equal(nativeStormDialOpportunity({ ...first, stormPending: 0 }, 'a'), null);
|
|
assert.deepEqual(nativeStormDialOpportunity({ ...first, turn: 2 }, 'a'), {
|
|
turn: 2, player: 'a', source: source(2, 'a'), min: 1, max: 3,
|
|
});
|
|
});
|
|
|
|
void test('server and public view eligibility test submission without reading sealed values', () => {
|
|
const hidden = Object.defineProperty({}, 'a', { enumerable: true, get() { throw Error('read sealed value'); } });
|
|
const state = { ...first, stormDials: hidden };
|
|
assert.equal(nativeStormDialOpportunity(state, 'a'), null);
|
|
assert.ok(nativeStormDialOpportunity(state, 'b'));
|
|
const { stormDials: _hidden, ...publicView } = state;
|
|
assert.equal(nativeStormDialOpportunity({ ...publicView, stormSubmitted: ['a'] }, 'a'), null);
|
|
assert.ok(nativeStormDialOpportunity({ ...publicView, stormSubmitted: ['a'] }, 'b'));
|
|
assert.equal(nativeStormDialOpportunity({ ...publicView, stormSubmitted: ['c'] }, 'b'), null);
|
|
});
|
|
|
|
void test('canonical source and claim enforce actual turn-specific ranges and no extra keys', () => {
|
|
assert.deepEqual(parseStormDialSource(source(1, 'a')), { turn: 1, player: 'a' });
|
|
for (const invalid of [' ["stormDial",1,"a"]', source(0, 'a'), source(1, ''), '["stormDial",1,"a",0]', 'not-json'])
|
|
assert.equal(parseStormDialSource(invalid), null);
|
|
assert.deepEqual(parseStormDialClaim(claim(1, 'a', 0)), claim(1, 'a', 0));
|
|
assert.deepEqual(parseStormDialClaim(claim(1, 'a', 20)), claim(1, 'a', 20));
|
|
assert.deepEqual(parseStormDialClaim(claim(2, 'a', 3)), claim(2, 'a', 3));
|
|
for (const invalid of [claim(1, 'a', -1), claim(1, 'a', 21), claim(2, 'a', 0),
|
|
claim(2, 'a', 4), claim(2, 'a', 2.5), { ...claim(1, 'a', 4), reveal: true }])
|
|
assert.throws(() => parseStormDialClaim(invalid));
|
|
});
|
|
|
|
void test('match evaluates only an actual legal submission in the named native range', () => {
|
|
assert.equal(stormDialMatches(claim(1, 'a', 0), 0), true);
|
|
assert.equal(stormDialMatches(claim(1, 'a', 0), 1), false);
|
|
assert.equal(stormDialMatches(claim(2, 'a', 3), 3), true);
|
|
for (const invalid of [undefined, null, -1, 0, 4, 1.5, '3'])
|
|
assert.throws(() => stormDialMatches(claim(2, 'a', 2), invalid));
|
|
});
|
|
|
|
void test('threshold comparisons retain the old exact claim shape and native bounds', () => {
|
|
const old = claim(1, 'a', 5);
|
|
assert.deepEqual(parseStormDialClaim(old), old);
|
|
assert.equal(stormDialMatches({ ...old, compare: 'eq' }, 5), true);
|
|
assert.equal(stormDialMatches({ ...old, compare: 'gte' }, 6), true);
|
|
assert.equal(stormDialMatches({ ...old, compare: 'gte' }, 4), false);
|
|
assert.equal(stormDialMatches({ ...old, compare: 'lte' }, 4), true);
|
|
assert.equal(stormDialMatches({ ...old, compare: 'lte' }, 6), false);
|
|
assert.throws(() => parseStormDialClaim({ ...old, compare: 'gt' }));
|
|
});
|
|
|
|
void test('malformed or inconsistent public seat and dialer sets have no opportunity', () => {
|
|
for (const game of [
|
|
{ ...first, stormDialers: ['a', 'a'] },
|
|
{ ...first, stormDialers: ['a', 'unknown'] },
|
|
{ ...first, players: [{ id: 'a' }, { id: 'a' }] },
|
|
{ ...first, stormDialers: [] },
|
|
{ ...first, stormDials: undefined },
|
|
{ ...first, stormSubmitted: ['a', 'a'] },
|
|
]) assert.equal(nativeStormDialOpportunity(game, 'a'), null);
|
|
});
|