import assert from 'node:assert/strict'; import { TERRITORIES } from '../game/board'; import { viewGame, type Action, type Game } from '../game/engine'; import { actTruthRevival, createTruthRevivalFixture, nextTruthRevival, truthRevivalClean, truthRevivalInventory, truthRevivalPlayer, } from './fixture-truth-revival'; import { futureBattleLeaderRecords } from './fixture-truth-future-battle-leader'; export const actFutureBattleRevival = actTruthRevival; export const futureBattleRevivalInventory = truthRevivalInventory; export const futureBattleRevivalRecords = futureBattleLeaderRecords; /** The source has real Basic turn-two storm casualties and has reached quiet * Revival, with one original Truthtrance still in Harkonnen's hand. */ export function createFutureBattleRevivalFixture() { const base = createTruthRevivalFixture(); const game = structuredClone(base.game); assert.equal(game.phase, 4); assert.equal(game.advanced, false); assert.deepEqual(game.expansions, []); assert.ok(truthRevivalClean(game)); const target = truthRevivalPlayer(game, base.target); const available = target.leaders.filter(leader => !leader.dead && !leader.capturedBy && !leader.gholaBy && !leader.usedAt); assert.ok(available.length >= 2); const leader = available[0].id, alternative = available[1].id; const question = {kind: 'futureBattleLeader', target: base.target, turn: game.turn, leader}; return { base, game, source: structuredClone(game), asker: base.asker, target: base.target, truth: base.truth, leader, alternative, question, originalCards: base.originalCards, ask: {type: 'truthAsk', question} as unknown as Action }; } export type FutureBattleRevivalFixture = ReturnType; export function askFutureBattleRevival(f: FutureBattleRevivalFixture): Game { let game = actFutureBattleRevival(f.game, f.asker, {type: 'card', card: f.truth}); for (let count = 0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const seat = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(seat); game = actFutureBattleRevival(game, seat.id, {type: 'truthPass'}); } assert.equal(game.truthtrance?.stage, 'ask'); return actFutureBattleRevival(game, f.asker, f.ask); } /** Finish Revival and skip unrelated Movement opportunities, preserving the * target's own turn for its explicit decision or no-battle expiry. */ export function reachFutureBattleRevivalMovement(game: Game, f: FutureBattleRevivalFixture): Game { for (let count = 0; count < 800; count++) { if (game.phase === 5 && game.active === f.target && truthRevivalClean(game)) return game; if (game.phase === 5 && game.active && truthRevivalClean(game)) { game = actFutureBattleRevival(game, game.active, {type: 'endMovement'}); continue; } const next = nextTruthRevival(game); assert.ok(next, 'Finish the original native Revival opportunity.'); game = actFutureBattleRevival(game, next.actor, next.action); } throw Error('The original Revival source did not reach the respondent Movement turn.'); } /** Make a real one-force shipment into an existing unallied opponent group, * then use ordinary battle-selection and preparation actions. */ export function reachFutureBattleRevivalBattle(game: Game, f: FutureBattleRevivalFixture): Game { const target = truthRevivalPlayer(game, f.target); const location = game.players.filter(player => player.id !== f.target && player.id !== target.ally) .flatMap(player => Object.entries(player.forces)).find(([key, count]) => { const separator = key.lastIndexOf(':'), name = key.slice(0, separator), sector = Number(key.slice(separator + 1)); return count > 0 && sector !== game.storm && TERRITORIES.some(territory => territory.id === name && territory.sectors.includes(sector)); }); assert.ok(location, 'The native source retains an eligible opponent group.'); const separator = location[0].lastIndexOf(':'), territory = location[0].slice(0, separator); const sector = Number(location[0].slice(separator + 1)); game = actFutureBattleRevival(game, f.target, {type: 'ship', territory, sector, amount: 1, elite: 0, allyPayment: 0}); for (let count = 0; count < 800; count++) { if (game.phase === 5 && game.active === f.target && truthRevivalClean(game)) { game = actFutureBattleRevival(game, f.target, {type: 'endMovement'}); continue; } if (game.phase === 5 && game.active && truthRevivalClean(game)) { game = actFutureBattleRevival(game, game.active, {type: 'endMovement'}); continue; } if (game.phase === 6 && game.battle && [game.battle.attacker, game.battle.defender].includes(f.target) && truthRevivalClean(game) && !game.battle.preparation && game.battle.preLeader?.closed !== false) return game; if (game.phase === 6 && !game.battle && game.active && truthRevivalClean(game)) { const choice = viewGame(game, game.active).battleChoices.find(row => row.territory === territory && [row.attacker, row.defender].includes(f.target)); if (choice) { game = actFutureBattleRevival(game, choice.chooser, {type: 'chooseBattle', territory: choice.territory, target: choice.attacker === choice.chooser ? choice.defender : choice.attacker}); continue; } } const next = nextTruthRevival(game); assert.ok(next, 'Finish the original native battle preparation.'); game = actFutureBattleRevival(game, next.actor, next.action); } throw Error('The native source did not reach the target’s first battle.'); }