Files
Dune/tests/great-maker-atomics-ride.test.ts
T
Quintine 58303654e8
Verify and publish checkpoint container / container (push) Canceled after 0s
Extend Semuta ordinary discards to native factions
2026-10-29 03:07:55 +11:00

87 lines
4.9 KiB
TypeScript

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