101 lines
5.7 KiB
TypeScript
101 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction, createGame, initializeDiscoveryGameForAudit, initializeEcazTreacheryGameForAudit, joinGame, newPlayer,
|
|
type Action, type Game,
|
|
} from '../game/engine';
|
|
import { placeAmbassador, validateAmbassadors } from '../game/ecaz-ambassadors';
|
|
import { placeTerror } from '../game/moritani-terror';
|
|
import { nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
|
|
import { nextSpiceBankerIncomeNativeStep } from './fixture-spice-banker-income';
|
|
|
|
export const act = (game: Game, actor: string, action: Action): Game =>
|
|
applyAction(structuredClone(game), actor, action);
|
|
|
|
export function player(game: Game, id: string) {
|
|
const seat = game.players.find(row => row.id === id); assert.ok(seat); return seat;
|
|
}
|
|
|
|
/** Original Advanced native setup, actual Great Maker votes and reserve ride.
|
|
* The test controls a later turn/phase boundary, the existing unplayed Spice
|
|
* order and original physical Terror/Ambassador placement. It does not stage
|
|
* forces, player purses, a Great Maker encounter, a pending reaction or receipt.
|
|
* Ecaz additionally selects the independent printed card variant to enter the
|
|
* existing paired E3 Discovery profile. Paired tests control the public order
|
|
* before the real Great Maker votes to exercise both original owner orders. */
|
|
export function greatMakerSneakFixture(withBG = false, withEcaz = false,
|
|
ambassadorEffect: 'fremen' | 'emperor' = 'fremen', first: 'terror' | 'ambassador' = 'terror') {
|
|
let game = createGame('MAKERSNEAK', newPlayer('moritani','Moritani','moritani'), true, ['ecaz']);
|
|
for (const faction of ['fremen','atreides', ...(withBG ? ['beneGesserit'] as const : []),
|
|
...(withEcaz ? ['ecaz'] as const : [])] as const)
|
|
joinGame(game, newPlayer(faction,faction,faction));
|
|
game.discoveryEnabled = true;
|
|
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
|
|
game = withEcaz ? initializeEcazTreacheryGameForAudit(game) : initializeDiscoveryGameForAudit(game);
|
|
for (let count = 0; game.status === 'setup' && count < 250; count++) {
|
|
if (game.setupStage === 'forces' && player(game,'fremen').reserves === 20) {
|
|
game = act(game,'fremen',{type:'fremenSetup',placements:{sietch_tabr:10}});
|
|
continue;
|
|
}
|
|
const next = withEcaz ? nextBasicSemutaAmbassadorStep(game) : nextSpiceBankerIncomeNativeStep(game);
|
|
assert.ok(next, 'Native Discovery setup needs an existing legal choice');
|
|
game = act(game, next.actor, next.action);
|
|
}
|
|
assert.equal(game.status, 'playing');
|
|
assert.ok(game.advanced && game.discoveryEnabled && game.discoveries);
|
|
const sneak = game.moritaniTerror!.tokens.find(row => row.kind === 'sneakAttack'); assert.ok(sneak);
|
|
game.moritaniTerror = placeTerror(game.moritaniTerror!, sneak.id, 'carthag', 1);
|
|
|
|
let ambassador: string | undefined;
|
|
if (withEcaz) {
|
|
const inventory = game.ecazAmbassadors!;
|
|
const token = inventory.tokens.find(row => row.effect === ambassadorEffect); assert.ok(token);
|
|
if (token.zone === 'pool') {
|
|
const replacement = inventory.tokens.find(row => row.zone === 'supply' && row.effect !== 'ecaz');
|
|
assert.ok(replacement);
|
|
const index = inventory.cohort.indexOf(replacement.id); assert.ok(index >= 0);
|
|
inventory.cohort[index] = token.id; replacement.zone = 'pool'; token.zone = 'supply';
|
|
}
|
|
assert.equal(token.zone, 'supply'); validateAmbassadors(inventory);
|
|
game.ecazAmbassadors = placeAmbassador(inventory, token.id, {turn:1,
|
|
availableSpice:player(game,'ecaz').spice,
|
|
destination:{id:'carthag',stronghold:true,inStorm:false,allowed:true}}).state;
|
|
ambassador = token.id;
|
|
}
|
|
|
|
const take = (matches: (card: Game['spiceDeck'][number]) => boolean) => {
|
|
const index = game.spiceDeck.findIndex(matches); assert.ok(index >= 0);
|
|
return game.spiceDeck.splice(index,1)[0];
|
|
};
|
|
const maker = take(card => 'worm' in card && card.greatMaker === true);
|
|
const source = take(card => 'territory' in card && card.territory === 'broken_land' && !card.discovery);
|
|
const replacement = take(card => 'territory' in card && !card.discovery);
|
|
Object.assign(game, {turn:2, phase:1, storm:18, nexus:false, summonedBeforeBlow:false,
|
|
wormRides:[], spiceSequence:null, spiceResolution:null, spiceWindow:null,
|
|
response:null, decision:null, phaseOpening:null, ready:[]});
|
|
game.spiceDiscard = [[source], []];
|
|
game.spiceDeck = [maker, replacement, ...game.spiceDeck];
|
|
if (withEcaz && ambassadorEffect !== 'fremen') {
|
|
const firstOwner = first === 'terror' ? 'moritani' : 'ecaz';
|
|
const secondOwner = first === 'terror' ? 'ecaz' : 'moritani';
|
|
game.order = [firstOwner, secondOwner, ...game.order.filter(id => id !== firstOwner && id !== secondOwner)];
|
|
}
|
|
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
|
|
assert.equal(game.greatMaker?.stage, 'vote');
|
|
while (game.decision?.kind === 'greatMakerVote') {
|
|
const decision = game.decision;
|
|
game = act(game, decision.player, {type:'decision',event:decision.event,yes:false});
|
|
}
|
|
assert.equal(game.decision?.kind, 'greatMakerRide');
|
|
const event = game.decision.event;
|
|
const originalReserves = player(game,'fremen').reserves;
|
|
game = act(game,'fremen',{type:'decision',event,accept:true,
|
|
territory:'carthag',sector:11,amount:1,elite:0});
|
|
assert.equal(game.greatMaker?.stage, 'arrival');
|
|
if (withEcaz && ambassadorEffect !== 'fremen' && first === 'ambassador')
|
|
assert.equal(game.pendingAmbassador?.token, ambassador);
|
|
else assert.equal(game.pendingTerrorEntry?.token, sneak.id);
|
|
assert.equal(player(game,'fremen').reserves, originalReserves - 1);
|
|
return {game, event, sneak:sneak.id, ambassador, withBG, withEcaz, first, originalReserves,
|
|
destination:'carthag', sector:11};
|
|
}
|