import assert from 'node:assert/strict'; import { applyAction, viewGame, type Game } from '../game/engine'; import { acquireDuke } from '../game/duke-vidal'; import { basicEcazNativeFixture, nextBasicEcazNativeStep } from './fixture-basic-ecaz-native'; import { basicEcazOccupyBattleAction, basicEcazOccupyFixture } from './fixture-basic-ecaz-occupy'; import { leaderlessSeat, stageLeaderlessCard } from './fixture-basic-leaderless-harkonnen'; export type OccupyLeaderlessOptions = { lead: 'ecaz' | 'ally'; dead: 'selected' | 'opponent' | 'unselected' | 'none'; duke?: boolean; cancel?: boolean; }; /** Public Basic selected-deck setup and original alliance/coalition admission. * The shared fixture stages conserved board pieces and wallets. This fixture * stages native public leader history and conserves the physical hands. */ export function basicNativeLeaderlessOccupy(options: OccupyLeaderlessOptions) { const fixture = basicEcazNativeFixture({ allyFaction: 'choam', opponentFaction: 'guild' }); let game = structuredClone(fixture.beforeBattle); const selected = options.lead === 'ecaz' ? fixture.ecaz : fixture.ally; const unselected = options.lead === 'ecaz' ? fixture.ally : fixture.ecaz; const dead = options.dead === 'selected' ? selected : options.dead === 'opponent' ? fixture.opponent : options.dead === 'unselected' ? unselected : null; for (const actor of [fixture.ecaz, fixture.ally, fixture.opponent]) { stageLeaderlessCard(game, actor, 'none'); for (const leader of leaderlessSeat(game, actor).leaders) { leader.dead = actor === dead; leader.deaths = actor === dead ? Math.max(1, leader.deaths) : leader.deaths; if (actor !== dead) leader.usedAt = 'arrakeen'; else delete leader.usedAt; } } if (options.duke) { assert.ok(game.dukeVidal); game.dukeVidal = acquireDuke(game.dukeVidal, fixture.ecaz, game.turn, 'ecaz'); } const choice = viewGame(game, game.active!).battleChoices.find(row => row.territory === fixture.territory); assert.ok(choice); game = applyAction(game, choice.chooser, { type: 'chooseBattle', territory: fixture.territory, target: choice.attacker === choice.chooser ? choice.defender : choice.attacker }); assert.equal(game.decision?.kind, 'ecazBattleLead'); const event = game.decision.event; game = applyAction(game, game.decision.player, { type: 'decision', event, lead: selected }); if (options.cancel) { const card = leaderlessSeat(game, fixture.opponent).hand.find(c => c.effect === 'karama'); assert.ok(card); game = applyAction(game, fixture.opponent, { type: 'card', mode: 'cancel', card: card.id }); } for (let attempts = 0; attempts < 100 && (game.pendingTreacheryDiscard || game.response || game.phaseOpening || game.decision); attempts++) { const next = nextBasicEcazNativeStep(game); assert.ok(next); game = applyAction(game, next.actor, next.action); } assert.ok(game.battle && !game.pendingTreacheryDiscard && !game.response && !game.phaseOpening && !game.decision); return { fixture, game, selected, unselected, dead, event }; } /** Resolve existing Richese/shared preparation without submitting battle plans. */ export function finishOccupyLeaderlessPreparation(state: Game): Game { let game = state; for (let attempts = 0; attempts < 100; attempts++) { if (game.battle && !game.battle.preparation && (!game.battle.preLeader || game.battle.preLeader.closed) && !game.pendingTreacheryDiscard && !game.response && !game.phaseOpening && !game.decision) return game; const next = nextBasicEcazNativeStep(game); assert.ok(next); game = applyAction(game, next.actor, next.action); } throw new Error('Native Occupy leader preparation did not finish'); } /** Supplemental audit-entry fixture without Richese: original setup, worm * alliance and shipments, plus labelled conserved rival and leader staging. */ export function basicOccupyNoRicheseLeaderless(duke = false) { const fixture = basicEcazOccupyFixture({ chooseBattle: false }); let game = structuredClone(fixture.game); assert.ok(!game.players.some(player => player.faction === 'richese')); stageLeaderlessCard(game, fixture.ecaz, 'none'); for (const leader of leaderlessSeat(game, fixture.ecaz).leaders) { leader.dead = true; leader.deaths = Math.max(1, leader.deaths); delete leader.usedAt; } if (duke) { assert.ok(game.dukeVidal); game.dukeVidal = acquireDuke(game.dukeVidal, fixture.ecaz, game.turn, 'ecaz'); } const chosen = basicEcazOccupyBattleAction({ ...fixture, game }); game = applyAction(game, chosen.actor, chosen.action); assert.equal(game.decision?.kind, 'ecazBattleLead'); game = applyAction(game, game.decision.player, { type: 'decision', event: game.decision.event, lead: fixture.ecaz }); for (let attempts = 0; attempts < 100 && (game.pendingTreacheryDiscard || game.response || game.decision); attempts++) { const next = nextBasicEcazNativeStep(game); assert.ok(next); game = applyAction(game, next.actor, next.action); } assert.ok(game.battle && !game.pendingTreacheryDiscard && !game.response && !game.decision); return { fixture, game }; }