Files
Dune/tests/coalition-outsider-commitment-funding.test.ts
T
Quintine 0faa7923b0
Verify and publish checkpoint container / container (push) Successful in 10m51s
Allow group departures with bribe commitments
2026-10-29 00:46:59 +11:00

117 lines
6.3 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, id:string, action:Action) => applyAction(game,id,action);
const seat = (game:Game, id:string) => game.players.find(player => player.id === id)!;
/** The Basic Nexus/ledger, phase boundary and wallets are staged; the pair,
* admission, held physical Truthtrance and every question/payment are real. */
function fixture(phase: 'bidding' | 'movement'):Game {
let game=createGame('OUTSIDERFUND',newPlayer('a','Atreides','atreides'),false);
game.players.push(newPlayer('b','Emperor','emperor'),newPlayer('c','Fremen','fremen'),
newPlayer('d','Guild','guild'),newPlayer('e','Harkonnen','harkonnen'));
Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,order:['a','b','c','d','e'],
ready:[],storm:18,deck:baseDeck(),discard:[],response:null,decision:null,phaseOpening:null});
game.coalitionState=emptyCoalitionState('unlimited');
game=act(game,'a',{type:'alliance',target:'b'});
game=act(game,'b',{type:'alliance',target:'a'});
const group=coalitionOf(game.players,game.coalitionState,'a'); assert.ok(group);
game=act(game,'a',{type:'coalitionPropose',groupId:group.id,entrant:'c',
expectedRevision:game.coalitionState!.revision});
for(const id of ['b','c']) {
const offer=game.coalitionState!.proposal!;
game=act(game,id,{type:'coalitionAnswer',event:offer.event,nexus:offer.nexus,
turn:offer.turn,accept:true,expectedRevision:game.coalitionState!.revision});
}
for(const player of game.players) {
player.spice=10; player.hand=[]; player.forces={}; player.reserves=20; player.tanks=0;
player.traitors=[]; player.traitorChoices=[]; player.shipped=false; player.moved=0;
}
seat(game,'a').spice=2;
if(phase==='bidding') {
Object.assign(game,{phase:2,nexus:false,active:null,ready:[]});
for(const player of game.players) game=act(game,player.id,{type:'ready'});
assert.equal(game.phase,3); assert.equal(game.auction?.active,'a');
} else Object.assign(game,{phase:5,nexus:false,active:'a',ready:[],
movementRemaining:['a','b','c','d','e'],guildTimingGranted:false,guildTimingLocked:false});
const index=game.deck.findIndex(card=>card.effect==='truthtrance'); assert.ok(index>=0);
seat(game,'a').hand.push(game.deck.splice(index,1)[0]);
return game;
}
function ask(game:Game,question:Record<string,unknown>):Game {
const card=seat(game,'a').hand.find(row=>row.effect==='truthtrance'); assert.ok(card);
game=act(game,'a',{type:'card',card:card.id});
while(game.truthtrance?.stage==='priority') {
const actor=game.players.find(player=>!game.truthtrance!.passed.includes(player.id)); assert.ok(actor);
game=act(game,actor.id,{type:'truthPass'});
}
return act(game,'a',{type:'truthAsk',question});
}
function pledge(game:Game):Game {
game=act(game,'b',{type:'coalitionPledge',recipient:'a',amount:4});
return act(game,'c',{type:'coalitionPledge',recipient:'a',amount:3});
}
function passResponses(game:Game):Game {
for(let count=0;game.response && count<30;count++) {
const actor=game.players.find(player=>!game.response!.passed.includes(player.id)); assert.ok(actor);
game=act(game,actor.id,{type:'passResponse'});
}
assert.equal(game.response ?? null,null); return game;
}
const shipment:Action={type:'ship',territory:'imperial_basin',sector:9,amount:3,elite:0,allyPayment:0,
coalitionContributions:[{donor:'b',amount:3},{donor:'c',amount:2}]};
void test('an unrelated outsider bribe No leaves group shipment funding available',()=>{
let game=fixture('movement');
game=ask(game,{kind:'bribe',target:'d',turn:game.turn,recipient:'e',minimum:1});
assert.ok(viewGame(game,'d').truthBribeAnswers?.includes('no'));
game=act(game,'d',{type:'truthAnswer',answer:'no'});
const promise=game.bribePromises!.find(row=>row.player==='d')!;
assert.equal(promise.fulfilled ?? false,false);
game=pledge(game);
assert.equal(viewGame(game,'a').coalitionFunding?.incoming.length,2);
game=passResponses(act(game,'a',shipment));
assert.equal(seat(game,'a').forces['imperial_basin:9'],3);
assert.equal(seat(game,'d').spice,16);
assert.ok(game.bribePromises?.some(row=>row.event===promise.event && !row.fulfilled && !row.released));
});
void test('an unrelated outsider whole-turn payment No leaves group shipment funding available',()=>{
let game=fixture('movement');
game=ask(game,{kind:'turnActions',target:'d',turn:game.turn,
mixed:{kind:'action',action:{kind:'payment',recipient:'e',minimum:1}}});
assert.ok(viewGame(game,'d').truthTurnActionAnswers?.includes('no'));
game=act(game,'d',{type:'truthAnswer',answer:'no'});
const promise=game.turnActionPromises!.find(row=>row.player==='d')!;
game=pledge(game); game=passResponses(act(game,'a',shipment));
assert.equal(seat(game,'a').forces['imperial_basin:9'],3);
assert.ok(game.turnActionPromises?.some(row=>row.event===promise.event && !row.fulfilled && !row.released));
});
void test('an unrelated outsider phase-bid No leaves group bid funding available',()=>{
let game=fixture('bidding');
game=ask(game,{kind:'phaseBid',target:'d',turn:game.turn,minimum:10});
assert.ok(viewGame(game,'d').truthPhaseBidAnswers?.includes('no'));
game=act(game,'d',{type:'truthAnswer',answer:'no'});
const promise=game.phaseBidPromises!.find(row=>row.player==='d')!;
game=pledge(game);
game=act(game,'a',{type:'bid',amount:6,allyPayment:0,
coalitionContributions:[{donor:'b',amount:3},{donor:'c',amount:2}]});
assert.equal(game.auction?.bidder,'a');
assert.ok(game.phaseBidPromises?.some(row=>row.event===promise.event && !row.fulfilled && !row.released));
});
void test('a group member commitment still fences the entire group funding path',()=>{
let game=fixture('movement');
game=ask(game,{kind:'bribe',target:'b',turn:game.turn,recipient:'e',minimum:1});
assert.ok(viewGame(game,'b').truthBribeAnswers?.includes('no'));
game=act(game,'b',{type:'truthAnswer',answer:'no'});
assert.equal(viewGame(game,'a').coalitionFunding,null);
const before=structuredClone(game);
assert.throws(()=>act(game,'c',{type:'coalitionPledge',recipient:'a',amount:1}));
assert.deepEqual(game,before);
});