91 lines
4.6 KiB
TypeScript
91 lines
4.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction,createGame,initializeFactionExpansionsGameForAudit,joinGame,newPlayer,
|
|
type Action,type Game,
|
|
} from '../game/engine';
|
|
import {nextSpiceBankerIncomeNativeStep,type SpiceBankerIncomeNativeStep} from './fixture-spice-banker-income';
|
|
|
|
const clean=(game:Game)=>!game.phaseOpening&&!game.response&&!game.decision;
|
|
|
|
/** Genuine printed setup, Terror placement, worm alliance, paid entry and
|
|
* detonation. Only original unplayed Spice Card order is controlled. */
|
|
export function createNativeAtomicsAllianceFixture(advanced=false,allied=true,
|
|
options:{laterAllianceToken?:boolean}={}) {
|
|
let game=createGame('NATIVEATOMICS',newPlayer('m','Moritani','moritani'),advanced,['ecaz']);
|
|
for(const [id,faction] of [['a','atreides'],['e','emperor'],['f','fremen']] as const)
|
|
joinGame(game,newPlayer(id,id,faction));
|
|
for(const p of game.players)if(!p.ready)game=applyAction(game,p.id,{type:'ready'});
|
|
const initial=structuredClone(game);
|
|
game=initializeFactionExpansionsGameForAudit(game);
|
|
const actions:SpiceBankerIncomeNativeStep[]=[];
|
|
const step=(actor:string,action:Action)=>{
|
|
game=applyAction(game,actor,action);actions.push({actor,action});
|
|
};
|
|
const advance=(stop:(game:Game)=>boolean)=>{
|
|
for(let i=0;i<1000&&!stop(game);i++) {
|
|
let next:SpiceBankerIncomeNativeStep|null;
|
|
if(game.decision?.kind==='moritaniSetup')
|
|
next={actor:'m',action:{type:'decision',territory:'carthag',sector:11}};
|
|
else if(game.decision?.kind==='moritaniPlacement')
|
|
next={actor:'m',action:{type:'decision',decline:true}};
|
|
else next=nextSpiceBankerIncomeNativeStep(game);
|
|
assert.ok(next,'A native Atomics control must retain legal continuation.');
|
|
step(next.actor,next.action);
|
|
}
|
|
assert.ok(stop(game),`Native Atomics did not reach its control: ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind}`);
|
|
};
|
|
const front=(position:number,predicate:(card:Game['spiceDeck'][number])=>boolean)=>{
|
|
const index=game.spiceDeck.findIndex((card,i)=>i>=position&&predicate(card));
|
|
assert.ok(index>=position,'The original unplayed card must remain in its deck.');
|
|
game.spiceDeck.splice(position,0,game.spiceDeck.splice(index,1)[0]);
|
|
};
|
|
advance(g=>g.status==='playing');
|
|
assert.equal(game.players.find(p=>p.id==='m')!.forces['carthag:11'],6);
|
|
const ordinaryBlows=advanced?2:1;
|
|
for(let i=0;i<ordinaryBlows;i++)front(i,c=>'territory' in c&&!c.discovery&&c.territory!=='sietch_tabr');
|
|
front(ordinaryBlows,c=>'worm' in c&&!c.greatMaker);
|
|
// Keep the two following original blows ordinary, and reserve a second real
|
|
// worm for the later Nexus rather than relying on a random chain of worms.
|
|
for(let i=ordinaryBlows+1;i<=ordinaryBlows+2;i++)
|
|
front(i,c=>'territory' in c&&!c.discovery&&c.territory!=='sietch_tabr');
|
|
front(ordinaryBlows+3,c=>'worm' in c&&!c.greatMaker);
|
|
advance(g=>g.turn===1&&g.decision?.kind==='moritaniPlacement');
|
|
const token=game.moritaniTerror!.tokens.find(t=>t.kind==='atomics'&&t.status==='available')!;
|
|
assert.ok(token);
|
|
step('m',{type:'decision',token:token.id,territory:'carthag'});
|
|
advance(g=>g.turn===2&&g.phase===1&&g.nexus&&clean(g)&&!g.spiceWindow&&!g.spiceResolution);
|
|
if(allied) {
|
|
step('m',{type:'alliance',target:'a'});
|
|
step('a',{type:'alliance',target:'m'});
|
|
}
|
|
advance(g=>g.turn===2&&g.phase===5&&g.active==='e'&&clean(g));
|
|
step('e',{type:'ship',territory:'carthag',sector:11,amount:1,elite:0,allyPayment:0});
|
|
assert.equal(game.decision?.kind,'moritaniTerror');
|
|
step('m',{type:'decision',reveal:true});
|
|
let laterToken:string|null=null;
|
|
if(options.laterAllianceToken) {
|
|
advance(g=>g.turn===2&&g.decision?.kind==='moritaniPlacement');
|
|
const robbery=game.moritaniTerror!.tokens.find(t=>t.kind==='robbery'&&t.status==='available')!;
|
|
assert.ok(robbery);
|
|
laterToken=robbery.id;
|
|
step('m',{type:'decision',token:robbery.id,territory:'arrakeen'});
|
|
}
|
|
advance(g=>g.turn===2&&g.phase===8&&clean(g));
|
|
front(0,c=>'worm' in c&&!c.greatMaker);
|
|
advance(g=>g.turn===3&&g.phase===1&&g.nexus&&clean(g)&&!g.spiceWindow&&!g.spiceResolution);
|
|
assert.equal(game.moritaniAtomics!.allyAtActivation,allied?'a':null);
|
|
assert.equal(game.players.find(p=>p.id==='m')!.tanks,6);
|
|
return {initial,game,actions,token:token.id,laterToken,allied,advanced};
|
|
}
|
|
|
|
export function advanceNativeAtomicsFixture(initial:Game,stop:(game:Game)=>boolean) {
|
|
let game=initial;
|
|
for(let i=0;i<1000&&!stop(game);i++) {
|
|
const next=nextSpiceBankerIncomeNativeStep(game);
|
|
assert.ok(next,'The original post-Atomics phase must have a legal continuation.');
|
|
game=applyAction(game,next.actor,next.action);
|
|
}
|
|
assert.ok(stop(game));
|
|
return game;
|
|
}
|