Files
Dune/tests/fixture-great-maker-sneak.ts
T
Quintine 2ad7f127ec
Verify and publish checkpoint container / container (push) Canceled after 0s
Resolve Sneak arrivals during Great Maker rides
2026-10-29 19:59:10 +11:00

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};
}