Files
Dune/tests/fixture-semuta-homeworld-karama-auction.ts

133 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict';
import { handLimit, type Action, type Game } from '../game/engine';
import { biddingPhysicalIds } from './fixture-homeworld-occupied-bidding';
import { semutaEmperorCardId, moveSemutaEmperorCard, semutaEmperorPlayer } from './fixture-semuta-emperor-share';
import { createSemutaHomeworldChoamSetup, applySemutaHomeworldChoamStep, advanceSemutaHomeworldChoam } from './fixture-semuta-homeworld-choam';
import { nextRicheseSkillsTechNativeStep, type RicheseSkillsTechStep } from './fixture-richese-skills-tech';
export type SemutaHomeworldKaramaAuctionScope = 'purchase' | 'payment';
export type SemutaHomeworldKaramaAuctionFixture = {
scope: SemutaHomeworldKaramaAuctionScope;
setup: Game;
beforeLot: Game;
game: Game;
gameBeforeCost: Game;
actor: string;
owner: string;
holder: string;
card: string;
action: Action;
event: string;
reservedLot: string;
nativeBid: number;
payers: { player: string; spice: number }[];
custody: Game['homeworlds'];
actions: RicheseSkillsTechStep[];
staging: string[];
};
const clean = (game: Game) => !game.phaseOpening && !game.response && !game.decision && !game.pendingTreacheryDiscard;
/** The original R/C/E/A setup and real first Bidding. Only conserved physical
* cards and two still-unplayed Spice Cards are positioned; no economy or clock
* is assigned. Richese declares last before the native normal pool is drawn. */
export function createSemutaHomeworldKaramaAuctionFixture(
scope: SemutaHomeworldKaramaAuctionScope, advanced = false, fullHolder = false,
): SemutaHomeworldKaramaAuctionFixture {
const native = createSemutaHomeworldChoamSetup(advanced);
const setup = structuredClone(native.game), actions = native.actions, staging: string[] = [];
let game = native.game;
const actor = native.target, holder = native.holder;
const physical = biddingPhysicalIds(game);
const held = semutaEmperorPlayer(game, actor).hand.find(card => card.effect === 'karama');
const card = held?.id ?? moveSemutaEmperorCard(game,
semutaEmperorCardId(game, candidate => candidate.effect === 'karama'), actor);
staging.push(`Conserved printed Karama ${card} in original Atreides ${actor} custody before Bidding.`);
assert.ok(semutaEmperorPlayer(game, actor).hand.length < handLimit(semutaEmperorPlayer(game, actor)));
assert.deepEqual(biddingPhysicalIds(game), physical);
for (let position = 0; position < 2; position++) {
const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card);
assert.ok(index >= position);
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
}
staging.push('Position two original unplayed non-worm Spice Cards, retaining the original deck.');
game = advanceSemutaHomeworldChoam(game, state => state.decision?.kind === 'richeseDeclaration', actions);
game = applySemutaHomeworldChoamStep(game, game.decision!.player,
{ type: 'decision', event: game.richeseBidding!.event, position: 'last' }, actions);
game = advanceSemutaHomeworldChoam(game, state => !!state.auction && clean(state), actions);
const beforeLot = structuredClone(game);
assert.equal(game.auction!.peekKnown, true, 'The actual native Atreides inspection has completed');
assert.equal(game.richeseBidding!.position, 'last');
const reservedLot = game.auction!.cards[game.auction!.index].id;
if (fullHolder) {
const original = biddingPhysicalIds(game), receiver = semutaEmperorPlayer(game, holder);
while (receiver.hand.length < handLimit(receiver)) {
const incoming = game.deck.shift();
assert.ok(incoming, 'Original undealt stock must supply holder capacity');
receiver.hand.push(incoming);
staging.push(`After ordinary lot quantity committed, conserve undealt ${incoming.id} into ${holder} hand.`);
}
assert.equal(receiver.hand.length, 4);
assert.deepEqual(biddingPhysicalIds(game), original);
}
if (scope === 'payment') {
for (let n = 0; game.auction!.active !== actor && n < game.players.length; n++)
game = applySemutaHomeworldChoamStep(game, game.auction!.active, { type: 'passBid' }, actions);
assert.equal(game.auction!.active, actor);
const amount = semutaEmperorPlayer(game, actor).spice + 1;
game = applySemutaHomeworldChoamStep(game, actor, { type: 'bid', amount }, actions);
for (let n = 0; game.decision?.kind !== 'auctionPayment' && n < game.players.length + 2; n++)
game = applySemutaHomeworldChoamStep(game, game.auction!.active, { type: 'passBid' }, actions);
assert.equal(game.decision?.kind, 'auctionPayment');
assert.equal(game.decision!.player, actor);
assert.equal(game.auction!.bidder, actor);
assert.ok(game.auction!.bid > semutaEmperorPlayer(game, actor).spice);
}
assert.equal(game.auction!.cards[game.auction!.index].id, reservedLot);
const action: Action = scope === 'purchase'
? { type: 'card', mode: 'purchase', card }
: { type: 'decision', karama: true, card };
return { scope, setup, beforeLot, game, gameBeforeCost: structuredClone(game), actor, owner: actor,
holder, card, action, event: game.richeseBidding!.event, reservedLot, nativeBid: game.auction!.bid,
payers: game.players.map(player => ({ player: player.id, spice: player.spice })),
custody: structuredClone(game.homeworlds), actions, staging };
}
export function disposeSemutaHomeworldKaramaAuction(
fixture: SemutaHomeworldKaramaAuctionFixture, state = fixture.game, actions?: RicheseSkillsTechStep[],
): Game {
return applySemutaHomeworldChoamStep(state, fixture.actor, fixture.action, actions);
}
/** Keep every original remaining normal/cache lot and invoice. Finish with
* native no-spend human choices, including the actual Bidding-end ready gate. */
export function finishSemutaHomeworldKaramaAuction(
state: Game, fixture: SemutaHomeworldKaramaAuctionFixture, actions?: RicheseSkillsTechStep[],
): Game {
let game = state;
for (let n = 0; !(game.phase === 5 && clean(game)) && n < 1600; n++) {
assert.equal(game.turn, fixture.game.turn);
const end = game.biddingEnd;
const actor = end && clean(game) ? end.owners.find(id => !end.ready.includes(id)) : undefined;
if (actor) game = applySemutaHomeworldChoamStep(game, actor,
{ type: 'biddingEnd', event: end!.event, mode: 'ready' }, actions);
else if (game.pendingTreacheryDiscard?.reaction?.stage === 'offer') {
const pending = game.pendingTreacheryDiscard, reaction = pending.reaction;
assert.ok(reaction?.stage === 'offer');
const seat = game.players.find(player => !reaction.passed.includes(player.id))!;
game = applySemutaHomeworldChoamStep(game, seat.id,
{ type: 'semutaPass', event: pending.batch.event }, actions);
} else if (game.decision?.kind === 'choamMarket')
game = applySemutaHomeworldChoamStep(game, game.decision.player, { type: 'decision', done: true }, actions);
else if (game.decision?.kind === 'caladanReinforcement')
game = applySemutaHomeworldChoamStep(game, game.decision.player,
{ type: 'decision', event: game.decision.event, decline: true }, actions);
else {
const next = nextRicheseSkillsTechNativeStep(game);
game = applySemutaHomeworldChoamStep(game, next.actor, next.action, actions);
}
}
assert.equal(game.phase, 5);
assert.ok(clean(game));
return game;
}