Files
Dune/tests/independent-discovery-nexus-entry.test.ts
Quintine 677ad7aa3c Compose independent cards with Discovery and optional Nexus
Connect original selected-deck Discovery setup, optional Nexus/World/Skills/phase modules and physical card consumers through shared public capabilities. Retain Cunning revealed-location arrival, Sandtrout suppression, paid nested R/Harass and actual later Recruits revival. Preserve native trained-capture and unrelated-preview boundaries.

Validation: frozen 15-file118/118, types/lint/build and six finite original-action/rendered-control programmes; four minimal legal policies. External smoke corrections changed no application source. Existing persistent notices also rechecked23/23. Full combinations, assurance and deployed acceptance remain open; no saved-game, restart or deployment operation.
2026-10-18 11:48:45 +11:00

151 lines
9.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { spiceDeck } from '../game/cards';
import { DISCOVERY_SPICE_CARDS, validateDiscoveryState } from '../game/discoveries';
import { ECAZ_TREACHERY_DEFINITIONS } from '../game/ecaz-cards';
import { independentDiscoveryProfile, independentDiscoveryNexusProfile, independentNexusProfile, nexusCoreProfile } from '../game/faction-module-profile';
import { createTechTokens } from '../game/tech-tokens';
import { createSelectedIndependentEntry, type SelectedIndependentEntryOptions } from './fixture-selected-independent-nexus-entry';
const entries: SelectedIndependentEntryOptions[] = [
{ expansions: [], factions: ['atreides', 'guild'] },
{ expansions: ['ix'], factions: ['ixians', 'tleilaxu'] },
{ expansions: ['choam'], factions: ['choam', 'richese', 'guild'], advanced: true,
skills: true, homeworlds: true, tech: true, strongholds: true },
{ expansions: ['ecaz'], factions: ['ecaz', 'moritani'] },
{ expansions: ['ecaz', 'ix', 'choam'], factions: ['ecaz', 'moritani', 'ixians', 'tleilaxu', 'choam', 'richese'],
advanced: true, skills: true, homeworlds: true, tech: true, strongholds: true },
{ expansions: ['ix', 'choam', 'ecaz'], factions: ['emperor', 'atreides'],
advanced: true, skills: true, strongholds: true },
];
for (const options of [entries[0], entries[4]]) void test(
`normal independent Discovery keeps Nexus optional for ${options.expansions.join('+') || 'classic'}`,
() => {
const { game, actions } = createSelectedIndependentEntry({ ...options, discoveries: true, nexus: false });
assert.ok(independentDiscoveryProfile(game));
assert.equal(independentDiscoveryNexusProfile(game), false);
assert.equal(nexusCoreProfile(game), false);
assert.equal(game.nexusCards, undefined);
assert.ok(game.discoveries);
validateDiscoveryState(game.discoveries);
assert.equal(game.discoveries.tokens.length, 8);
assert.ok(!actions.some(step => step.action.type === 'moduleOption' && step.action.module === 'nexusCards'));
assert.equal(game.nexusIxianReplacementPreview, undefined);
assert.equal(game.nexusIxianBetrayalPreview, undefined);
assert.equal(game.guildBetrayalPreview, undefined);
assert.equal(!!game.leaderSkills, !!options.skills);
assert.equal(!!game.homeworlds, !!options.homeworlds);
},
);
for (const options of entries) void test(
`normal ${options.advanced ? 'Advanced' : 'Basic'} independent Discovery Nexus setup ${options.expansions.join('+') || 'classic'}/${options.factions.join('+')}`,
() => {
const { lobby, setup, game, actions } = createSelectedIndependentEntry({ ...options, discoveries: true });
assert.ok(actions.some(step => step.actor === lobby.host && step.action.type === 'moduleOption' &&
step.action.module === 'discoveries' && step.action.enabled === true));
assert.equal(lobby.discoveryEnabled, true);
assert.equal(lobby.lobbyOptions?.ecazTreachery, true);
assert.equal(lobby.discoveries, undefined, 'The host selects a module before any token is dealt.');
assert.equal(game.discoveryEnabled, true);
assert.equal(game.ecazTreachery, true);
assert.equal(independentDiscoveryNexusProfile(setup), true);
assert.equal(independentDiscoveryNexusProfile(game), true);
assert.equal(independentNexusProfile(game), true);
assert.equal(nexusCoreProfile(game), true);
assert.deepEqual(game.expansions, options.expansions);
assert.equal(game.status, 'playing');
assert.equal(game.phase, 0);
assert.equal(!!game.homeworlds, !!options.homeworlds);
assert.equal(!!game.leaderSkills, !!options.skills);
assert.equal(!!game.techTokens, !!options.tech);
assert.equal(!!game.strongholdCards, !!options.strongholds);
const ordinary = [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand)];
assert.equal(ordinary.length, options.expansions.includes('ix') ? 50 : options.expansions.includes('choam') ? 38 : 36);
for (const definition of ECAZ_TREACHERY_DEFINITIONS)
assert.equal(ordinary.filter(card => card.id === definition.card.id).length, 1,
`${definition.card.name} remains one real selected independent card.`);
assert.equal(game.richeseCache?.length ?? 0, options.factions.includes('richese') ? 10 : 0);
assert.equal(game.nexusCards!.cards!.deck.length, 12);
assert.equal(game.nexusCards!.cards!.discard.length, 0);
assert.ok(Object.values(game.nexusCards!.cards!.hands).every(card => card === null),
'Fresh module setup does not invent a qualifying closing Nexus draw.');
assert.ok(game.discoveries);
validateDiscoveryState(game.discoveries);
assert.equal(game.discoveries.tokens.length, 8);
assert.ok(game.discoveries.tokens.every(token => token.status === 'supply' &&
token.territory === null && token.sector === null && token.revealedTurn === null));
assert.deepEqual(game.discoveries.newlyRevealed, []);
assert.equal(game.spiceDeck.length, spiceDeck(options.expansions.includes('ix')).length + 7);
assert.deepEqual(game.spiceDeck.flatMap(card => 'territory' in card && card.discovery ? [card.discovery] : []).sort(),
DISCOVERY_SPICE_CARDS.map(card => card.discovery).sort());
assert.equal(game.spiceDeck.filter(card => 'worm' in card && card.greatMaker).length, 1);
assert.equal(game.spiceDeck.filter(card => 'sandtrout' in card).length, options.expansions.includes('ix') ? 1 : 0);
assert.equal(game.greatMaker, undefined, 'No appearance occurs during fresh Storm setup.');
assert.equal(!!game.nexusIxianBetrayalPreview, options.factions.includes('ixians'));
assert.equal(!!game.nexusIxianReplacementPreview, !options.factions.includes('ixians'));
assert.equal(!!game.ecazOccupyPreview, options.factions.includes('ecaz'));
assert.equal(!!game.moritaniAssassinate, !!options.advanced && options.factions.includes('moritani'));
if (options.factions.includes('richese'))
assert.equal(game.players.find(player => player.faction === 'richese')!.noField!.tokens.length, 3);
if (options.factions.includes('tleilaxu'))
assert.ok(game.players.find(player => player.faction === 'tleilaxu')!.faceDancers?.length,
'The original Tleilaxu private setup remains connected.');
if (options.skills) assert.ok(['skillTreachery', 'leaderSkills'].includes(setup.setupStage!));
if (setup.techTokens) assert.deepEqual(setup.techTokens, createTechTokens(setup.players));
if (game.strongholdCards) assert.ok(Object.values(game.strongholdCards.owners).every(owner => owner === null));
},
);
void test('independent Discovery capability requires explicit cards, Discovery, Nexus and registered selected seats', () => {
const base = { advanced: true, expansions: [], players: [{ faction: 'atreides' }, { faction: 'guild' }],
ecazTreachery: true, discoveryEnabled: true, nexusCards: true };
assert.equal(independentDiscoveryNexusProfile(base), true);
const invalid = [
{ ...base, ecazTreachery: false }, { ...base, discoveryEnabled: false },
{ ...base, discoveryEnabled: undefined, discoveries: {} }, { ...base, nexusCards: false },
{ ...base, expansions: ['ix', 'ix'] }, { ...base, expansions: ['ecaz', 'ix', 'choam', 'ecaz'] },
{ ...base, expansions: ['sandtrout'] }, { ...base, players: [{ faction: 'atreides' }] },
{ ...base, players: [{ faction: 'atreides' }, { faction: 'atreides' }] },
{ ...base, players: [{ faction: 'atreides' }, { faction: 'richese' }] },
{ ...base, players: [{ faction: 'atreides' }, { faction: 'unknown' }] },
{ ...base, expansions: ['ix'], players: [{ faction: 'ixians' }, { faction: 'richese' }] },
{ ...base, techTokens: true }, { ...base, advanced: false, strongholdCards: true },
{ ...base, semutaPreview: true }, { ...base, advancedPreview: true },
{ ...base, kullPreview: true }, { ...base, nexusKullPreview: true },
{ ...base, richeseBetrayalPreview: true },
];
for (const configuration of invalid)
assert.equal(independentDiscoveryNexusProfile(configuration), false, JSON.stringify(configuration));
});
void test('live independent Discovery retains native stamps and transient encounters while preserving trained E3 capture boundary', () => {
const native = { advanced: true, expansions: ['ecaz'],
players: [{ faction: 'moritani' }, { faction: 'harkonnen' }],
ecazTreachery: true, discoveryEnabled: true, nexusCards: true };
assert.equal(independentDiscoveryNexusProfile({ ...native, moritaniAssassinatePreview: true }), true);
assert.equal(independentDiscoveryNexusProfile({ ...native, leaderSkills: true }), false);
assert.equal(independentDiscoveryNexusProfile({ ...native, advanced: false, leaderSkills: true }), true);
assert.equal(independentDiscoveryNexusProfile({ ...native, advanced: false, moritaniAssassinatePreview: true }), false);
assert.equal(independentDiscoveryNexusProfile({ ...native, leaderSkills: true,
players: [{ faction: 'atreides' }, { faction: 'harkonnen' }] }), true,
'Selecting an unused E3 deck does not add native trained capture.');
const encounter = { ...native, expansions: ['ecaz', 'ix'],
players: [{ faction: 'moritani' }, { faction: 'ixians' }, { faction: 'guild' }],
nexusIxianBetrayalPreview: true, moritaniAssassinatePreview: true,
discoveries: {}, discoveryStash: {}, greatMaker: {}, sandtrout: {},
homeworlds: true, leaderSkills: true, techTokens: true, strongholdCards: true };
assert.equal(independentDiscoveryNexusProfile(encounter), true,
'The public runtime envelope does not reject a legal initialized module because an encounter exists.');
assert.equal(nexusCoreProfile(encounter), true);
assert.equal(independentNexusProfile(encounter), true);
assert.equal(independentDiscoveryNexusProfile({ ...encounter,
players: [{ faction: 'moritani' }, { faction: 'tleilaxu' }, { faction: 'guild' }] }), false,
'An Ixian-specific native stamp requires the actual seated Ixian.');
});