97 lines
4.7 KiB
TypeScript
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;
|
|
}
|