Files
Dune/tests/fixture-advanced-revival-order.ts

149 lines
9.3 KiB
TypeScript

import assert from 'node:assert/strict';
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import { actAdvisorPreparation, advisorPreparationInventory } from './fixture-advisor-preparation';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
export type AdvancedRevivalOrderStep = { actor: string; action: Action };
export type AdvancedRevivalOrderTransition = {
before: Game; steps: AdvancedRevivalOrderStep[]; after: Game;
};
export const actAdvancedRevivalOrder = actAdvisorPreparation;
export const advancedRevivalOrderInventory = advisorPreparationInventory;
export const advancedRevivalOrderPlayer = (game: Game, actor: string) => {
const player = game.players.find(player => player.id === actor);
assert.ok(player, `Missing original Revival seat ${actor}`); return player;
};
export const advancedRevivalOrderClean = (game: Game) => !game.phaseOpening && !game.response &&
!game.decision && !game.pendingRevival && !game.truthtrance && !game.pendingTreacheryDiscard;
export function advancedRevivalOrderStocks(game: Game) {
return Object.fromEntries(game.players.map(player => [player.id, {
reserves: player.reserves, tanks: player.tanks, forces: structuredClone(player.forces),
spice: player.spice, revived: player.revived,
freeForcesRevived: player.freeForcesRevived, leaderRevived: player.leaderRevived,
elites: structuredClone(player.elites),
}]));
}
/** Actual native decisions only. Ordered Revival finishes the current seat;
* an unstamped Basic/legacy phase retains its original all-seat readiness. */
export function nextAdvancedRevivalOrder(game: Game): AdvancedRevivalOrderStep | null {
if (advancedRevivalOrderClean(game) && game.status === 'playing') {
if (game.phase === 0 && game.stormPending === null) {
const actor = game.stormDialers.find(id => game.stormDials[id] === undefined);
if (actor) return { actor, action: { type: 'stormDial', amount: game.turn === 1
? game.stormDialers.indexOf(actor) === 0 ? game.advanced ? 11 : 10 : 0
: 1 } };
}
if (game.phase === 4 && game.revivalOrder) {
const actor = game.revivalOrder.remaining[0];
if (actor) return { actor, action: { type: 'ready' } };
}
}
if (!game.phaseOpening && !game.response && !game.pendingTreacheryDiscard) {
const decision = game.decision;
if (decision?.kind === 'revivalStop')
return { actor: decision.player, action: { type: 'decision', decline: true } };
if (decision?.kind === 'guildTiming')
return { actor: decision.player, action: { type: 'decision', take: false } };
}
return nextStrongholdFactionsNativeStep(game);
}
export function advanceAdvancedRevivalOrder(game: Game, until: (state: Game) => boolean,
actions: AdvancedRevivalOrderStep[] = []): Game {
for (let count = 0; count < 1500; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished', 'The genuine Revival source ended before its next control');
const step = nextAdvancedRevivalOrder(game);
assert.ok(step, `Choose the original control at ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind}`);
game = actAdvancedRevivalOrder(game, step.actor, step.action, actions);
}
throw Error('The genuine Revival source did not reach its finite boundary');
}
export function passAdvancedRevivalOrder(game: Game, actions: AdvancedRevivalOrderStep[] = []): Game {
for (let count = 0; game.response && count < 60; count++) {
const actor = game.players.find(player => !game.response!.passed.includes(player.id)); assert.ok(actor);
game = actAdvancedRevivalOrder(game, actor.id, { type: 'passResponse' }, actions);
}
assert.ok(!game.response); return game;
}
export function finishAdvancedRevivalOrder(game: Game, actions: AdvancedRevivalOrderStep[] = []): Game {
return advanceAdvancedRevivalOrder(game, state => state.phase === 5 && advancedRevivalOrderClean(state), actions);
}
export function reachAdvancedRevivalOrderActor(game: Game, actor: string,
actions: AdvancedRevivalOrderStep[] = []): Game {
return advanceAdvancedRevivalOrder(game, state => state.phase === 4 && advancedRevivalOrderClean(state) &&
(!state.revivalOrder || state.revivalOrder.remaining[0] === actor), actions);
}
/** Genuine public E/A/T + original Ix-deck start. The first source stops at
* turn-one Revival. casualty:true additionally earns a paid Emperor normal1
* shipment to Hagga13 and its next native Storm death before turn-two Revival.
* Only unopened original Spice positions and scalar lotteries are selected.
* No played hand, force, wallet, Tanks, phase, active seat or queue is installed.
* The supplied Advanced physical9 orders Force Revival; the single native
* force/leader opportunity used by this slice is explicitly composed policy. */
export function createAdvancedRevivalOrderFixture(options: { advanced?: boolean; casualty?: boolean } = {}) {
const advanced = options.advanced ?? true, casualty = options.casualty ?? false;
const owner = 'emperor', responder = 'tleilaxu', casualtyLocation = 'hagga_basin:13';
let game = createGame('ADVANCEDREVIVALORDER', newPlayer(owner, 'Emperor', 'emperor'), advanced, ['ix']);
joinGame(game, newPlayer('atreides', 'Atreides', 'atreides'));
joinGame(game, newPlayer(responder, 'Tleilaxu', 'tleilaxu'));
const initial = structuredClone(game), actions: AdvancedRevivalOrderStep[] = [];
const act = (actor: string, action: Action) => { game = actAdvancedRevivalOrder(game, actor, action, actions); };
const reach = (until: (state: Game) => boolean) => { game = advanceAdvancedRevivalOrder(game, until, actions); };
const transition = (run: () => void): AdvancedRevivalOrderTransition => {
const before = structuredClone(game), start = actions.length; run();
return { before, steps: structuredClone(actions.slice(start)), after: structuredClone(game) };
};
for (const player of game.players) act(player.id, { type: 'ready' });
const lobby = structuredClone(game);
act(game.host, advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
const setup = structuredClone(game);
assert.equal(game.status, 'setup'); assert.ok(game.players.every(player => !player.hand.length));
assert.ok(!game.homeworlds && !game.leaderSkills && !game.nexusCards && !game.techTokens &&
!game.strongholdCards && !game.discoveryEnabled);
const originalInventory = advancedRevivalOrderInventory(game);
reach(state => state.status === 'playing');
const afterSetup = structuredClone(game);
const originalSpice = game.spiceDeck.map(card => JSON.stringify(card)).sort();
// Four ordinary original blows avoid an incidental Nexus/worm programme.
const spiceOrder = [0, 1, 2, 3].map(position => {
const from = game.spiceDeck.findIndex((card, index) => index >= position && 'territory' in card && !card.discovery);
assert.ok(from >= position); const card = game.spiceDeck.splice(from, 1)[0];
game.spiceDeck.splice(position, 0, card); return { position, from, card: structuredClone(card) };
});
assert.deepEqual(game.spiceDeck.map(card => JSON.stringify(card)).sort(), originalSpice);
reach(state => state.turn === 1 && state.phase === 4 && advancedRevivalOrderClean(state));
const firstRevival = structuredClone(game);
let shipment: AdvancedRevivalOrderTransition | null = null, storm: AdvancedRevivalOrderTransition | null = null;
if (casualty) {
reach(state => state.turn === 1 && state.phase === 5 && state.active === owner && advancedRevivalOrderClean(state));
shipment = transition(() => {
act(owner, { type: 'ship', territory: 'hagga_basin', sector: 13, amount: 1, elite: 0, allyPayment: 0 });
reach(advancedRevivalOrderClean);
});
assert.equal(advancedRevivalOrderPlayer(game, owner).forces[casualtyLocation], 1);
assert.equal(advancedRevivalOrderPlayer(shipment.before, owner).spice - advancedRevivalOrderPlayer(game, owner).spice, 2);
reach(state => state.turn === 2 && state.phase === 0 && advancedRevivalOrderClean(state));
if (advanced) {
assert.ok(game.stormPending !== null && game.stormPending >= 1 && game.stormPending <= 6);
assert.equal(game.stormMovementSource?.kind, 'card');
assert.equal(game.stormMovementSource?.turn, 2);
assert.equal(game.stormMovementSource?.distance, game.stormPending);
} else assert.equal(game.stormPending, null);
storm = transition(() => { reach(state => state.turn === 2 && state.phase === 1); });
assert.equal(advancedRevivalOrderPlayer(game, owner).forces[casualtyLocation], undefined);
assert.equal(advancedRevivalOrderPlayer(game, owner).tanks, 1);
reach(state => state.turn === 2 && state.phase === 4 && advancedRevivalOrderClean(state));
}
assert.deepEqual(advancedRevivalOrderInventory(game), originalInventory);
const actor = game.revivalOrder?.remaining[0] ?? game.order[0];
const outOfTurn = game.players.find(player => player.id !== actor)!.id;
return { game, sourceGame: structuredClone(game), owner, responder, actor, outOfTurn,
originalOrder: [...game.order], initial, lobby, setup, afterSetup, firstRevival,
casualtyLocation, revivalAction: { type: 'revive', amount: 1, elite: 0 } as Action,
shipment, storm, spiceOrder, originalInventory, originalSpice,
actions, sourceActions: structuredClone(actions), sourceActionCount: actions.length,
staging: [] as string[] };
}