import assert from 'node:assert/strict'; import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action } from '../game/engine'; import { botActions } from '../game/bots'; import type { FactionId } from '../game/catalog'; import { withClassicDiscoveryNexusLottery } from './fixture-discovery-classic-nexus'; import { advanceSelectedIndependentNexus, actSelectedIndependentNexus, selectedIndependentNexusClean } from './fixture-selected-independent-nexus-cards'; export interface SelectedNexusNoCardsOptions { factions: readonly FactionId[]; advanced?: boolean; discovery?: boolean; } /** Uses the public lobby controls and real private setup; the three selected * faction decks do not imply the separate Ecaz independent-card option. */ export function createSelectedNexusNoCards(options: SelectedNexusNoCardsOptions) { const [host, ...others] = options.factions; let game = withClassicDiscoveryNexusLottery(() => createGame('SELECTEDNEXUSNOCARDS', newPlayer(host, host, host), options.advanced ?? false, ['ix', 'choam', 'ecaz'])); for (const faction of others) joinGame(game, newPlayer(faction, faction, faction)); const actions: { actor: string; action: Action }[] = []; const act = (actor: string, action: Action) => { actions.push({ actor, action: structuredClone(action) }); game = withClassicDiscoveryNexusLottery(() => applyAction(game, actor, action)); }; act(host, { type: 'moduleOption', module: 'nexusCards', enabled: true }); if (options.discovery) act(host, { type: 'moduleOption', module: 'discoveries', enabled: true }); for (const player of game.players) act(player.id, { type: 'ready' }); const lobby = structuredClone(game); act(host, options.advanced ? { type: 'start', advancedPreview: true } : { type: 'start' }); const setup = structuredClone(game); for (let count = 0; game.status === 'setup' && count < 100; count++) { let next: { actor: string; action: Action } | undefined; for (const player of game.players) { const view = viewGame(game, player.id); view.players.find(seat => seat.id === player.id)!.bot = 'Easy'; const action = botActions(view)[0]; if (action) { next = { actor: player.id, action }; break; } } assert.ok(next, `Original setup must expose an owned choice at ${game.setupStage}`); act(next.actor, next.action); } assert.equal(game.status, 'playing'); return { lobby, setup, game, actions }; } /** Real second-turn worm and closing Nexus. Reorder only undealt singleton * spice and Nexus cards; all hands and alliances result from player actions. */ export function selectedNexusNoCardsDraw(advanced = false) { const fixture = createSelectedNexusNoCards({ factions: ['atreides', 'guild', 'harkonnen', 'emperor'], advanced }); let game = structuredClone(fixture.game); const take = (worm: boolean) => { const index = game.spiceDeck.findIndex(card => worm ? 'worm' in card && !card.greatMaker && !card.thumper && !card.suppressed : 'territory' in card && !card.discovery); assert.ok(index >= 0); return game.spiceDeck.splice(index, 1)[0]; }; const first = [take(false), ...(advanced ? [take(false)] : [])]; const second = [take(true), take(false), ...(advanced ? [take(false)] : [])]; game.spiceDeck.unshift(...first, ...second); game = advanceSelectedIndependentNexus(game, state => state.turn === 2 && !!state.nexus && !state.spiceWindow && !state.spiceResolution && selectedIndependentNexusClean(state), fixture.actions); game = actSelectedIndependentNexus(game, 'guild', { type: 'alliance', target: 'harkonnen' }, fixture.actions); game = actSelectedIndependentNexus(game, 'harkonnen', { type: 'alliance', target: 'guild' }, fixture.actions); game = advanceSelectedIndependentNexus(game, state => state.nexusCards?.phase?.stage === 'drawing' && selectedIndependentNexusClean(state), fixture.actions); const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('richese'); assert.ok(index >= 0); cards.deck.unshift(cards.deck.splice(index, 1)[0]); const beforeDraw = structuredClone(game); const draw: Action = { type: 'nexusCardChoice', turn: game.turn, card: cards.hands.atreides, choice: 'draw', ownRedraws: 0 }; game = actSelectedIndependentNexus(game, 'atreides', draw, fixture.actions); const afterDraw = structuredClone(game); game = advanceSelectedIndependentNexus(game, state => state.phase === 5 && state.active === 'atreides' && selectedIndependentNexusClean(state), fixture.actions); return { ...fixture, beforeDraw, draw, afterDraw, game }; }