Files
Dune/tests/revival-terror-arrival-engine.test.ts

122 lines
6.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { viewGame, type Game } from '../game/engine';
import { isAdvisor } from '../game/advisors';
import {
createRevivalTerrorFixture as fixture, commitRevivalTerror as commit,
actRevivalTerror as act, passRevivalTerror as pass, finishRevivalTerror as finish,
revivalTerrorPlayer as player, revivalTerrorInventory as inventory,
type RevivalTerrorFixture,
} from './fixture-revival-terror';
function enter(f: RevivalTerrorFixture) {
let g = commit(f.game, f).game;
if (g.decision?.kind === 'advisor') {
assert.equal(g.decision.shipment, f.entrant);
g = act(g, f.bg!, {type:'decision',accept:false});
}
assert.equal(player(g,f.entrant).forces[f.location],1);
assert.equal(g.homeworldRevivalReturn?.stage,'arrival');
return g;
}
function declineAmbassador(g: Game, f: RevivalTerrorFixture) {
assert.equal(g.decision?.kind,'ecazAmbassador');
assert.equal(g.decision?.player,f.ecaz);
return act(g,f.ecaz!,{type:'decision',event:g.pendingAmbassador!.event,decline:true});
}
function complete(g: Game, f: RevivalTerrorFixture) {
g = finish(g);
assert.equal(g.homeworldRevivalReturn?.stage,'complete');
assert.equal(g.pendingArrivalOverlap ?? null,null);
assert.equal(g.pendingArrivalReaction ?? null,null);
assert.equal(g.pendingTerrorEntry ?? null,null);
assert.equal(g.pendingAmbassador ?? null,null);
assert.equal(player(g,f.entrant).forces[f.location],1);
assert.equal(player(g,f.entrant).spice,player(f.beforeRequest,f.entrant).spice+1);
assert.equal(player(g,f.entrant).shipped,player(f.game,f.entrant).shipped);
assert.deepEqual(inventory(g),f.originalInventory);
return g;
}
for (const advanced of [false,true]) for (const ambassadorFirst of [false,true]) {
void test(`${advanced?'Advanced':'Basic'} native revived overlap: ${ambassadorFirst?'Ambassador':'Terror'} acts first and both declines finish original income`,()=>{
const f=fixture({advanced,ecaz:true,ambassadorFirst}), g=enter(f);
assert.equal(g.pendingArrivalOverlap?.first,ambassadorFirst?'ambassador':'terror');
assert.equal(g.pendingArrivalOverlap?.cause,'homeworldRevival');
assert.equal(g.pendingArrivalOverlap?.revivalArrival?.event,f.event);
const second=ambassadorFirst?declineAmbassador(g,f):act(g,f.owner,f.declineAction);
assert.equal(second.homeworldRevivalReturn?.stage,'arrival');
const result=complete(ambassadorFirst?act(second,f.owner,f.declineAction):declineAmbassador(second,f),f);
assert.ok(result.homeworldRevivalReturn!.ambassadors!.every(entry=>entry.completed));
assert.equal(result.moritaniTerror!.tokens.find(token=>token.id===f.token)!.status,'placed');
assert.equal(result.ecazAmbassadors!.tokens.find(token=>token.id===f.ambassadorToken)!.zone,'placed');
});
}
for (const ambassadorFirst of [false,true]) {
void test(`native revival Sabotage discard retains ${ambassadorFirst?'completed':'pending'} original Ambassador opportunity`,()=>{
const f=fixture({kind:'sabotage',ecaz:true,ambassadorFirst});
let g=enter(f);
if (ambassadorFirst) g=declineAmbassador(g,f);
g=act(g,f.owner,f.revealAction);
assert.equal(g.pendingTreacheryDiscard ?? null,null);
assert.equal(g.pendingTerrorEntry?.stage,'gift');
assert.equal(g.pendingTerrorEntry?.revivalArrival?.event,f.event);
assert.equal(g.discard.filter(card=>card.id===f.karama).length,1);
assert.equal(player(g,f.entrant).hand.length,0);
g=act(g,f.owner,{type:'decision',decline:true});
if (!ambassadorFirst) g=declineAmbassador(g,f);
complete(g,f);
});
}
for (const advanced of [false,true]) {
void test(`${advanced?'Advanced':'Basic'} native revived Sneak preserves original Ambassador then opens its own BG and Ambassador arrivals`,()=>{
const f=fixture({advanced,kind:'sneakAttack',bg:true,ecaz:true}), source=enter(f);
assert.equal(source.pendingArrivalOverlap?.first,'terror');
let g=act(source,f.owner,f.revealAction);
g=act(g,f.owner,{type:'decision',amount:1});
assert.equal(g.decision?.kind,'ecazAmbassador');
assert.equal(g.pendingAmbassador?.entrant,f.entrant);
assert.equal(g.pendingArrivalReaction?.[0].revivalSneakAdvisor,true);
assert.equal(player(g,f.owner).forces[f.location],1);
g=declineAmbassador(g,f);
assert.equal(g.decision?.kind,'advisor');
if(g.decision?.kind!=='advisor')throw Error('Expected native Sneak Spiritual control');
assert.equal(g.decision.shipment,f.owner);
assert.equal(g.decision.revivalEvent,f.event);
assert.equal(g.decision.revivalTerror?.owner,f.owner);
g=act(g,f.bg!,{type:'decision',accept:true,accompany:advanced,amount:1});
assert.equal(g.response?.kind,'advisor');
assert.equal(g.response?.spiritualAdvisorSource?.revivalEvent,f.event);
g=pass(g);
assert.equal(g.decision?.kind,'ecazAmbassador');
assert.equal(g.pendingAmbassador?.entrant,f.owner);
const child=g.homeworldRevivalReturn!.ambassadors!.at(-1)!;
assert.equal(child.terror?.owner,f.owner);
assert.equal(child.terror?.amount,1);
g=declineAmbassador(g,f);
g=complete(g,f);
assert.equal(player(g,f.owner).reserves,13);
assert.equal(player(g,f.bg!).forces[advanced?f.location:'polar_sink:0'],advanced?1:2);
if(advanced)assert.equal(isAdvisor(player(g,f.bg!),f.territory),true);
assert.deepEqual(g.homeworldRevivalReturn!.ambassadors!.map(entry=>entry.entrant),[f.entrant,f.owner]);
});
}
void test('native original Karama stops only the Sneak-derived free BG packet, retaining both committed groups',()=>{
const f=fixture({kind:'sneakAttack',bg:true});
let g=enter(f);
g=act(g,f.owner,f.revealAction);
g=act(g,f.owner,{type:'decision',amount:1});
assert.equal(g.decision?.kind,'advisor');
g=act(g,f.bg!,{type:'decision',accept:true,accompany:true,amount:1});
assert.equal(g.response?.spiritualAdvisorSource?.revivalEvent,f.event);
g=act(g,f.holder,f.cancelAction);
g=complete(g,f);
assert.equal(player(g,f.owner).forces[f.location],1);
assert.equal(player(g,f.bg!).forces[f.location],undefined);
assert.equal(g.discard.filter(card=>card.id===f.karama).length,1);
assert.equal(viewGame(g,f.entrant).terrorEntry,null);
});