Files
Dune/tests/discoveries.test.ts
T
Quintine f0256c1b38 Prototype Discovery spice encounters, private tokens and stash rewards
Connect real Basic/Advanced base-faction audit setup to all seven Discovery
Spice Cards and eight physical tokens. Add Great Maker's ordered majority
vote and Fremen reserve ride, destructive Discovery blows, private faction
peeks and Collection inspection/reveal, stash rewards and mandatory saved
card overflow, and revealed nested board locations. Share private legal
choices with human controls and all four AI profiles. Preserve normal gates.

Independent review repairs orphaned worm/vote/ride controls, retargeted
reserve arrivals, actor bindings and incomplete overflow discard receipts.
Harden room-aware admission against unrevealed nested locations. Extend the
backed-up local prototype starter with a Discovery profile; preserve seats.

Still missing: next-turn free entry, five special location benefits and
Ornithopter movement spending. Current prototype playthroughs do not certify
those effects or complete module/expansion compliance.

Verification: npm run check passed 4264 offline tests plus types/lint;
npm run build passed; npm run test:integration passed 40 tests. The single
source-bound broad run took 129358ms / 6782ms / 23918ms respectively.
Report: /home/quintine/.local/share/dune/checkpoints/20260913-discovery-prototype/verification/report.json
Source fingerprint: 4ea7b5209a58c369b0b1f8dc239b5dd2f3302bb759d83c32ba5406e80d8ae154

Two seeded Basic/Advanced games complete with component custody intact.
SQLite races pay stashes once and resume committed discards without replay.
Browser 3RJBH26Y verifies genuine human Fremen setup, a Discovery blow from
one reordered physical card, native private Hiereg visibility, enlarged
card/rules reading and refresh. Room remains paused at spice acceptance.
All 125 opening saved rooms unchanged; 31 added, 156 total. No server restart.

Efficiency: three reused bounded helpers; aggregate counter 60591430 to
61812513 after verification, 1221083 tokens before final Git/push overhead.
47 added offline tests. One stable broad run after focused review fixes;
no calibration or exhaustive optional-module interaction matrix.
2026-09-13 13:27:58 +10:00

463 lines
14 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
DISCOVERY_CARD_PLACEMENTS,
DISCOVERY_LOCATION_BY_ID,
DISCOVERY_LOCATION_IDS,
DISCOVERY_LOCATIONS,
DISCOVERY_SPICE_CARDS,
DISCOVERY_TOKEN_BY_ID,
DISCOVERY_TOKENS,
JACURUTU_SIETCH,
clearNewlyRevealed,
consumeOrnithopter,
createDiscoveryState,
isDiscoveryLocationId,
isDiscoveryTokenId,
placeDiscovery,
projectDiscoveryState,
rememberDiscoveryFace,
revealDiscoveryToken,
validateDiscoveryState,
type DiscoveryState,
type DiscoveryTokenFace,
} from '../game/discoveries';
const ordered = () => createDiscoveryState(() => 0.999);
const reload = (state: DiscoveryState) =>
JSON.parse(JSON.stringify(state)) as DiscoveryState;
const tokenWithFace = (state: DiscoveryState, face: DiscoveryTokenFace) =>
state.tokens.find((token) => token.face === face)!;
void test('printed cards and immutable token metadata preserve the sourced inventory and placements', () => {
assert.deepEqual(DISCOVERY_SPICE_CARDS, [
{
territory: 'hagga_basin',
amount: 6,
sector: 12,
discovery: 'discovery-hagga-basin',
},
{
territory: 'rock_outcroppings',
amount: 6,
sector: 13,
discovery: 'discovery-rock-outcroppings',
},
{
territory: 'sihaya_ridge',
amount: 6,
sector: 9,
discovery: 'discovery-sihaya-ridge',
},
{
territory: 'wind_pass_north',
amount: 6,
sector: 17,
discovery: 'discovery-wind-pass-north',
},
{
territory: 'funeral_plain',
amount: 6,
sector: 15,
discovery: 'discovery-funeral-plain',
},
{
territory: 'oh_gap',
amount: 6,
sector: 10,
discovery: 'discovery-old-gap',
},
]);
assert.deepEqual(DISCOVERY_CARD_PLACEMENTS, {
'discovery-hagga-basin': {
type: 'hiereg',
territory: 'gara_kulon',
sector: 8,
},
'discovery-rock-outcroppings': {
type: 'hiereg',
territory: 'meridian',
sector: 1,
},
'discovery-sihaya-ridge': {
type: 'hiereg',
territory: 'cielago_east',
sector: 3,
},
'discovery-wind-pass-north': {
type: 'smuggler',
territory: 'plastic_basin',
sector: 13,
},
'discovery-funeral-plain': {
type: 'smuggler',
territory: 'pasty_mesa',
sector: 7,
},
'discovery-old-gap': {
type: 'smuggler',
territory: 'false_wall_west',
sector: 17,
},
});
assert.deepEqual(
DISCOVERY_TOKENS.map(({ id, type, kind }) => ({ id, type, kind })),
[
{ id: 'jacurutu-sietch', type: 'hiereg', kind: 'location' },
{ id: 'cistern', type: 'hiereg', kind: 'location' },
{
id: 'ecological-testing-station',
type: 'hiereg',
kind: 'location',
},
{ id: 'shrine', type: 'hiereg', kind: 'location' },
{
id: 'orgiz-processing-station',
type: 'smuggler',
kind: 'location',
},
{ id: 'treachery-card-stash', type: 'smuggler', kind: 'instant' },
{ id: 'spice-stash', type: 'smuggler', kind: 'instant' },
{ id: 'ornithopter', type: 'smuggler', kind: 'carried' },
],
);
assert.equal(JACURUTU_SIETCH, 'jacurutu-sietch');
assert.deepEqual(DISCOVERY_LOCATION_IDS, [
'jacurutu-sietch',
'cistern',
'ecological-testing-station',
'shrine',
'orgiz-processing-station',
]);
assert.equal(DISCOVERY_LOCATION_BY_ID[JACURUTU_SIETCH].stronghold, true);
assert.equal(DISCOVERY_TOKEN_BY_ID.ornithopter.location, null);
assert.equal(isDiscoveryLocationId('cistern'), true);
assert.equal(isDiscoveryLocationId('ornithopter'), false);
assert.equal(isDiscoveryTokenId('discovery-token-8'), true);
assert.equal(isDiscoveryTokenId('discovery-token-9'), false);
assert.ok(Object.isFrozen(DISCOVERY_SPICE_CARDS));
assert.ok(DISCOVERY_SPICE_CARDS.every(Object.isFrozen));
assert.ok(Object.isFrozen(DISCOVERY_TOKENS));
assert.ok(DISCOVERY_TOKENS.every(Object.isFrozen));
assert.ok(Object.isFrozen(DISCOVERY_LOCATIONS));
assert.ok(DISCOVERY_LOCATIONS.every(Object.isFrozen));
});
void test('setup shuffles eight secret faces behind stable opaque physical IDs and survives JSON', () => {
let calls = 0;
const state = createDiscoveryState(() => {
calls++;
return 0.999;
});
assert.equal(calls, 7);
assert.deepEqual(
state.tokens.map((token) => token.id),
Array.from({ length: 8 }, (_, index) => `discovery-token-${index + 1}`),
);
assert.deepEqual(
state.tokens.map((token) => token.face),
DISCOVERY_TOKENS.map((token) => token.id),
);
assert.ok(
state.tokens.every(
(token) =>
token.status === 'supply' &&
token.territory === null &&
token.sector === null &&
token.revealedTurn === null &&
token.owner === null &&
token.known.length === 0,
),
);
assert.notDeepEqual(
createDiscoveryState(() => 0).tokens.map((token) => token.face),
state.tokens.map((token) => token.face),
);
validateDiscoveryState(reload(state));
for (const draw of [-1, 1, NaN, Infinity])
assert.throws(() => createDiscoveryState(() => draw), /random draws/);
});
void test('placement draws only the required type and uses the printed token destination without mutation', () => {
const initial = ordered();
const before = reload(initial);
const hiereg = placeDiscovery(initial, 'discovery-rock-outcroppings', () => 0);
const placedHiereg = hiereg.tokens.find((token) => token.status === 'placed')!;
assert.deepEqual(
{
id: placedHiereg.id,
type: placedHiereg.type,
face: placedHiereg.face,
territory: placedHiereg.territory,
sector: placedHiereg.sector,
},
{
id: 'discovery-token-1',
type: 'hiereg',
face: 'jacurutu-sietch',
territory: 'meridian',
sector: 1,
},
);
const smuggler = placeDiscovery(initial, 'discovery-funeral-plain', () => 0);
const placedSmuggler = smuggler.tokens.find(
(token) => token.status === 'placed',
)!;
assert.deepEqual(
[
placedSmuggler.id,
placedSmuggler.type,
placedSmuggler.face,
placedSmuggler.territory,
placedSmuggler.sector,
],
['discovery-token-5', 'smuggler', 'orgiz-processing-station', 'pasty_mesa', 7],
);
assert.deepEqual(initial, before);
assert.notEqual(hiereg.tokens, initial.tokens);
assert.notEqual(hiereg.tokens[0].known, initial.tokens[0].known);
});
void test('unavailable types and bad randomness reject before consuming custody', () => {
let state = ordered();
for (const card of [
'discovery-hagga-basin',
'discovery-rock-outcroppings',
'discovery-sihaya-ridge',
'discovery-hagga-basin',
] as const)
state = placeDiscovery(state, card, () => 0);
const before = reload(state);
let calls = 0;
assert.throws(
() =>
placeDiscovery(state, 'discovery-hagga-basin', () => {
calls++;
return 0;
}),
/No hiereg/,
);
assert.equal(calls, 0);
assert.deepEqual(state, before);
const initial = ordered();
for (const draw of [-0.1, 1, NaN, Infinity]) {
assert.throws(
() => placeDiscovery(initial, 'discovery-hagga-basin', () => draw),
/random draws/,
);
assert.deepEqual(initial, ordered());
}
});
void test('private projection hides supply order and hidden faces except for eligible or informed factions', () => {
let state = placeDiscovery(
ordered(),
'discovery-rock-outcroppings',
() => 0,
);
state = placeDiscovery(state, 'discovery-funeral-plain', () => 0);
const hiereg = state.tokens.find(
(token) => token.status === 'placed' && token.type === 'hiereg',
)!;
const smuggler = state.tokens.find(
(token) => token.status === 'placed' && token.type === 'smuggler',
)!;
const publicView = projectDiscoveryState(state);
assert.equal(publicView.supplyCount, 6);
assert.deepEqual(
publicView.tokens.map((token) => token.face),
[null, null],
);
assert.equal('known' in publicView.tokens[0], false);
assert.equal(projectDiscoveryState(state, 'fremen').tokens[0].face, hiereg.face);
assert.equal(projectDiscoveryState(state, 'fremen').tokens[1].face, null);
assert.equal(projectDiscoveryState(state, 'guild').tokens[0].face, null);
assert.equal(
projectDiscoveryState(state, 'guild').tokens[1].face,
smuggler.face,
);
const informed = rememberDiscoveryFace(state, hiereg.id, 'atreides');
assert.equal(
projectDiscoveryState(informed, 'atreides').tokens[0].face,
hiereg.face,
);
assert.equal(projectDiscoveryState(informed, 'harkonnen').tokens[0].face, null);
const reordered = reload(state);
reordered.tokens.reverse();
assert.deepEqual(
projectDiscoveryState(reordered).tokens
.map((token) => ({ ...token, id: 'opaque' }))
.sort((a, b) => String(a.type).localeCompare(String(b.type))),
publicView.tokens
.map((token) => ({ ...token, id: 'opaque' }))
.sort((a, b) => String(a.type).localeCompare(String(b.type))),
);
});
void test('location reveal remains on the board, becomes public, and tracks one migration window', () => {
const placed = placeDiscovery(
ordered(),
'discovery-rock-outcroppings',
() => 0,
);
const token = placed.tokens.find((candidate) => candidate.status === 'placed')!;
const before = reload(placed);
const revealed = revealDiscoveryToken(placed, token.id, 4);
assert.deepEqual(revealed.outcome, {
kind: 'location',
location: 'jacurutu-sietch',
});
const face = revealed.state.tokens.find((candidate) => candidate.id === token.id)!;
assert.equal(face.status, 'placed');
assert.equal(face.territory, 'meridian');
assert.equal(face.sector, 1);
assert.equal(face.revealedTurn, 4);
assert.deepEqual(revealed.state.newlyRevealed, [token.id]);
assert.equal(projectDiscoveryState(revealed.state).tokens[0].face, token.face);
assert.deepEqual(placed, before);
assert.throws(
() => revealDiscoveryToken(revealed.state, token.id, 4),
/hidden placed/,
);
const cleared = clearNewlyRevealed(revealed.state);
assert.deepEqual(cleared.newlyRevealed, []);
assert.equal(cleared.tokens.find((candidate) => candidate.id === token.id)!.revealedTurn, 4);
validateDiscoveryState(reload(cleared));
});
void test('stash outcomes remove one face while Ornithopter transfers and consumes exact custody', () => {
const cases = [
{
face: 'treachery-card-stash' as const,
draw: 0.3,
outcome: { kind: 'treachery-card-stash' },
},
{
face: 'spice-stash' as const,
draw: 0.6,
outcome: { kind: 'spice-stash', amount: 7 },
},
];
for (const entry of cases) {
const placed = placeDiscovery(
ordered(),
'discovery-funeral-plain',
() => entry.draw,
);
const token = tokenWithFace(placed, entry.face);
const result = revealDiscoveryToken(placed, token.id, 6);
assert.deepEqual(result.outcome, entry.outcome);
const removed = tokenWithFace(result.state, entry.face);
assert.equal(removed.status, 'removed');
assert.equal(removed.territory, null);
assert.equal(removed.sector, null);
assert.equal(removed.revealedTurn, 6);
assert.equal(removed.acquiredTurn, 6);
validateDiscoveryState(reload(result.state));
}
const placed = placeDiscovery(
ordered(),
'discovery-funeral-plain',
() => 0.999,
);
const ornithopter = tokenWithFace(placed, 'ornithopter');
const before = reload(placed);
assert.throws(
() => revealDiscoveryToken(placed, ornithopter.id, 7),
/receiving player/,
);
assert.deepEqual(placed, before);
const carried = revealDiscoveryToken(placed, ornithopter.id, 7, 'player-a');
assert.deepEqual(carried.outcome, {
kind: 'ornithopter',
owner: 'player-a',
});
assert.deepEqual(
{
status: tokenWithFace(carried.state, 'ornithopter').status,
owner: tokenWithFace(carried.state, 'ornithopter').owner,
acquiredTurn: tokenWithFace(carried.state, 'ornithopter').acquiredTurn,
},
{ status: 'carried', owner: 'player-a', acquiredTurn: 7 },
);
const consumed = consumeOrnithopter(carried.state, 'player-a');
assert.equal(tokenWithFace(consumed, 'ornithopter').status, 'removed');
assert.equal(tokenWithFace(consumed, 'ornithopter').owner, null);
assert.throws(
() => consumeOrnithopter(consumed, 'player-a'),
/carry exactly one Ornithopter/,
);
validateDiscoveryState(reload(consumed));
});
void test('malformed census, private knowledge, placement and migration state reject unchanged', () => {
const mutations: ((state: DiscoveryState) => void)[] = [
(state) => {
state.tokens.pop();
},
(state) => {
state.tokens[1].id = state.tokens[0].id;
},
(state) => {
state.tokens[1].face = state.tokens[0].face;
},
(state) => {
state.tokens[0].type = 'smuggler';
},
(state) => {
state.tokens[0].known = ['unknown' as never];
},
(state) => {
state.tokens[0].status = 'placed';
},
(state) => {
state.tokens[0].owner = 'player-a';
},
(state) => {
state.newlyRevealed.push('discovery-token-1');
},
(state) => {
Object.assign(state.tokens[0], { leakedFace: 'secret' });
},
(state) => {
state.version = 2 as 1;
},
];
for (const mutate of mutations) {
const state = ordered();
mutate(state);
const before = JSON.stringify(state);
assert.throws(() => validateDiscoveryState(state));
let calls = 0;
assert.throws(() =>
placeDiscovery(state, 'discovery-hagga-basin', () => {
calls++;
return 0;
}),
);
assert.equal(calls, 0);
assert.equal(JSON.stringify(state), before);
}
const frozen = placeDiscovery(
ordered(),
'discovery-rock-outcroppings',
() => 0,
);
frozen.tokens.forEach((token) => {
Object.freeze(token.known);
Object.freeze(token);
});
Object.freeze(frozen.tokens);
Object.freeze(frozen.newlyRevealed);
Object.freeze(frozen);
const next = rememberDiscoveryFace(
frozen,
frozen.tokens.find((token) => token.status === 'placed')!.id,
'atreides',
);
validateDiscoveryState(next);
assert.notEqual(next.tokens, frozen.tokens);
});