Files
Dune/tests/fixture-semuta-market-ghola.ts
T
Quintine bcca08cac5
Verify and publish checkpoint container / container (push) Successful in 11m25s
Support printed Kull during market Ghola
2026-10-29 08:37:50 +11:00

112 lines
6.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { actSemutaNativeMarkets as act, advanceSemutaNativeMarkets as advance,
semutaNativeMarketsInventory as inventory, semutaNativeMarketsPlayer as player } from './fixture-semuta-native-markets';
export const actSemutaMarketGhola = act;
export const semutaMarketGholaPlayer = player;
export const semutaMarketGholaInventory = inventory;
/** Public Basic setup and real CHOAM market declaration. The original
* starting card order and Spice positions are selected before they are drawn.
* After setup, the real Ghola and Semuta cards are moved from their respective
* decks to their owners' hands and one CHOAM reserve is moved to Tanks. When
* selected, physical Kull and a second Karama are also moved from the deck to
* CHOAM and Tleilaxu respectively. Those
* conserved staging choices establish the relevant holdings and casualty; no
* response, market, discard receipt, wallet, revival or income is staged. */
export function createSemutaMarketGholaFixture(options: {fullHand?: boolean; withoutTleilaxu?: boolean; withKull?: boolean} = {}) {
const owner = 'choam', claimant = 'richese',
incomeOwner = options.withoutTleilaxu ? 'atreides' : 'tleilaxu', counterOwner = 'emperor';
let game = createGame('SEMUTAMARKETGHOLA', newPlayer(claimant, 'Richese', 'richese'), false, ['choam', 'ix']);
for (const id of [owner, incomeOwner, counterOwner] as const) joinGame(game, newPlayer(id, id, id));
if (!options.withKull) game = act(game, claimant, {type:'moduleOption', module:'kullPreview', enabled:false});
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
game = act(game, claimant, {type:'start'});
assert.equal(game.status, 'setup');
assert.equal(game.semutaPreview, true);
assert.ok(!game.leaderSkills && !game.nexusCards && !game.homeworlds);
const originalInventory = inventory(game);
const ordinary = game.deck.filter(card => card.kind === 'projectile').slice(0, 2);
const sale = game.deck.find(card => card.kind === 'worthless');
const counter = game.deck.find(card => card.effect === 'karama');
assert.equal(ordinary.length, 2); assert.ok(sale); assert.ok(counter);
for (const [position, card] of [ordinary[0], sale, ordinary[1], counter].entries()) {
const index = game.deck.findIndex(candidate => candidate.id === card.id); assert.ok(index >= position);
[game.deck[position], game.deck[index]] = [game.deck[index], game.deck[position]];
}
game = advance(game, state => state.status === 'playing');
assert.ok(player(game, owner).hand.some(card => card.id === sale.id));
assert.ok(player(game, counterOwner).hand.some(card => card.id === counter.id));
const front = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery);
assert.ok(front >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(front, 1)[0]);
const semutaIndex = game.richeseCache!.findIndex(card => card.id === SEMUTA_DRUG_ID);
assert.ok(semutaIndex >= 0);
player(game, claimant).hand.push(game.richeseCache!.splice(semutaIndex, 1)[0]);
const gholaIndex = game.deck.findIndex(card => card.effect === 'ghola'); assert.ok(gholaIndex >= 0);
const ghola = game.deck.splice(gholaIndex, 1)[0]; player(game, owner).hand.push(ghola);
let kull: string | undefined, kullCounter: string | undefined;
if (options.withKull) {
assert.equal(game.kullPreview, true);
const index = game.deck.findIndex(card => card.id === 'ix-kull-wahad'); assert.ok(index >= 0);
const card = game.deck.splice(index, 1)[0]; player(game, owner).hand.push(card); kull = card.id;
const counterIndex = game.deck.findIndex(candidate => candidate.effect === 'karama'); assert.ok(counterIndex >= 0);
const secondCounter = game.deck.splice(counterIndex, 1)[0]; player(game, incomeOwner).hand.push(secondCounter);
kullCounter = secondCounter.id;
}
const choam = player(game, owner); assert.ok(choam.reserves > 0);
choam.reserves--; choam.tanks++;
if (options.fullHand) while (player(game, claimant).hand.length < 4) {
const index = game.deck.findIndex(card => ['projectile','poison','shield','snooper','worthless'].includes(card.kind));
assert.ok(index >= 0); player(game, claimant).hand.push(game.deck.splice(index, 1)[0]);
}
assert.deepEqual(inventory(game), originalInventory);
game = advance(game, state => state.decision?.kind === 'choamMarket' && !state.response && !state.phaseOpening);
assert.equal(game.decision?.player, owner);
game = act(game, owner, {type:'decision', mode:'sell', card:sale.id});
assert.equal(game.response?.kind, 'choamSale');
const source = structuredClone(game), market = structuredClone(game.choamMarket), response = structuredClone(game.response);
const gholaAction:Action = {type:'card', card:ghola.id, amount:1, elite:0};
game = act(game, owner, gholaAction);
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
const event = game.pendingTreacheryDiscard!.batch.event;
assert.deepEqual(inventory(game), originalInventory);
return {game, source, owner, claimant, incomeOwner, counterOwner, ghola:ghola.id,
semuta:SEMUTA_DRUG_ID, counter:counter.id, sale:sale.id, event, originalInventory, kull, kullCounter,
market, response, gholaAction, fullHand:!!options.fullHand};
}
/** Pass only the current discard offer, without consuming a later response. */
export function passSemutaMarketGhola(source:Game):Game {
const pending = source.pendingTreacheryDiscard;
assert.equal(pending?.reaction?.stage, 'offer');
const event = pending!.batch.event;
let game = source;
for (const seat of source.players) {
if (game.pendingTreacheryDiscard?.reaction?.stage !== 'offer') break;
if (!game.pendingTreacheryDiscard.reaction.passed.includes(seat.id))
game = act(game, seat.id, {type:'semutaPass', event});
}
return game;
}
export function claimSemutaMarketGhola(source:Game, actor:string, card:string):Game {
const event = source.pendingTreacheryDiscard!.batch.event;
let game = act(source, actor, {type:'semutaCommit', event});
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select')
game = act(game, actor, {type:'semutaSelect', event, card});
return game;
}
export function allowSemutaMarketGholaIncome(source:Game):Game {
let game = source;
for (let count = 0; game.response?.kind === 'revivalIncome' && count < game.players.length; count++) {
const seat = game.players.find(candidate => !game.response!.passed.includes(candidate.id)); assert.ok(seat);
game = act(game, seat.id, {type:'passResponse'});
}
assert.notEqual(game.response?.kind, 'revivalIncome');
return game;
}