117 lines
6.3 KiB
TypeScript
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);
|
|
});
|