133 lines
7.2 KiB
TypeScript
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;
|
|
}
|