Files
Dune/tests/grumman-cunning-runtime.test.ts
T

133 lines
7.7 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { grummanCollectionChoice } from '../game/grumman-collection-options';
import { assertRuledGrummanInventory, createRuledGrummanFixture } from './fixture-ruled-grumman';
import { nextNativeE3HomeworldStep } from './fixture-homeworld-native-e3-modules';
const clean = (game: Game) => !game.phaseOpening && !game.response && !game.decision;
function advance(state: Game, until: (game: Game) => boolean): Game {
let game = state;
for (let count = 0; count < 900; count++) {
if (until(game)) return game;
assert.notEqual(game.status, 'finished');
if (game.decision?.kind === 'moritaniPlacement' && !game.response && !game.phaseOpening)
game = applyAction(game, game.decision.player, {type:'decision',decline:true});
else {
const step = nextNativeE3HomeworldStep(game);
assert.ok(step);
game = applyAction(game,step.actor,step.action);
}
}
throw Error('Native Grumman Cunning programme missed its boundary');
}
function reject(game: Game, actor: string, action: Action): void {
const before = structuredClone(game);
assert.throws(()=>applyAction(game,actor,action));
assert.deepEqual(game,before);
}
function cunningFixture(advanced = false) {
const f = createRuledGrummanFixture({advanced,nexus:true});
const token = f.game.moritaniTerror!.tokens.find(t=>t.status==='available'&&t.kind==='sabotage')!;
const view = viewGame(f.game,f.owner);
const action = grummanCollectionChoice(view,token.id,'polar_sink','add',true).action;
assert.ok(action);
return {...f,token,action};
}
void test('native high-Grumman Cunning adds from supply to an empty static territory using its actual ordinary prerequisite',()=>{
for (const advanced of [false,true]) {
const f = cunningFixture(advanced);
const before = f.game;
const wallet = before.players.find(p=>p.id===f.owner)!.spice;
const armies = before.players.map(p=>({id:p.id,forces:p.forces,reserves:p.reserves,tanks:p.tanks,elites:p.elites}));
reject(before,'guild',f.action);
reject(before,f.owner,{...f.action,nexus:'old-cunning-opportunity'});
reject(before,f.owner,{...f.action,token:f.placed});
reject(before,f.owner,{...f.action,territory:'hidden_mobile_stronghold'});
reject(before,f.owner,{...f.action,territory:'homeworld:moritani'});
const game = applyAction(before,f.owner,f.action);
assert.equal(game.players.find(p=>p.id===f.owner)!.spice,wallet+4);
assert.deepEqual(game.players.map(p=>({id:p.id,forces:p.forces,reserves:p.reserves,tanks:p.tanks,elites:p.elites})),armies);
assert.equal(game.moritaniTerror!.placementTurn,before.moritaniTerror!.placementTurn);
assert.deepEqual(game.moritaniTerror!.tokens.find(t=>t.id===f.token.id),{...f.token,status:'placed',location:'polar_sink'});
assert.deepEqual(game.moritaniTerror!.tokens.find(t=>t.id===f.placed),before.moritaniTerror!.tokens.find(t=>t.id===f.placed));
const receipt = game.nexusMoritaniHistory!.at(-1)!.receipt;
assert.equal(receipt.source,'grumman');
if(receipt.source!=='grumman')throw Error('Expected the original Grumman source');
assert.equal(receipt.sourceEvent,before.grummanCollection!.event);
assert.equal(receipt.prerequisite,f.placedTerritory);
assert.equal(game.nexusCards!.cards!.hands[f.owner],null);
assert.equal(game.nexusCards!.cards!.discard.filter(card=>card==='moritani').length,1);
assert.equal(game.grummanCollection!.nexusEvent,receipt.event);
for(const p of game.players.filter(p=>p.id!==f.owner)){
const view = viewGame(game,p.id);
assert.ok(view.moritaniTerror!.tokens.every(t=>!('kind' in t)));
assert.equal(Object.hasOwn(view,'nexusMoritaniHistory'),false);
}
reject(game,f.owner,f.action);
assertRuledGrummanInventory(game);
}
});
void test('a Grumman Cunning location authorizes a later real entry and its unrevealed return retires only that location',()=>{
const f = cunningFixture();
let game = applyAction(f.game,f.owner,f.action);
game = advance(game,g=>g.turn===3&&g.phase===5&&g.active==='guild'&&clean(g));
const beforeShipment = structuredClone(game);
const payer = game.players.find(p=>p.id==='guild')!;
game = applyAction(game,'guild',{type:'ship',territory:'polar_sink',sector:0,amount:1,elite:0,
homeworldSources:{'homeworld:guild':{normal:1,elite:0}}});
assert.equal(game.pendingTerrorEntry!.token,f.token.id);
assert.equal(game.pendingTerrorEntry!.territory,'polar_sink');
assert.equal(game.players.find(p=>p.id==='guild')!.spice,payer.spice-1);
assert.equal(game.players.find(p=>p.id==='guild')!.forces['polar_sink:0'],1);
assert.equal(game.players.find(p=>p.id==='guild')!.reserves,payer.reserves-1);
game = applyAction(game,f.owner,{type:'decision',decline:true});
assert.equal(game.moritaniTerror!.tokens.find(t=>t.id===f.token.id)!.status,'placed');
assert.equal(game.players.find(p=>p.id==='guild')!.spice,beforeShipment.players.find(p=>p.id==='guild')!.spice-1);
game = advance(game,g=>g.turn===3&&g.decision?.kind==='grummanCollection');
const balance = game.players.find(p=>p.id===f.owner)!.spice;
game = applyAction(game,f.owner,{type:'decision',event:game.grummanCollection!.event,mode:'remove',token:f.token.id});
assert.equal(game.players.find(p=>p.id===f.owner)!.spice,balance+4);
assert.equal(game.nexusMoritaniLocations![f.token.id],undefined);
assert.equal(game.moritaniTerror!.tokens.some(t=>t.id===f.token.id),false);
assert.equal(game.moritaniTerror!.tokens.find(t=>t.id===f.placed)!.location,f.placedTerritory);
assert.equal(game.moritaniTerror!.tokens.find(t=>t.kind===f.token.kind)!.status,'available');
assertRuledGrummanInventory(game);
});
void test('held Cunning cannot manufacture a Grumman addition after the only board token actually returned',()=>{
const f = createRuledGrummanFixture({nexus:true});
let game = applyAction(f.game,f.owner,{type:'decision',event:f.game.grummanCollection!.event,mode:'remove',token:f.placed});
game = advance(game,g=>g.turn===3&&g.decision?.kind==='grummanCollection');
assert.equal(game.nexusCards!.cards!.hands[f.owner],'moritani');
const view = viewGame(game,f.owner), offer = view.grummanCollection!;
assert.ok(offer.cunning!.blocked);
assert.equal(offer.cunning!.prerequisite,null);
const token = offer.tokens[0].id;
assert.equal(grummanCollectionChoice(view,token,'polar_sink','add',true).action,null);
reject(game,f.owner,{type:'decision',event:offer.event,mode:'add',token,territory:'polar_sink',nexus:offer.cunning!.event});
const declined = applyAction(game,f.owner,{type:'decision',event:offer.event,decline:true});
assert.equal(declined.players.find(p=>p.id===f.owner)!.spice,game.players.find(p=>p.id===f.owner)!.spice);
assert.equal(declined.nexusCards!.cards!.hands[f.owner],'moritani');
});
void test('all existing legal policies resolve the owned Grumman Cunning source without reading hidden opposing faces',()=>{
const f = cunningFixture();
for(const difficulty of DIFFICULTIES){
const view = viewGame(f.game,f.owner);
view.players.find(p=>p.id===f.owner)!.bot = difficulty;
const action = botActions(view)[0];
assert.ok(action);
const done = applyAction(f.game,f.owner,action);
assert.equal(done.players.find(p=>p.id===f.owner)!.spice,f.game.players.find(p=>p.id===f.owner)!.spice+4);
assert.equal(done.grummanCollection!.stage,'complete');
assert.equal(done.nexusCards!.cards!.hands[f.owner],null);
assert.equal(done.moritaniTerror!.placementTurn,f.game.moritaniTerror!.placementTurn);
assertRuledGrummanInventory(done);
}
});