113 lines
6.1 KiB
TypeScript
113 lines
6.1 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)!;
|
|
|
|
/** The unfinished group mode is staged in a quiet Basic Nexus, along with
|
|
* wallets and one physical Truthtrance card. Group formation, deal consent,
|
|
* the question, admission answers and both payments are real actions. */
|
|
function fixture(): Game {
|
|
let game = createGame('GROUPDEALTRUTH', 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');
|
|
for (const player of game.players) player.spice = 8;
|
|
game = act(game,'a',{type:'alliance',target:'b'});
|
|
game = act(game,'b',{type:'alliance',target:'a'});
|
|
const pair = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(pair);
|
|
game = act(game,'a',{type:'coalitionPropose',groupId:pair.id,entrant:'c',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
game = answer(game,'b',true);
|
|
game = answer(game,'c',true);
|
|
const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0);
|
|
const [card] = game.deck.splice(index,1); seat(game,'f').hand.push(card);
|
|
return game;
|
|
}
|
|
function answer(game: Game, actor: string, accept: boolean): Game {
|
|
const proposal = game.coalitionState!.proposal; assert.ok(proposal);
|
|
return act(game,actor,{type:'coalitionAnswer',event:proposal.event,nexus:proposal.nexus,
|
|
turn:proposal.turn,accept,expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
function propose(game: Game, actor: string): Game {
|
|
const group = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(group);
|
|
return act(game,actor,{type:'coalitionPropose',groupId:group.id,entrant:'d',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
}
|
|
function reject(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game);
|
|
assert.throws(()=>act(game,actor,action));
|
|
assert.deepEqual(game,before,'A rejected action must leave the source and obligations intact.');
|
|
}
|
|
function acceptAction(game: Game): Action {
|
|
const proposal = game.coalitionState!.proposal; assert.ok(proposal);
|
|
return {type:'coalitionAnswer',event:proposal.event,nexus:proposal.nexus,
|
|
turn:proposal.turn,accept:true,expectedRevision:game.coalitionState!.revision};
|
|
}
|
|
function offerDeal(game: Game): {game: Game; id: string} {
|
|
game = act(game,'e',{type:'dealOffer',target:'a',terms:'Pay for information this turn.',
|
|
payment:{kind:'deferredBribe',payer:'a',recipient:'e',amount:2,dueTurn:game.turn}});
|
|
const id = game.deals!.at(-1)!.id;
|
|
game = act(game,'a',{type:'dealRespond',deal:id,accept:true});
|
|
assert.equal(game.deals!.find(deal=>deal.id===id)!.paymentStatus,'pending');
|
|
return {game,id};
|
|
}
|
|
function promise(game: Game, recipient: string): Game {
|
|
const card = seat(game,'f').hand.find(candidate=>candidate.effect==='truthtrance'); assert.ok(card);
|
|
game = act(game,'f',{type:'card',card:card.id});
|
|
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:{kind:'bribe',target:'a',turn:game.turn,recipient,minimum:2}});
|
|
assert.ok(viewGame(game,'a').truthBribeAnswers?.includes('yes'));
|
|
return act(game,'a',{type:'truthAnswer',answer:'yes'});
|
|
}
|
|
function admit(game: Game): Game {
|
|
game = propose(game,'b');
|
|
for (const actor of ['a','c','d']) game = answer(game,actor,true);
|
|
assert.deepEqual(coalitionOf(game.players,game.coalitionState,'a')!.members,['a','b','c','d']);
|
|
return game;
|
|
}
|
|
|
|
void test('an outsider deal does not hide a separate Yes that admission would make unpayable',()=>{
|
|
const offered = offerDeal(fixture());
|
|
let game = promise(offered.game,'d');
|
|
const group = coalitionOf(game.players,game.coalitionState,'a'); assert.ok(group);
|
|
reject(game,'a',{type:'coalitionPropose',groupId:group.id,entrant:'d',
|
|
expectedRevision:game.coalitionState!.revision});
|
|
game = propose(game,'b');
|
|
reject(game,'a',acceptAction(game));
|
|
assert.equal(game.deals!.find(deal=>deal.id===offered.id)!.paymentStatus,'pending');
|
|
assert.ok(game.bribePromises!.some(row=>row.player==='a' && !row.fulfilled && !row.released));
|
|
game = answer(game,'a',false);
|
|
game = act(game,'a',{type:'dealPay',deal:offered.id});
|
|
assert.equal(game.deals!.find(deal=>deal.id===offered.id)!.paymentStatus,'paid');
|
|
assert.ok(game.bribePromises!.some(row=>row.player==='a' && !row.fulfilled && !row.released));
|
|
game = act(game,'a',{type:'bribe',target:'d',amount:2});
|
|
assert.ok(game.bribePromises!.some(row=>row.player==='a' && row.fulfilled));
|
|
game = admit(game);
|
|
assert.equal(seat(game,'e').bribes,2);
|
|
assert.equal(seat(game,'d').bribes,2);
|
|
});
|
|
|
|
void test('admission preserves an outsider deal that also fulfills the matching Yes',()=>{
|
|
const offered = offerDeal(fixture());
|
|
let game = promise(offered.game,'e');
|
|
game = admit(game);
|
|
assert.equal(game.deals!.find(deal=>deal.id===offered.id)!.paymentStatus,'pending');
|
|
assert.ok(game.bribePromises!.some(row=>row.player==='a' && !row.fulfilled && !row.released));
|
|
assert.equal(viewGame(game,'a').deferredDealPaymentOptions[offered.id]?.blocked,null);
|
|
game = act(game,'a',{type:'dealPay',deal:offered.id});
|
|
assert.equal(game.deals!.find(deal=>deal.id===offered.id)!.paymentStatus,'paid');
|
|
assert.ok(game.bribePromises!.some(row=>row.player==='a' && row.fulfilled));
|
|
assert.equal(seat(game,'a').spice,6);
|
|
assert.equal(seat(game,'e').bribes,2);
|
|
});
|