95 lines
5.8 KiB
TypeScript
95 lines
5.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { viewGame, type Action, type Game } from '../game/engine';
|
|
import { TERRITORIES, territory } from '../game/board';
|
|
import { baseDeck } from '../game/cards';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { drawClassicNexusReaction, reactionAction, reactionPlayer } from './fixture-classic-independent-nexus-reactions';
|
|
import { advanceClassicIndependentNexus, type ClassicIndependentNexusOptions, type ClassicIndependentNexusStep } from './fixture-classic-independent-nexus';
|
|
import { classicDiscoveryNexusClean } from './fixture-discovery-classic-nexus';
|
|
|
|
export type ClassicNexusCoreOptions = Pick<ClassicIndependentNexusOptions, 'advanced' | 'skills' | 'independent' | 'training' | 'tech' | 'strongholds' | 'homeworlds'>;
|
|
function drawSecondFace(game: Game, owner: string, face: FactionId, actions: ClassicIndependentNexusStep[]) {
|
|
assert.equal(game.nexusCards!.phase!.stage, 'drawing');
|
|
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf(face);
|
|
assert.ok(index >= 0);
|
|
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
|
|
return reactionAction(game, owner, { type: 'nexusCardChoice', turn: game.turn,
|
|
card: cards.hands[owner], choice: 'draw', ownRedraws: 0 }, actions);
|
|
}
|
|
|
|
/** Two distinct original cards: A holds Richese (or an irrelevant Harkonnen),
|
|
* Emperor holds Guild, and actual Guild/Harkonnen are allied. */
|
|
export function createClassicNexusGuildPayment(options: ClassicNexusCoreOptions & { discounted?: boolean } = {}) {
|
|
const discounted = options.discounted ?? true;
|
|
const fixture = drawClassicNexusReaction(discounted ? 'richese' : 'harkonnen', 'atreides', {
|
|
...options, independent: options.independent ?? false,
|
|
roster: ['atreides', 'guild', 'harkonnen', 'emperor'],
|
|
training: options.training ?? (options.skills ? { atreides: 'bureaucrat' } : undefined),
|
|
});
|
|
const actions = fixture.actions;
|
|
let game = drawSecondFace(fixture.game, 'emperor', 'guild', actions);
|
|
const afterDraws = structuredClone(game);
|
|
game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'atreides' &&
|
|
classicDiscoveryNexusClean(state), actions);
|
|
const beforeShipment = structuredClone(game), place = territory('carthag');
|
|
assert.ok(!place.sectors.includes(game.storm));
|
|
const amount = 5;
|
|
const action: Action = { type: 'ship', territory: place.id, sector: place.sectors[0],
|
|
amount, elite: 0, allyPayment: 0 };
|
|
if (discounted) {
|
|
const offer = viewGame(game, 'atreides').nexusRichese!;
|
|
assert.equal(offer.blocked, null);
|
|
action.nexus = offer.event;
|
|
}
|
|
game = reactionAction(game, 'atreides', action, actions);
|
|
game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
|
|
classicDiscoveryNexusClean(state), actions);
|
|
return { ...fixture, game, afterDraws, beforeShipment, actions, action, amount,
|
|
key: `${place.id}:${place.sectors[0]}`, holder: 'emperor' };
|
|
}
|
|
|
|
/** A/H form the real alliance; native Guild and Emperor draw distinct faces.
|
|
* The latter holds Harkonnen, never a second copy of Guild's singleton. */
|
|
export function createClassicNexusCunning(options: ClassicNexusCoreOptions = {}) {
|
|
const counter = baseDeck().find(card => card.effect === 'karama')!.id;
|
|
const fixture = drawClassicNexusReaction('guild', 'guild', { ...options,
|
|
independent: options.independent ?? false, roster: ['atreides', 'harkonnen', 'guild', 'emperor'],
|
|
startingCards: { atreides: [counter] },
|
|
});
|
|
const actions = fixture.actions;
|
|
let game = drawSecondFace(fixture.game, 'emperor', 'harkonnen', actions);
|
|
game = advanceClassicIndependentNexus(game, state => state.phase === 5 && state.active === 'guild' &&
|
|
classicDiscoveryNexusClean(state), actions);
|
|
const empty = TERRITORIES.filter(place => place.type === 'stronghold' &&
|
|
!place.sectors.includes(game.storm) && game.players.every(player =>
|
|
Object.keys(player.forces).every(key => !key.startsWith(`${place.id}:`))));
|
|
assert.ok(empty.length >= 2);
|
|
const first = empty[0], second = empty[1], origin = `${first.id}:${first.sectors[0]}`;
|
|
const beforeFirst = structuredClone(game);
|
|
game = reactionAction(game, 'guild', { type: 'ship', territory: first.id, sector: first.sectors[0],
|
|
amount: 1, elite: 0, allyPayment: 0 }, actions);
|
|
game = advanceClassicIndependentNexus(game, state => reactionPlayer(state, 'guild').shipped &&
|
|
!state.pendingGuildBetrayal && classicDiscoveryNexusClean(state), actions);
|
|
const beforeCunning = structuredClone(game), offer = viewGame(game, 'guild').nexusGuildCunning!.offer!;
|
|
assert.equal(offer.blocked, null);
|
|
game = reactionAction(game, 'guild', { type: 'endMovement', nexus: offer.event }, actions);
|
|
return { ...fixture, game, beforeFirst, beforeCunning, actions, counter, origin,
|
|
second: second.id, secondSector: second.sectors[0] };
|
|
}
|
|
|
|
export function submitClassicNexusSecondShipment(fixture: ReturnType<typeof createClassicNexusCunning>,
|
|
kind: 'reserve' | 'cross' | 'return') {
|
|
let game = advanceClassicIndependentNexus(fixture.game, state =>
|
|
state.nexusGuildCunningHistory?.at(-1)?.stage === 'secondShipment' && classicDiscoveryNexusClean(state), fixture.actions);
|
|
const beforeSecond = structuredClone(game);
|
|
const action: Action = kind === 'reserve'
|
|
? { type: 'ship', territory: fixture.second, sector: fixture.secondSector, amount: 1, elite: 0, allyPayment: 0 }
|
|
: { type: 'guildShip', from: fixture.origin,
|
|
territory: kind === 'return' ? 'reserves' : fixture.second,
|
|
...(kind === 'return' ? {} : { sector: fixture.secondSector }), amount: 1, elite: 0, allyPayment: 0 };
|
|
game = reactionAction(game, 'guild', action, fixture.actions);
|
|
game = advanceClassicIndependentNexus(game, state => !!state.pendingGuildBetrayal &&
|
|
classicDiscoveryNexusClean(state), fixture.actions);
|
|
return { ...fixture, game, beforeSecond, action, kind };
|
|
}
|