Allow group departures with bribe commitments
Verify and publish checkpoint container / container (push) Successful in 10m51s
Verify and publish checkpoint container / container (push) Successful in 10m51s
This commit is contained in:
1 parent
3e0fa2a8a2
commit
0faa7923b0
13 files changed
+437
-37
No files matched your search
@@ -0,0 +1,116 @@
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user