103 lines
6.7 KiB
TypeScript
103 lines
6.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { treacheryDeck } from '../game/cards';
|
|
import { tleilaxuSecretAllyOptions } from '../game/nexus-tleilaxu-revival';
|
|
import { createClassicIndependentNexusFixture, advanceClassicIndependentNexus, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
|
|
import { applyClassicDiscoveryNexusStep, classicDiscoveryNexusClean, nextClassicDiscoveryNexusStep } from './fixture-discovery-classic-nexus';
|
|
|
|
export const utilityPlayer = (game: Game, id = 'atreides') => {
|
|
const player = game.players.find(seat => seat.id === id);
|
|
assert.ok(player);
|
|
return player;
|
|
};
|
|
export function utilityAction(game: Game, actor: string, action: Action, actions?: ClassicIndependentNexusStep[]) {
|
|
return applyClassicDiscoveryNexusStep(game, { actor, action }, actions);
|
|
}
|
|
|
|
/** Reorder only the unopened physical Spice and Nexus singletons. Every hand,
|
|
* death, wallet, alliance and phase below is produced by an original action. */
|
|
function drawUtilityNexus(game: Game, face: FactionId, actions: ClassicIndependentNexusStep[]) {
|
|
const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper);
|
|
assert.ok(worm >= 0);
|
|
game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]);
|
|
game = advanceClassicIndependentNexus(game, state => state.nexus && !state.spiceWindow &&
|
|
!state.spiceResolution && classicDiscoveryNexusClean(state), actions);
|
|
const others = game.players.filter(seat => seat.id !== 'atreides');
|
|
game = utilityAction(game, others[0].id, { type: 'alliance', target: others[1].id }, actions);
|
|
game = utilityAction(game, others[1].id, { type: 'alliance', target: others[0].id }, actions);
|
|
game = advanceClassicIndependentNexus(game, state => state.nexusCards?.phase?.stage === 'drawing', actions);
|
|
const cards = game.nexusCards!.cards!;
|
|
const index = cards.deck.indexOf(face);
|
|
assert.ok(index >= 0);
|
|
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
|
|
const beforeDraw = structuredClone(game);
|
|
game = utilityAction(game, 'atreides', { type: 'nexusCardChoice', turn: game.turn,
|
|
card: cards.hands.atreides, choice: 'draw', ownRedraws: 0 }, actions);
|
|
return { game, beforeDraw, afterDraw: structuredClone(game) };
|
|
}
|
|
|
|
export function createIndependentNexusRecruitsRevival() {
|
|
const weapon = treacheryDeck([]).find(card => card.kind === 'projectile')!;
|
|
const guildCard = treacheryDeck([]).find(card => card.kind === 'worthless')!;
|
|
const fixture = createClassicIndependentNexusFixture({ startingCards: {
|
|
atreides: ['ecaz-recruits'], guild: [guildCard.id], emperor: [weapon.id],
|
|
} });
|
|
const actions = fixture.actions;
|
|
let game = advanceClassicIndependentNexus(fixture.game, state => state.phase === 5 &&
|
|
state.active === 'emperor' && classicDiscoveryNexusClean(state), actions);
|
|
game = utilityAction(game, 'emperor', { type: 'ship', territory: 'arrakeen', sector: 10,
|
|
amount: 1, elite: 0, allyPayment: 0 }, actions);
|
|
game = advanceClassicIndependentNexus(game, state => state.phase === 6 && classicDiscoveryNexusClean(state), actions);
|
|
if (!game.battle) game = utilityAction(game, game.active!, { type: 'chooseBattle', territory: 'arrakeen',
|
|
target: game.active === 'atreides' ? 'emperor' : 'atreides' }, actions);
|
|
game = advanceClassicIndependentNexus(game, state => !!state.battle && !state.battle.revealed &&
|
|
classicDiscoveryNexusClean(state) && !nextClassicDiscoveryNexusStep(state), actions);
|
|
const leader = utilityPlayer(game).leaders.filter(disc => !disc.dead).sort((a, b) => a.strength - b.strength)[0].id;
|
|
const opponentLeader = utilityPlayer(game, 'emperor').leaders.filter(disc => !disc.dead).sort((a, b) => a.strength - b.strength)[0].id;
|
|
game = utilityAction(game, 'atreides', { type: 'battlePlan', dial: 8, support: 0,
|
|
leader, weapon: null, defense: null }, actions);
|
|
game = utilityAction(game, 'emperor', { type: 'battlePlan', dial: 0, support: 0,
|
|
leader: opponentLeader, weapon: weapon.id, defense: null }, actions);
|
|
game = advanceClassicIndependentNexus(game, state => state.turn === 2 && state.phase === 1 &&
|
|
classicDiscoveryNexusClean(state) && !state.spiceSequence && !state.spiceResolution && !state.spiceWindow && !state.nexus && !state.ready.length, actions);
|
|
const afterBattle = structuredClone(game);
|
|
const drawn = drawUtilityNexus(game, 'tleilaxu', actions);
|
|
game = advanceClassicIndependentNexus(drawn.game, state => state.phase === 4 && classicDiscoveryNexusClean(state), actions);
|
|
const offer = tleilaxuSecretAllyOptions(game, utilityPlayer(game));
|
|
const action: Action = { type: 'nexusTleilaxuRevival', event: offer.event, leader, amount: 5, elite: 0 };
|
|
return { ...fixture, ...drawn, game, afterBattle, leader, action, actions };
|
|
}
|
|
|
|
/** Separate Emperor-absent table: the independent face stays in the unused
|
|
* original deck until the native auction reserves and delivers that one card. */
|
|
export function createIndependentNexusBankPurchase() {
|
|
const fixture = createClassicIndependentNexusFixture({ roster: ['atreides', 'guild', 'harkonnen'] });
|
|
const actions = fixture.actions;
|
|
let game = advanceClassicIndependentNexus(fixture.game, state => state.turn === 2 && state.phase === 1 &&
|
|
classicDiscoveryNexusClean(state) && !state.spiceSequence && !state.spiceResolution && !state.spiceWindow && !state.nexus && !state.ready.length, actions);
|
|
const drawn = drawUtilityNexus(game, 'emperor', actions);
|
|
game = advanceClassicIndependentNexus(drawn.game, state => state.phase === 2 && classicDiscoveryNexusClean(state), actions);
|
|
const index = game.deck.findIndex(card => card.id === 'ecaz-reinforcements');
|
|
assert.ok(index >= 0);
|
|
game.deck.unshift(game.deck.splice(index, 1)[0]);
|
|
game = advanceClassicIndependentNexus(game, state => state.phase === 3 && !!state.auction && classicDiscoveryNexusClean(state), actions);
|
|
let beforeBid: Game | undefined;
|
|
for (let count = 0; game.decision?.kind !== 'auctionPayment' && count < 100; count++) {
|
|
if (classicDiscoveryNexusClean(game) && game.auction?.active === 'atreides' && game.auction.bid === 0) {
|
|
beforeBid = structuredClone(game);
|
|
game = utilityAction(game, 'atreides', { type: 'bid', amount: 2 }, actions);
|
|
}
|
|
else {
|
|
const step = nextClassicDiscoveryNexusStep(game); assert.ok(step);
|
|
game = applyClassicDiscoveryNexusStep(game, step, actions);
|
|
}
|
|
}
|
|
assert.equal(game.decision?.kind, 'auctionPayment');
|
|
assert.equal(game.decision.player, 'atreides');
|
|
const offer = viewGame(game, 'atreides').nexusEmperorSecretAlly!;
|
|
const action: Action = { type: 'nexusEmperorPurchase', event: offer.event };
|
|
assert.ok(beforeBid);
|
|
return { ...fixture, ...drawn, game, beforeBid, action, actions };
|
|
}
|