import assert from 'node:assert/strict'; import { territory } from '../game/board'; import { botActions } from '../game/bots'; import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; export type ShortAllianceStep = { actor: string; action: Action }; export function shortAllianceLobby(): Game { const game = createGame('SHORTALLIANCE', newPlayer('at', 'Atreides', 'atreides')); joinGame(game, newPlayer('em', 'Emperor', 'emperor')); joinGame(game, newPlayer('gu', 'Guild', 'guild')); return game; } /** Only real owned controls advance setup and the first turn. Quiet Movement * is deliberately ended so the fixture cannot win before the real Nexus. */ function nextStep(game: Game): ShortAllianceStep | null { 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) continue; if (game.status === 'playing' && game.phase === 5 && game.active === player.id && !game.response && !game.decision && !game.truthtrance && !game.phaseOpening) return { actor: player.id, action: { type: 'endMovement' } }; return { actor: player.id, action }; } return null; } export function advanceShortAlliance(game: Game, predicate: (game: Game) => boolean, actions: ShortAllianceStep[], maximum = 1200): Game { for (let count = 0; count < maximum; count++) { if (predicate(game)) return game; assert.notEqual(game.status, 'finished', 'The native game ended before the requested boundary.'); const step = nextStep(game); assert.ok(step, `No native continuation at ${game.status}/${game.turn}/${game.phase}.`); actions.push(structuredClone(step)); game = applyAction(game, step.actor, step.action); } throw Error('The original game did not reach the requested alliance boundary.'); } /** Genuine Basic setup and genuine Nexus/alliance; only the unopened original * Spice deck order is selected to make the next normal worm deterministic. */ export function genuineShortAlliance(enabled: boolean) { let game = shortAllianceLobby(); const actions: ShortAllianceStep[] = []; const act = (actor: string, action: Action) => { actions.push({ actor, action }); game = applyAction(game, actor, action); }; if (enabled) act('at', { type: 'shortAllianceVictory', enabled: true }); for (const player of game.players) act(player.id, { type: 'ready' }); act('at', { type: 'start' }); game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions); const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper); assert.ok(worm >= 0, 'The original unopened deck retains an ordinary worm.'); const beforeDeck = game.spiceDeck.map(card => JSON.stringify(card)).sort(); game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]); assert.deepEqual(game.spiceDeck.map(card => JSON.stringify(card)).sort(), beforeDeck); game = advanceShortAlliance(game, state => state.nexus && !state.spiceWindow && !state.spiceResolution && !state.response && !state.decision && !state.truthtrance && !state.phaseOpening, actions); act('at', { type: 'alliance', target: 'em' }); act('em', { type: 'alliance', target: 'at' }); assert.equal(game.players.find(player => player.id === 'at')!.ally, 'em'); assert.equal(game.players.find(player => player.id === 'em')!.ally, 'at'); game = advanceShortAlliance(game, state => !state.nexus && state.phase >= 2, actions); return { game, actions }; } /** The setup and alliance above are real. For the rare three-stronghold * victory boundary, this arranges conserved forces and the Collection phase; * the actual final Collection readiness and Mentat win remain native actions. */ export function shortAllianceCollection(source: Game): Game { const game = structuredClone(source); for (const player of game.players) { player.reserves += Object.values(player.forces).reduce((sum, count) => sum + count, 0); player.forces = {}; if (player.elites) { player.elites.reserves += Object.values(player.elites.forces).reduce((sum, count) => sum + count, 0); player.elites.forces = {}; } } const at = game.players.find(player => player.id === 'at')!; const em = game.players.find(player => player.id === 'em')!; for (const id of ['arrakeen', 'carthag']) { at.forces[`${id}:${territory(id).sectors[0]}`] = 1; at.reserves--; } em.forces[`tueks_sietch:${territory('tueks_sietch').sectors[0]}`] = 1; em.reserves--; Object.assign(game, { phase: 7, ready: [], active: null, storm: 18, response: null, decision: null, truthtrance: null, phaseOpening: null }); return game; }