92 lines
5.5 KiB
TypeScript
92 lines
5.5 KiB
TypeScript
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<typeof createFutureBattleRevivalFixture>;
|
||
|
||
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.');
|
||
}
|