Files
Dune/tests/coalition-admission-joint-stay.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

138 lines
6.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { baseDeck } from '../game/cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
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)!;
/** Stage a quiet Basic Nexus, six wallets and two physical Truthtrance cards.
* Pair formation, questions, admission answers and payments are real actions. */
function fixture(): {game:Game; cards:string[]} {
let game = createGame('GROUPJOINTSTAY',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 = 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 propose(game:Game, actor:string):Game {
const pair = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(pair);
return act(game,actor,{type:'coalitionPropose',groupId:pair.id,entrant:'c',
expectedRevision:game.coalitionState!.revision});
}
function answer(game:Game, actor:string, accept:boolean):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,expectedRevision:game.coalitionState!.revision});
}
function live(game:Game) {
assert.ok(game.alliancePromises!.every(row=>!row.released));
assert.ok(game.bribePromises!.every(row=>!row.released));
}
void test('a jointly feasible Yes to an existing ally and outsider bribe survives group admission and payment',()=>{
const {cards,game:initial}=fixture();
let game=ask(initial,cards[0],{kind:'alliance',target:'a',turn:initial.turn,partner:'b'},'yes');
game=ask(game,cards[1],{kind:'bribe',target:'a',turn:game.turn,recipient:'e',minimum:2},'yes');
// Even an empty ledger switches accepted-order reconciliation to the whole
// turn planner; both previously accepted answers must remain binding.
game.turnActionPromises=[];
game=propose(game,'a');
game=answer(game,'b',true);
game=answer(game,'c',true);
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'a')!.members,['a','b','c']);
live(game);
const guide=viewGame(game,'a').bribePromiseGuide; assert.ok(guide);
assert.equal(guide.action.type,'bribe'); assert.equal(guide.action.target,'e');
game=act(game,'a',guide.action);
assert.equal(seat(game,'e').bribes,2);
assert.ok(game.bribePromises!.find(row=>row.player==='a')!.fulfilled);
live(game);
game=act(game,'a',{type:'ready'});
assert.ok(game.ready.includes('a'));
live(game);
});
void test('the respondent can decline admission that conflicts with simultaneous Yes and No',()=>{
const {cards,game:initial}=fixture();
let game=ask(initial,cards[0],{kind:'alliance',target:'a',turn:initial.turn,partner:'b'},'yes');
game=ask(game,cards[1],{kind:'alliance',target:'a',turn:game.turn,partner:'c'},'no');
// Stage an independently accepted ordinary bribe to exercise the joint
// reconciler without manufacturing a third physical Truthtrance card.
game.bribePromises=[{event:'joint-payment',player:'a',asker:'f',turn:game.turn,
recipient:'e',minimum:2,answer:true,progress:0}];
game.turnActionPromises=[];
game=propose(game,'b');
game=answer(game,'c',true);
const before=structuredClone(game);
assert.throws(()=>answer(game,'a',true));
assert.deepEqual(game,before);
live(game);
for (const difficulty of DIFFICULTIES) {
const view=viewGame(game,'a'); seatView(view,'a').bot=difficulty;
const action=botActions(view)[0]; assert.ok(action);
assert.equal(action.type,'coalitionAnswer'); assert.equal(action.accept,false);
assert.doesNotThrow(()=>act(game,'a',action));
}
game=answer(game,'a',false);
assert.equal(game.coalitionState!.proposal,null);
live(game);
});
function seatView(view:ReturnType<typeof viewGame>, id:string) {
const player=view.players.find(player=>player.id===id); assert.ok(player); return player;
}
void test('admission checks a matching alliance, bribe and shipment against one physical purse',()=>{
const {cards,game:initial}=fixture();
let game=ask(initial,cards[0],{kind:'alliance',target:'a',turn:initial.turn,partner:'b'},'yes');
game=ask(game,cards[1],{kind:'bribe',target:'a',turn:game.turn,recipient:'e',minimum:2},'yes');
// Stage an already accepted shipment, its respondent's purse and a spent
// charity opportunity; the available physical reserves come from newPlayer.
game.shipmentPromises=[{player:'a',asker:'f',turn:game.turn,territory:'carthag',minimum:3,answer:true}];
game.turnActionPromises=[];
seat(game,'a').charityTurn=game.turn;
seat(game,'a').spice=4; // Two spice for the bribe plus three to ship to Carthag.
const before=structuredClone(game);
assert.throws(()=>propose(game,'a'));
assert.deepEqual(game,before);
live(game);
assert.ok(!game.shipmentPromises![0].released);
seat(game,'a').spice=5;
game=propose(game,'a');
game=answer(game,'b',true);
game=answer(game,'c',true);
live(game);
assert.ok(!game.shipmentPromises![0].released);
const guide=viewGame(game,'a').bribePromiseGuide; assert.ok(guide);
game=act(game,'a',guide.action);
assert.equal(seat(game,'a').spice,3);
assert.equal(seat(game,'e').bribes,2);
assert.ok(game.bribePromises![0].fulfilled);
assert.ok(!game.shipmentPromises![0].released);
});