Files
Dune/tests/fixture-short-alliance-victory.ts
Quintine e896570f07
Verify and publish checkpoint container / container (push) Successful in 11m2s
Add optional short alliance victory
2026-10-28 21:40:24 +11:00

97 lines
4.7 KiB
TypeScript

import assert from 'node:assert/strict';
import { territory } from '../game/board';
import { botActions } from '../game/bots';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
export type ShortAllianceStep = { actor: string; action: Action };
export function shortAllianceLobby(): Game {
const game = createGame('SHORTALLIANCE', newPlayer('at', 'Atreides', 'atreides'));
joinGame(game, newPlayer('em', 'Emperor', 'emperor'));
joinGame(game, newPlayer('gu', 'Guild', 'guild'));
return game;
}
/** Only real owned controls advance setup and the first turn. Quiet Movement
* is deliberately ended so the fixture cannot win before the real Nexus. */
function nextStep(game: Game): ShortAllianceStep | null {
for (const player of game.players) {
const view = viewGame(game, player.id);
view.players.find(seat => seat.id === player.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (!action) continue;
if (game.status === 'playing' && game.phase === 5 && game.active === player.id &&
!game.response && !game.decision && !game.truthtrance && !game.phaseOpening)
return { actor: player.id, action: { type: 'endMovement' } };
return { actor: player.id, action };
}
return null;
}
export function advanceShortAlliance(game: Game, predicate: (game: Game) => boolean,
actions: ShortAllianceStep[], maximum = 1200): Game {
for (let count = 0; count < maximum; count++) {
if (predicate(game)) return game;
assert.notEqual(game.status, 'finished', 'The native game ended before the requested boundary.');
const step = nextStep(game);
assert.ok(step, `No native continuation at ${game.status}/${game.turn}/${game.phase}.`);
actions.push(structuredClone(step));
game = applyAction(game, step.actor, step.action);
}
throw Error('The original game did not reach the requested alliance boundary.');
}
/** Genuine Basic setup and genuine Nexus/alliance; only the unopened original
* Spice deck order is selected to make the next normal worm deterministic. */
export function genuineShortAlliance(enabled: boolean) {
let game = shortAllianceLobby();
const actions: ShortAllianceStep[] = [];
const act = (actor: string, action: Action) => {
actions.push({ actor, action });
game = applyAction(game, actor, action);
};
if (enabled) act('at', { type: 'shortAllianceVictory', enabled: true });
for (const player of game.players) act(player.id, { type: 'ready' });
act('at', { type: 'start' });
game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions);
const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
assert.ok(worm >= 0, 'The original unopened deck retains an ordinary worm.');
const beforeDeck = game.spiceDeck.map(card => JSON.stringify(card)).sort();
game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]);
assert.deepEqual(game.spiceDeck.map(card => JSON.stringify(card)).sort(), beforeDeck);
game = advanceShortAlliance(game, state => state.nexus && !state.spiceWindow && !state.spiceResolution &&
!state.response && !state.decision && !state.truthtrance && !state.phaseOpening, actions);
act('at', { type: 'alliance', target: 'em' });
act('em', { type: 'alliance', target: 'at' });
assert.equal(game.players.find(player => player.id === 'at')!.ally, 'em');
assert.equal(game.players.find(player => player.id === 'em')!.ally, 'at');
game = advanceShortAlliance(game, state => !state.nexus && state.phase >= 2, actions);
return { game, actions };
}
/** The setup and alliance above are real. For the rare three-stronghold
* victory boundary, this arranges conserved forces and the Collection phase;
* the actual final Collection readiness and Mentat win remain native actions. */
export function shortAllianceCollection(source: Game): Game {
const game = structuredClone(source);
for (const player of game.players) {
player.reserves += Object.values(player.forces).reduce((sum, count) => sum + count, 0);
player.forces = {};
if (player.elites) {
player.elites.reserves += Object.values(player.elites.forces).reduce((sum, count) => sum + count, 0);
player.elites.forces = {};
}
}
const at = game.players.find(player => player.id === 'at')!;
const em = game.players.find(player => player.id === 'em')!;
for (const id of ['arrakeen', 'carthag']) {
at.forces[`${id}:${territory(id).sectors[0]}`] = 1;
at.reserves--;
}
em.forces[`tueks_sietch:${territory('tueks_sietch').sectors[0]}`] = 1;
em.reserves--;
Object.assign(game, { phase: 7, ready: [], active: null, storm: 18,
response: null, decision: null, truthtrance: null, phaseOpening: null });
return game;
}