67 lines
3.5 KiB
TypeScript
67 lines
3.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, handLimit, type Action, type Game } from '../game/engine';
|
|
import { treacheryDeck } from '../game/cards';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { createSemutaKaitainFixture } from './fixture-semuta-kaitain';
|
|
import { nextRicheseSkillsTechNativeStep, type RicheseSkillsTechStep } from './fixture-richese-skills-tech';
|
|
import { moveSemutaEmperorCard, semutaEmperorPlayer } from './fixture-semuta-emperor-share';
|
|
|
|
export function createSemutaHomeworldChoamSetup(advanced = false) {
|
|
const native = createSemutaKaitainFixture({ advanced, count: 1 });
|
|
// This exposed snapshot is the original completed native setup, before that
|
|
// fixture's later Kaitain hand staging or paid disposal. Not a clock rewind.
|
|
const game = structuredClone(native.setup);
|
|
assert.equal(game.status, 'playing');
|
|
assert.ok(game.homeworlds?.custody && game.semutaPreview);
|
|
assert.ok(game.players.some(p => p.faction === 'choam'));
|
|
moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, native.holder);
|
|
assert.ok(semutaEmperorPlayer(game, native.holder).hand.length <= handLimit(semutaEmperorPlayer(game, native.holder)));
|
|
return { game, owner: native.choam, holder: native.holder,
|
|
target: game.players.find(p => p.faction === 'atreides')!.id,
|
|
canceller: native.owner, actions: [] as RicheseSkillsTechStep[] };
|
|
}
|
|
|
|
/** Conserved physical card only; never mint a face or rewrite the whole deck. */
|
|
export function holdSemutaHomeworldChoamCard(game: Game, owner: string, name: string): string {
|
|
const card = [...treacheryDeck(game.expansions), ...richeseCards()].find(card => card.name === name);
|
|
assert.ok(card, `Missing printed card ${name}`);
|
|
return moveSemutaEmperorCard(game, card.id, owner);
|
|
}
|
|
|
|
/** Caller tests the requested boundary before each real native action. */
|
|
export function advanceSemutaHomeworldChoam(
|
|
state: Game, until: (game: Game) => boolean, actions?: RicheseSkillsTechStep[],
|
|
): Game {
|
|
let game = state;
|
|
for (let n = 0; !until(game) && n < 1600; n++) {
|
|
assert.notEqual(game.status, 'finished', 'The original Homeworld game must reach the requested boundary');
|
|
let next: RicheseSkillsTechStep;
|
|
const pending = game.pendingTreacheryDiscard, reaction = pending?.reaction;
|
|
if (pending && reaction?.stage === 'offer') {
|
|
const actor = game.players.find(p => !reaction.passed.includes(p.id))!.id;
|
|
next = { actor, action: { type: 'semutaPass', event: pending.batch.event } };
|
|
} else if (game.decision?.kind === 'choamMarket') {
|
|
next = { actor: game.decision.player, action: { type: 'decision', done: true } };
|
|
} else next = nextRicheseSkillsTechNativeStep(game);
|
|
actions?.push(next);
|
|
game = applyAction(game, next.actor, next.action);
|
|
}
|
|
assert.ok(until(game), 'The native Homeworld continuation must reach its boundary');
|
|
return game;
|
|
}
|
|
|
|
export function reactSemutaHomeworldChoam(state: Game, holder: string, claim: boolean): Game {
|
|
const event = state.pendingTreacheryDiscard!.batch.event;
|
|
if (claim) return applyAction(state, holder, { type: 'semutaCommit', event });
|
|
let game = state;
|
|
for (const player of state.players) game = applyAction(game, player.id, { type: 'semutaPass', event });
|
|
return game;
|
|
}
|
|
|
|
export function applySemutaHomeworldChoamStep(game: Game, actor: string, action: Action,
|
|
actions?: RicheseSkillsTechStep[]): Game {
|
|
actions?.push({ actor, action });
|
|
return applyAction(game, actor, action);
|
|
}
|