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); });