import assert from 'node:assert/strict'; import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; import { actAdvisorPreparation as act, advisorPreparationInventory as inventory, advisorPreparationPlayer as player, nextAdvisorPreparationStep } from './fixture-advisor-preparation'; export const actAdvisorWormRide = act; export const advisorWormRidePlayer = player; export const advisorWormRideInventory = inventory; /** Genuine Basic-faction Advanced public start, prediction, native placements * and Traitors. Only the unopened starting Treachery deal is selected. The * later phase, current ride, queued ride and conserved force/stance positions * are explicitly staged to isolate the source departure. */ export function createAdvisorWormRideFixture(options: {intrusion?: boolean; partial?: boolean} = {}) { const bg = 'beneGesserit', fremen = 'fremen', holder = 'atreides'; let game = createGame('ADVISORWORMRIDE', newPlayer(bg,'Bene Gesserit','beneGesserit'),true); joinGame(game,newPlayer(fremen,'Fremen','fremen')); joinGame(game,newPlayer(holder,'Atreides','atreides')); for (const seat of game.players) game = act(game,seat.id,{type:'ready'}); game = act(game,bg,{type:'start',advancedPreview:true}); assert.equal(game.status,'setup'); const originalCards = inventory(game); const karama = game.deck.find(card => card.effect === 'karama'); assert.ok(karama); const ordinary = game.deck.filter(card => ['projectile','poison','shield','snooper'].includes(card.kind)).slice(0,2); assert.equal(ordinary.length,2); for (const [position,card] of [ordinary[0],ordinary[1],karama].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]]; } for (let count=0; game.status === 'setup' && count<100; count++) { if (game.setupStage === 'forces' && player(game,fremen).reserves === 20) { game = act(game,fremen,{type:'fremenSetup',placements:{sietch_tabr:10}}); continue; } const step = nextAdvisorPreparationStep(game); assert.ok(step); game = act(game,step.actor,step.action); } assert.equal(game.status,'playing'); assert.equal(game.advanced,true); assert.ok(player(game,holder).hand.some(card => card.id === karama.id)); assert.deepEqual(inventory(game),originalCards); const bgSeat = player(game,bg), f = player(game,fremen), a = player(game,holder); bgSeat.forces = {'red_chasm:7':2, ...(options.intrusion ? {'imperial_basin:10':1} : {})}; bgSeat.reserves = 20 - (options.intrusion ? 3 : 2); bgSeat.tanks = 0; bgSeat.advisors = {red_chasm:{}}; f.forces = {'red_chasm:7':2, 'hagga_basin:12':1}; f.reserves = 17; f.tanks = 0; f.elites = {reserves:3,tanks:0,forces:{},revived:0}; a.forces = {}; a.reserves = 20; a.tanks = 0; Object.assign(game,{phase:1,turn:2,nexus:true,active:null,ready:[],storm:18, stormPending:null,phaseOpening:null,response:null,truthtrance:null, spiceWindow:null,spiceResolution:null,spiceSequence:{pile:1,skipped:[]}, wormRides:['hagga_basin'],decision:{kind:'wormRide',player:fremen,territory:'red_chasm'}}); const action:Action = {type:'decision',accept:true,territory:'imperial_basin',sector:10, forces:{'red_chasm:7':options.partial ? 1 : 2},eliteForces:{'red_chasm:7':0}}; return {game,bg,fremen,holder,karama:karama.id,action,origin:'red_chasm',source:'red_chasm:7', destination:'imperial_basin:10',second:'hagga_basin',originalCards,partial:!!options.partial, intrusion:!!options.intrusion}; } /** Pass only the committed source conversion; preserve any next decision. */ export function passAdvisorWormRide(source:Game):Game { let game = source; for (let count=0; game.response?.automaticAdvisorFlip && count<30; count++) { const actor = game.players.find(seat => !game.response!.passed.includes(seat.id)); assert.ok(actor); game = act(game,actor.id,{type:'passResponse'}); } assert.ok(!game.response?.automaticAdvisorFlip); return game; }