Files
Dune/tests/coalition-joint-leave-payment-order.test.ts
T
Quintine 52d932002b
Verify and publish checkpoint container / container (push) Successful in 11m20s
Support group departures in joint commitment plans
2026-10-29 01:01:51 +11:00

83 lines
4.5 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { baseDeck } from '../game/cards';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
const act=(game:Game,actor:string,action:Action)=>applyAction(game,actor,action);
const seat=(game:Game,id:string)=>game.players.find(player=>player.id===id)!;
/** Only a quiet Basic Nexus, wallets, an exhausted charity opportunity and
* two physical Truthtrances are staged. A later purse reduction represents a
* foreign loss. All group and Truthtrance actions and both bribes are real. */
function fixture():{game:Game;cards:string[]} {
let game=createGame('GROUPPAYORDER',newPlayer('a','Atreides','atreides'),false);
game.players.push(newPlayer('b','Emperor','emperor'),newPlayer('c','Fremen','fremen'),
newPlayer('d','Harkonnen','harkonnen'),newPlayer('e','Guild','guild'),
newPlayer('f','Bene Gesserit','beneGesserit'));
Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,
order:['a','b','c','d','e','f'],ready:[],deck:baseDeck()});
game.coalitionState=emptyCoalitionState('unlimited');
seat(game,'a').spice=3; seat(game,'a').charityTurn=game.turn;
seat(game,'d').spice=8;
game=act(game,'a',{type:'alliance',target:'b'});
game=act(game,'b',{type:'alliance',target:'a'});
const cards:string[]=[];
for (let count=0;count<2;count++) {
const index=game.deck.findIndex(card=>card.effect==='truthtrance'); assert.ok(index>=0);
const [card]=game.deck.splice(index,1); cards.push(card.id); seat(game,'f').hand.push(card);
}
return {game,cards};
}
function ask(game:Game,card:string,question:Record<string,unknown>,value:'yes'|'no'):Game {
game=act(game,'f',{type:'card',card});
while (game.truthtrance?.stage==='priority') {
const next=game.players.find(player=>!game.truthtrance!.passed.includes(player.id)); assert.ok(next);
game=act(game,next.id,{type:'truthPass'});
}
game=act(game,'f',{type:'truthAsk',question});
return act(game,'a',{type:'truthAnswer',answer:value});
}
function answer(game:Game,actor:string):Game {
const offer=game.coalitionState!.proposal; assert.ok(offer);
return act(game,actor,{type:'coalitionAnswer',event:offer.event,nexus:offer.nexus,
turn:offer.turn,accept:true,expectedRevision:game.coalitionState!.revision});
}
function stillBinding(game:Game) {
assert.equal(game.alliancePromises!.find(row=>row.player==='a')!.released ?? false,false);
assert.equal(game.bribeExpressionPromises!.find(row=>row.player==='a')!.released ?? false,false);
}
void test('owned departure can precede payment to the former ally when it is the only affordable OR branch',()=>{
const {game:initial,cards}=fixture();
let game=ask(initial,cards[0],{kind:'alliance',target:'a',turn:initial.turn,partner:'d'},'no');
game=ask(game,cards[1],{kind:'bribeExpression',target:'a',turn:game.turn,claim:{kind:'or',terms:[
{kind:'payment',recipient:'d',minimum:1},{kind:'payment',recipient:'e',minimum:3}]}},'yes');
// An empty ledger activates joint reconciliation. The three-spice outsider
// branch is available when A consents to the group including D.
game.turnActionPromises=[];
const pair=coalitionOf(game.players,game.coalitionState,'a'); assert.ok(pair);
game=act(game,'a',{type:'coalitionPropose',groupId:pair.id,entrant:'d',
expectedRevision:game.coalitionState!.revision});
game=answer(game,'b'); game=answer(game,'d');
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'a')!.members,['a','b','d']);
stillBinding(game);
// Stage a loss after admission. E's branch now costs too much, but A can
// satisfy both answers by leaving and paying the former ally D one spice.
seat(game,'a').spice=1;
game=act(game,'d',{type:'bribe',target:'e',amount:1});
stillBinding(game);
const leave=viewGame(game,'a').alliancePromiseGuide; assert.ok(leave);
assert.equal(leave.action.type,'coalitionLeave');
game=act(game,'a',leave.action);
assert.equal(coalitionOf(game.players,game.coalitionState,'a'),null);
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'b')!.members,['b','d']);
stillBinding(game);
const payment=viewGame(game,'a').bribePromiseGuide; assert.ok(payment);
assert.equal(payment.action.type,'bribe'); assert.equal(payment.action.target,'d');
game=act(game,'a',payment.action);
assert.equal(seat(game,'d').bribes,1);
assert.ok(game.bribeExpressionPromises!.find(row=>row.player==='a')!.fulfilled);
stillBinding(game);
});