133 lines
7.7 KiB
TypeScript
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);
|
|
}
|
|
});
|