import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, createGame, initializeDiscoveryGameForAudit, joinGame, newPlayer, viewGame, type Game } from '../game/engine'; import { placeTerror } from '../game/moritani-terror'; import { nextSpiceBankerIncomeNativeStep } from './fixture-spice-banker-income'; const player = (game: Game, id: string) => { const seat = game.players.find(candidate => candidate.id === id); assert.ok(seat); return seat; }; /** Genuine original Discovery and Fremen/Moritani setup. The first movement * position, Terror placement and the later unplayed Spice ordering are staged; * the original Atomics reveal, Great Maker vote and reserve ride are actions. */ function encounterAfterAtomics() { let game = createGame('MAKERATOMICS', newPlayer('m','Moritani','moritani'), false, ['ecaz']); joinGame(game,newPlayer('f','Fremen','fremen')); joinGame(game,newPlayer('a','Atreides','atreides')); game.discoveryEnabled = true; for (const seat of game.players) game = applyAction(game,seat.id,{type:'ready'}); game = initializeDiscoveryGameForAudit(game); for (let count = 0; game.status === 'setup' && count < 150; count++) { if (game.decision?.kind === 'moritaniSetup') game = applyAction(game,game.decision.player,{type:'decision',territory:'carthag',sector:11}); else { const next = nextSpiceBankerIncomeNativeStep(game); assert.ok(next); game = applyAction(game,next.actor,next.action); } } assert.equal(game.status,'playing'); const atomics = game.moritaniTerror!.tokens.find(token => token.kind === 'atomics'); assert.ok(atomics); game.moritaniTerror = placeTerror(game.moritaniTerror!,atomics.id,'arrakeen',1); Object.assign(game,{turn:2,phase:5,storm:18,active:'a',order:['a','m','f'], movementRemaining:['a','m','f'],ready:[],response:null,decision:null,phaseOpening:null}); for (const seat of game.players) { game.deck.push(...seat.hand); Object.assign(seat,{hand:[],forces:{},reserves:20,tanks:0,ally:null,shipped:false,moved:0,spice:20}); if (seat.elites) Object.assign(seat.elites,{reserves:seat.faction === 'fremen' ? 3 : 0,tanks:0,forces:{},revived:0}); } game = applyAction(game,'a',{type:'ship',territory:'arrakeen',sector:10,amount:1,elite:0,allyPayment:0}); assert.equal(game.pendingTerrorEntry?.stage,'offer'); game = applyAction(game,'m',{type:'decision',reveal:true}); assert.equal(game.moritaniAtomics?.territory,'arrakeen'); assert.equal(game.moritaniTerror!.tokens.find(token => token.id === atomics.id)?.status,'removed'); const find = (predicate: (card: Game['spiceDeck'][number]) => boolean) => { const index = game.spiceDeck.findIndex(predicate); assert.ok(index >= 0); return game.spiceDeck.splice(index,1)[0]; }; const maker = find(card => 'worm' in card && card.greatMaker === true); const source = find(card => 'territory' in card && card.territory === 'broken_land' && !card.discovery); const replacement = find(card => 'territory' in card && !card.discovery); assert.ok('territory' in source); Object.assign(game,{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]; game.spice = {[`${source.territory}:${source.sector}`]:7}; for (const seat of game.players) game = applyAction(game,seat.id,{type:'ready'}); assert.equal(game.greatMaker?.stage,'vote'); while (game.decision?.kind === 'greatMakerVote') game = applyAction(game,game.decision.player,{type:'decision',event:game.decision.event,yes:false}); assert.equal(game.greatMaker?.stage,'ride'); assert.equal(game.decision?.player,'f'); return game; } void test('Great Maker reserve ride may enter Atomics Aftermath without becoming a shipment', () => { const game = encounterAfterAtomics(), view = viewGame(game,'f'); assert.ok(view.greatMaker?.ride?.destinations.some(destination => destination.territory === 'arrakeen' && destination.sector === 10)); const before = player(game,'f'), count = before.reserves, spice = before.spice; const shipped = before.shipped, moved = before.moved; const event = game.greatMaker!.event; const done = applyAction(game,'f',{type:'decision',event,accept:true, territory:'arrakeen',sector:10,amount:2,elite:0}); assert.equal(player(done,'f').reserves,count-2); assert.equal(player(done,'f').forces['arrakeen:10'],2); assert.equal(player(done,'f').spice,spice); assert.equal(player(done,'f').shipped,shipped); assert.equal(player(done,'f').moved,moved); assert.equal(done.moritaniAtomics?.territory,'arrakeen'); assert.equal(done.greatMaker?.stage,'complete'); for (const seat of done.players) assert.equal(seat.reserves + seat.tanks + Object.values(seat.forces).reduce((sum,n) => sum+n,0),20); });