import assert from 'node:assert/strict'; import { applyAction, viewGame, type Action, type Game } from '../game/engine'; import { nexusRicheseFixture, nexusRicheseInventory, holdNexusRicheseCard, type NexusRicheseFixtureOptions, } from './fixture-nexus-richese'; import { nexusReload, nexusAllow, orderNexusSpice, } from './fixture-nexus-cards'; import { botActions } from '../game/bots'; import { nexusTraitorInventory } from './fixture-nexus-traitors'; import { homeworldGameIntegrity } from '../game/homeworld-game'; export type NexusGuildSecretAllyFixtureOptions = Omit< NexusRicheseFixtureOptions, 'guild' | 'ownerFaction' | 'opponentFaction' > & { ownerFaction?: Exclude; opponentFaction?: Exclude< NexusRicheseFixtureOptions['opponentFaction'], 'guild' >; }; /** CONTROLLED historical Movement boundary after genuine setup: the inherited * Richese fixture stages its wallets, reserve pools and phase5 rather than * playing the intervening phases. Retire its old phase2 Charity frame with that * declared jump. Physical Nexus identities are exchanged before declaration; * no transport receipt or completed native Charity opportunity is invented. */ export function nexusGuildSecretAllyFixture( options: NexusGuildSecretAllyFixtureOptions = {}, ) { const f = nexusRicheseFixture({ ...options, opponentFaction: options.opponentFaction ?? (options.ownerFaction === 'harkonnen' ? 'beneGesserit' : 'harkonnen'), }); assert.equal(f.g.phase, 5); delete f.g.charityOrder; const cards = f.g.nexusCards!.cards!, index = cards.deck.indexOf('guild'); assert.ok(index >= 0); assert.equal(cards.hands[f.owner], 'richese'); cards.deck[index] = 'richese'; cards.hands[f.owner] = 'guild'; assert.equal( f.g.players.some((p) => p.faction === 'guild'), false, ); nexusRicheseInventory(f.g); return f; } export function nexusGuildSecretAllyRequest( g: Game, owner: string, action: Action = { type: 'ship', territory: 'arrakeen', sector: 10, amount: 5, }, ): Action { return { ...action, nexus: viewGame(g, owner).nexusGuildSecretAlly!.event }; } export function nexusGuildSecretAllyInventory(g: Game) { const census = structuredClone(g); for (const p of census.players) p.reserves += Object.values(g.homeworlds?.custody?.visitors ?? {}).reduce( (sum, visitors) => sum + (visitors[p.id]?.normal ?? 0) + (visitors[p.id]?.elite ?? 0), 0, ); nexusTraitorInventory(census); if (g.homeworlds) homeworldGameIntegrity(g); } export const nexusGuildSecretAllyReload = nexusReload; export const holdGuildSecretAllyCard = holdNexusRicheseCard; export function nexusGuildSecretAllianceFixture( seatIds?: [string, string, string], ) { const f = nexusGuildSecretAllyFixture({ seatIds, opponentFaction: 'moritani', }); let g = f.g; while (g.phase === 5) g = applyAction(g, g.active!, { type: 'endMovement' }); assert.equal(g.phase, 7); for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' }); assert.equal(g.decision?.kind, 'moritaniPlacement'); const token = g.moritaniTerror!.tokens.find((t) => t.kind === 'robbery')!; g = nexusAllow( applyAction(g, f.target, { type: 'decision', token: token.id, territory: 'arrakeen', }), ); let ordered = false; for ( let step = 0; !( g.turn === 2 && g.phase === 5 && !g.response && !g.decision && !g.phaseOpening ) && step < 180; step++ ) { if (g.turn === 2 && g.phase === 1 && !ordered) { orderNexusSpice(g, ['land', 'land']); ordered = true; } let next: Game | undefined; for (const p of g.players) { const v = viewGame(g, p.id); v.players.find((s) => s.id === p.id)!.bot = 'Easy'; const a = botActions(v)[0]; if (a) { next = applyAction(g, p.id, a); break; } } assert.ok( next, `Secret Ally alliance progression stalled at ${g.turn}/${g.phase}/${g.decision?.kind}`, ); g = next; } assert.equal(g.turn, 2); assert.equal(g.phase, 5); while (g.active !== f.owner) g = applyAction(g, g.active!, { type: 'endMovement' }); assert.equal(g.nexusCards!.cards!.hands[f.owner], 'guild'); nexusGuildSecretAllyInventory(g); return { ...f, g }; }