import assert from 'node:assert/strict'; import { viewGame, type Action, type Game } from '../game/engine'; import { actTruthTraitorDecision, advanceTruthTraitorDecision, createTruthTraitorDecisionFixture, truthTraitorDecisionClean, truthTraitorDecisionInventory, truthTraitorDecisionPlayer, type TruthTraitorDecisionStep, } from './fixture-truth-traitor-decision'; export const actFutureBattleLeader = actTruthTraitorDecision; export const advanceFutureBattleLeader = advanceTruthTraitorDecision; export const futureBattleLeaderClean = truthTraitorDecisionClean; export const futureBattleLeaderInventory = truthTraitorDecisionInventory; export const futureBattleLeaderPlayer = truthTraitorDecisionPlayer; export type FutureBattleLeaderStep = TruthTraitorDecisionStep; export type FutureBattleLeaderRecord = { turn: number; asker: string; player: string; leader: string; answer: boolean; battle?: string; status: 'waiting' | 'bound' | 'settled' | 'expired' | 'released'; }; export function futureBattleLeaderRecords(game: Game): FutureBattleLeaderRecord[] { return (game as Game & { futureBattleLeaderPromises?: FutureBattleLeaderRecord[] }).futureBattleLeaderPromises ?? []; } /** Reuse the source's genuinely earned Basic Movement boundary. Only its * original unopened card/spice order and existing scalar lotteries were * selected; no battle, private plan or answer is installed. */ export function createFutureBattleLeaderFixture() { const base = createTruthTraitorDecisionFixture(); const game = structuredClone(base.beforeShipment); assert.equal(game.turn, 1); assert.equal(game.phase, 5); assert.equal(game.active, base.target); assert.equal(game.advanced, false); assert.deepEqual(game.expansions, []); assert.ok(!game.battle && futureBattleLeaderClean(game)); assert.ok(futureBattleLeaderPlayer(game, base.asker).hand.some(card => card.id === base.truth)); const leader = base.leaders.emperor; const otherLeader = futureBattleLeaderPlayer(game, base.target).leaders.find(item => item.id !== leader && !item.dead && !item.usedAt); assert.ok(otherLeader); assert.deepEqual(futureBattleLeaderInventory(game), base.originalCards); const question = { kind: 'futureBattleLeader', target: base.target, turn: game.turn, leader }; const ask = { type: 'truthAsk', question } as unknown as Action; return { base, game, source: structuredClone(game), question, ask, leader, otherLeader: otherLeader.id, asker: base.asker, target: base.target, truth: base.truth, originalCards: base.originalCards, targetPlan: base.targetPlan, ownPlan: base.ownPlan, territory: base.territory, shipmentStep: base.shipmentStep }; } export type FutureBattleLeaderFixture = ReturnType; export function declareFutureBattleLeader(fixture: FutureBattleLeaderFixture): Game { let game = actFutureBattleLeader(fixture.game, fixture.asker, { type: 'card', card: fixture.truth }); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const player = game.players.find(seat => !game.truthtrance!.passed.includes(seat.id)); assert.ok(player); game = actFutureBattleLeader(game, player.id, { type: 'truthPass' }); } assert.equal(game.truthtrance?.stage, 'ask'); return actFutureBattleLeader(game, fixture.asker, fixture.ask); } export function answerFutureBattleLeader(game: Game, fixture: FutureBattleLeaderFixture, answer: 'yes' | 'no'): Game { return actFutureBattleLeader(game, fixture.target, { type: 'truthAnswer', answer }); } /** Use the source's actual paid shipment, then the native choice and * preparation controls; the caller alone seals plans and traitor votes. */ export function reachFutureBattle(game: Game, fixture: FutureBattleLeaderFixture): Game { game = actFutureBattleLeader(game, fixture.shipmentStep.actor, fixture.shipmentStep.action); game = advanceFutureBattleLeader(game, futureBattleLeaderClean); game = actFutureBattleLeader(game, fixture.target, { type: 'endMovement' }); game = advanceFutureBattleLeader(game, state => state.phase === 6 && futureBattleLeaderClean(state) && !!state.active && !state.battle); const chooser = game.active!; const choice = viewGame(game, chooser).battleChoices.find(row => row.territory === fixture.territory && [row.attacker, row.defender].includes(fixture.target)); assert.ok(choice); game = actFutureBattleLeader(game, choice.chooser, { type: 'chooseBattle', territory: choice.territory, target: choice.attacker === choice.chooser ? choice.defender : choice.attacker }); assert.ok(game.battle?.event); return advanceFutureBattleLeader(game, state => !!state.battle && futureBattleLeaderClean(state) && !state.battle.preparation && state.battle.preLeader?.closed !== false); }