import assert from 'node:assert/strict'; import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; import { TERRITORIES } from '../game/board'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { advance, inventory, player } from './fixture-kull-richese-semuta'; export { inventory, player }; export const act = (game: Game, actor: string, action: Action) => applyAction(game, actor, action); export type GuildRoute = 'reserve' | 'cross' | 'return'; /** Genuine Basic public setup and native auction/movement actions. Only the * still-unopened starting Treachery deal and first Spice card are selected. */ export function createKullGuildSemutaFixture(options: { route?: GuildRoute; poor?: boolean } = {}) { const route = options.route ?? 'reserve'; let game = createGame('KULLGUILDSEMUTA', newPlayer('choam', 'CHOAM', 'choam'), false, ['choam', 'ix']); for (const faction of ['richese', 'emperor', 'guild', 'harkonnen'] as const) joinGame(game, newPlayer(faction, faction, faction)); game = act(game, 'choam', { type: 'moduleOption', module: 'kullPreview', enabled: true }); for (const seat of game.players) game = act(game, seat.id, { type: 'ready' }); game = act(game, game.host, { type: 'start' }); assert.equal(game.status, 'setup'); assert.ok(game.kullPreview && game.semutaPreview); const originalCards = inventory(game), selected: Game['deck'] = []; for (const kind of ['kull', 'filler', 'karama', 'filler', 'karama'] as const) { const index = game.deck.findIndex(card => kind === 'kull' ? card.id === 'ix-kull-wahad' : kind === 'karama' ? card.effect === 'karama' : card.kind === 'worthless' && card.id !== 'ix-kull-wahad'); assert.ok(index >= 0); selected.push(game.deck.splice(index, 1)[0]); } game.deck.unshift(...selected); game = advance(game, state => state.status === 'playing'); const [kull, , karama, , counter] = selected.map(card => card.id); assert.ok(player(game, 'choam').hand.some(card => card.id === kull)); assert.ok(player(game, 'emperor').hand.some(card => card.id === karama)); assert.ok(player(game, 'harkonnen').hand.some(card => card.id === counter)); const land = game.spiceDeck.findIndex(card => 'territory' in card && !card.discovery); assert.ok(land >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(land, 1)[0]); game = advance(game, state => player(state, 'richese').hand.some(card => card.id === SEMUTA_DRUG_ID) && !state.response && !state.decision && !state.phaseOpening && !state.pendingTreacheryDiscard); game = advance(game, state => state.phase === 5 && state.active === 'guild' && !state.response && !state.decision && !state.phaseOpening && !state.pendingTreacheryDiscard); if (options.poor) { const excess = player(game, 'guild').spice - 2; assert.ok(excess >= 0); if (excess) game = act(game, 'guild', { type: 'bribe', target: 'emperor', amount: excess }); } const beforeShipment = structuredClone(game); const destination = TERRITORIES.find(territory => territory.type === 'sand' && territory.sectors.some(sector => sector !== game.storm) && game.players.every(seat => !Object.keys(seat.forces).some(key => key.startsWith(`${territory.id}:`)))); assert.ok(destination); const sector = destination.sectors.find(value => value !== game.storm)!; const source = Object.entries(player(game, 'guild').forces) .find(([key, amount]) => amount >= 2 && Number(key.split(':')[1]) !== game.storm)?.[0]; assert.ok(source); const action: Action = route === 'reserve' ? { type: 'ship', territory: destination.id, sector, amount: 2, elite: 0, allyPayment: 0 } : { type: 'guildShip', from: source, territory: route === 'return' ? 'reserves' : destination.id, sector: route === 'return' ? 0 : sector, amount: 2, elite: 0, allyPayment: 0 }; game = act(game, 'guild', action); if (game.decision?.kind === 'guildShipment') game = act(game, 'guild', { type: 'decision', allow: true }); for (let count = 0; game.response?.kind === 'guildTransportPermission' && count < game.players.length; count++) { const actor = game.players.find(seat => !game.response!.passed.includes(seat.id)); assert.ok(actor); game = act(game, actor.id, { type: 'passResponse' }); } assert.equal(game.response?.kind, 'guildRate'); assert.deepEqual(inventory(game), originalCards); return { game, beforeShipment, originalCards, route, action, source, target: `${destination.id}:${sector}`, kull, karama, counter, semuta: SEMUTA_DRUG_ID, holder: 'richese', reactor: 'choam', actor: 'emperor', counterOwner: 'harkonnen', guild: 'guild' }; }