68 lines
4.5 KiB
TypeScript
68 lines
4.5 KiB
TypeScript
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' };
|
|
}
|