import assert from 'node:assert/strict'; import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; import type { FactionId } from '../game/catalog'; import { withClassicDiscoveryNexusLottery } from './fixture-discovery-classic-nexus'; import { nextPairedChoamSkillsStep } from './fixture-paired-choam-nexus-skills'; import { actSelectedIndependentNexus, advanceSelectedIndependentNexus, type SelectedIndependentNexusStep } from './fixture-selected-independent-nexus-cards'; export type DiscoverySkillsOptions = { advanced: boolean; factions: readonly FactionId[]; expansions?: Game['expansions']; }; /** Fresh public lobby, real original private training and native setup. Only * the original random lottery is fixed; no cards, leaders or forces are added. */ export function selectedDiscoverySkillsLobby(options: DiscoverySkillsOptions) { const [host, ...others] = options.factions; const expansions = options.expansions ?? ['ecaz', 'ix', 'choam']; let game = withClassicDiscoveryNexusLottery(() => createGame('SELECTEDDISCOVERYSKILLS', newPlayer(host, host, host), options.advanced, expansions)); for (const faction of others) joinGame(game, newPlayer(faction, faction, faction)); const actions: SelectedIndependentNexusStep[] = []; const step = (actor: string, action: Action) => { game = actSelectedIndependentNexus(game, actor, action, actions); }; if (expansions.includes('ix') && expansions.includes('choam')) step(host, {type:'moduleOption', module:'kullPreview', enabled:false}); step(host, {type:'moduleOption', module:'leaderSkills', enabled:true}); step(host, {type:'moduleOption', module:'discoveries', enabled:true}); for (const player of game.players) step(player.id, {type:'ready'}); return {game, host, step, actions}; } export function selectedDiscoverySkillsStart(options: DiscoverySkillsOptions) { const fixture = selectedDiscoverySkillsLobby(options); let game = fixture.game; const lobby = structuredClone(game); const start: Action = options.advanced ? {type:'start', advancedPreview:true} : {type:'start'}; game = actSelectedIndependentNexus(game, fixture.host, start, fixture.actions); const setup = structuredClone(game); for (let count = 0; game.status === 'setup' && count < 300; count++) { const next = nextPairedChoamSkillsStep(game); assert.ok(next, `Original setup must expose a choice at ${game.setupStage ?? game.decision?.kind}`); game = actSelectedIndependentNexus(game, next.actor, next.action, fixture.actions); } assert.equal(game.status, 'playing'); return {...fixture, game, lobby, setup}; } /** Reorder only the unopened original Spice Cards. The engine resolves the * Discovery and the following bidding and movement through ordinary actions. */ export function advanceDiscoverySkillsBlow(game: Game, actions: SelectedIndependentNexusStep[]) { for (const position of game.advanced ? [0, 1] : [0]) { const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card && (position === 0 ? card.discovery === 'discovery-hagga-basin' : !card.discovery)); assert.ok(index >= position); [game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]]; } return advanceSelectedIndependentNexus(game, state => state.turn === 1 && state.phase === 5 && !state.phaseOpening && !state.response && !state.decision && !state.truthtrance && !state.pendingTreacheryDiscard, actions); }