146 lines
6.9 KiB
TypeScript
146 lines
6.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
|
import { createBureaucratQualifyingSplitSetup, advanceBureaucratQualifyingSplit, type BureaucratQualifyingSplitStep } from './fixture-bureaucrat-qualifying-split';
|
|
export interface BureaucratQualifyingAuctionFixture {
|
|
game: Game;
|
|
setup: Game;
|
|
beforeBribe: Game;
|
|
bribe: Game;
|
|
afterFirstMentat: Game;
|
|
worm: Game;
|
|
alliance: Game;
|
|
sourceSetup: Game;
|
|
beforePledge: Game;
|
|
originalPledge: BureaucratQualifyingSplitStep;
|
|
pledged: Game;
|
|
beforeAction: Game;
|
|
action: Action;
|
|
payer: string;
|
|
donor: string;
|
|
trainer: string;
|
|
guild: string;
|
|
emperor: string;
|
|
payerShare: number;
|
|
donorShare: number;
|
|
amount: number;
|
|
counterCard?: string;
|
|
physical: string[];
|
|
reservedCard: string;
|
|
bonusCard?: string;
|
|
actions: BureaucratQualifyingSplitStep[];
|
|
staging: string[];
|
|
}
|
|
|
|
|
|
export const qualifyingAuctionPlayer = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
|
export function qualifyingAuctionPhysicalCards(game: Game): string[] {
|
|
return [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand),
|
|
...(game.auction?.cards.slice(game.auction.index + (game.currentAuctionSale?.origin === 'normal' ? 1 : 0)) ?? [])]
|
|
.map(card => card.id).sort();
|
|
}
|
|
|
|
/** Swap conserved physical cards, retaining the original hand size and auction
|
|
* quantity. Harkonnen's Karama makes the native winning payment a real choice;
|
|
* no purchase bonus is dealt until the original income continuation finishes. */
|
|
function positionKarama(game: Game, actor: string, staging: string[]): string {
|
|
const player = qualifyingAuctionPlayer(game, actor);
|
|
const held = player.hand.find(card => card.effect === 'karama');
|
|
if (held) return held.id;
|
|
const index = game.deck.findIndex(card => card.effect === 'karama');
|
|
assert.ok(index >= 0, 'An original undealt physical Karama must be available');
|
|
const outgoing = player.hand[0];
|
|
assert.ok(outgoing);
|
|
const incoming = game.deck[index];
|
|
game.deck[index] = outgoing;
|
|
player.hand[0] = incoming;
|
|
staging.push(`Swap original held ${outgoing.id} with original undealt ${incoming.id} for ${actor}; retain hand size and committed lots.`);
|
|
return incoming.id;
|
|
}
|
|
|
|
export function createBureaucratQualifyingAuctionFixture({
|
|
advanced = false, payerShare = 5, donorShare = 3, donor = 'f', counter = false, lastHandSpace = false,
|
|
}: { advanced?: boolean; payerShare?: number; donorShare?: number; donor?: 'f' | 'b' | 'e'; counter?: boolean; lastHandSpace?: boolean } = {}): BureaucratQualifyingAuctionFixture {
|
|
const native = createBureaucratQualifyingSplitSetup(advanced);
|
|
let game = native.game, alliance = native.alliance;
|
|
let actions = native.actions;
|
|
const staging = native.staging;
|
|
if (donor !== native.donor) {
|
|
// Branch at the actual worm, before either mutual alliance proposal.
|
|
game = structuredClone(native.worm);
|
|
actions = actions.slice(0, actions.findIndex(step => step.action.type === 'alliance'));
|
|
for (const [actor, target] of [[native.payer, donor], [donor, native.payer]]) {
|
|
const action: Action = { type: 'alliance', target };
|
|
actions.push({ actor, action });
|
|
game = applyAction(game, actor, action);
|
|
}
|
|
alliance = structuredClone(game);
|
|
game = advanceBureaucratQualifyingSplit(game, state => state.phase === 3 && !!state.auction &&
|
|
!state.phaseOpening && !state.response && !state.decision, actions);
|
|
}
|
|
const sourceSetup = structuredClone(game);
|
|
const physical = qualifyingAuctionPhysicalCards(game);
|
|
positionKarama(game, native.payer, staging);
|
|
const counterCard = counter ? positionKarama(game, native.trainer, staging) : undefined;
|
|
if (lastHandSpace) {
|
|
const payer = qualifyingAuctionPlayer(game, native.payer);
|
|
while (payer.hand.length < handLimit(payer) - 1) {
|
|
const incoming = game.deck.shift();
|
|
assert.ok(incoming);
|
|
payer.hand.push(incoming);
|
|
staging.push(`Conserve original undealt ${incoming.id} into ${payer.id} after original auction quantity committed.`);
|
|
}
|
|
}
|
|
assert.deepEqual(qualifyingAuctionPhysicalCards(game), physical);
|
|
const originalPledge: BureaucratQualifyingSplitStep = { actor: donor, action: { type: 'pledgeAid', amount: donorShare } };
|
|
const beforePledge = structuredClone(game);
|
|
actions.push(originalPledge);
|
|
game = applyAction(game, originalPledge.actor, originalPledge.action);
|
|
const pledged = structuredClone(game);
|
|
for (let n = 0; game.auction!.active !== native.payer && n < game.players.length; n++) {
|
|
const step: BureaucratQualifyingSplitStep = { actor: game.auction!.active, action: { type: 'passBid' } };
|
|
actions.push(step);
|
|
game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.equal(game.auction!.active, native.payer);
|
|
const beforeAction = structuredClone(game), amount = payerShare + donorShare;
|
|
const action: Action = { type: 'bid', amount, allyPayment: donorShare };
|
|
return { ...native, donor, alliance, game, sourceSetup, beforePledge, originalPledge, pledged, beforeAction, action,
|
|
payerShare, donorShare, amount, counterCard, physical, reservedCard: game.auction!.cards[game.auction!.index].id,
|
|
bonusCard: game.deck[0]?.id, actions, staging };
|
|
}
|
|
|
|
export function payBureaucratQualifyingAuction(fixture: BureaucratQualifyingAuctionFixture, state = fixture.game): Game {
|
|
let game = applyAction(state, fixture.payer, fixture.action);
|
|
fixture.actions.push({ actor: fixture.payer, action: fixture.action });
|
|
for (let n = 0; game.decision?.kind !== 'auctionPayment' && n < game.players.length + 2; n++) {
|
|
assert.ok(game.auction && !game.currentAuctionSale);
|
|
const step: BureaucratQualifyingSplitStep = { actor: game.auction.active, action: { type: 'passBid' } };
|
|
fixture.actions.push(step);
|
|
game = applyAction(game, step.actor, step.action);
|
|
}
|
|
assert.equal(game.decision?.kind, 'auctionPayment');
|
|
assert.equal(game.decision!.player, fixture.payer);
|
|
const action: Action = { type: 'decision', karama: false };
|
|
fixture.actions.push({ actor: fixture.payer, action });
|
|
return applyAction(game, fixture.payer, action);
|
|
}
|
|
|
|
export function allowQualifyingAuctionResponses(state: Game): Game {
|
|
let game = state;
|
|
for (let n = 0; game.response && n < 40; n++) {
|
|
const actor = game.players.find(player => {
|
|
const controls = viewGame(game, player.id).responseControls;
|
|
return controls && !controls.hasPassed && controls.cancelCards.length > 0;
|
|
});
|
|
assert.ok(actor, 'The original response must have a legal unpassed counter holder');
|
|
game = applyAction(game, actor.id, { type: 'passResponse' });
|
|
}
|
|
assert.equal(game.response, null);
|
|
return game;
|
|
}
|
|
|
|
export function chooseQualifyingAuction(game: Game, redirect = true): Game {
|
|
assert.equal(game.decision?.kind, 'bureaucratPayment');
|
|
return applyAction(game, game.decision!.player, { type: 'decision', event: game.bureaucratPaymentEvent, redirect });
|
|
}
|