import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { BribePromiseGuideButton } from '../components/bribe-promises'; import { mixedBribeInputError } from '../components/mixed-bribe-fields'; 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); /** Group entry is staged at a Basic Nexus. Pair/group formation, physical * Truthtrance transfer, declaration, priority and questions use native actions. */ function source() { let game = createGame('GROUPTRUTHBRIBE',newPlayer('a','Atreides','atreides'),false); game.players.push(newPlayer('e','Emperor','emperor'),newPlayer('f','Fremen','fremen'), newPlayer('h','Harkonnen','harkonnen')); Object.assign(game,{status:'playing',phase:1,turn:2,nexus:true,order:['a','e','f','h'], ready:[],response:null,decision:null,phaseOpening:null,deck:baseDeck()}); game.coalitionState = emptyCoalitionState(); game = act(game,'a',{type:'alliance',target:'e'}); game = act(game,'e',{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:'f',expectedRevision:game.coalitionState!.revision}); for (const id of ['e','f']) { const invitation = game.coalitionState!.proposal!; game = act(game,id,{type:'coalitionAnswer',event:invitation.event,nexus:invitation.nexus, turn:invitation.turn,accept:true,expectedRevision:game.coalitionState!.revision}); } Object.assign(game,{phase:3,nexus:false,active:'h',ready:[]}); for (const player of game.players) { player.spice = 10; player.hand = []; } const index = game.deck.findIndex(card => card.effect === 'truthtrance'); assert.ok(index >= 0); const [truth] = game.deck.splice(index,1); game.players.find(player => player.id === 'h')!.hand.push(truth); game = act(game,'h',{type:'card',card:truth.id}); for (let count=0; game.truthtrance?.stage === 'priority' && count < game.players.length; count++) { const next = game.players.find(player => !game.truthtrance!.passed.includes(player.id)); assert.ok(next); game = act(game,next.id,{type:'truthPass'}); } assert.equal(game.truthtrance?.stage,'ask'); return game; } const question = (recipient:string) => ({type:'truthAsk',question:{kind:'bribe',target:'a',turn:2,recipient,minimum:3}} as Action); void test('group allies cannot be named in a native Truthtrance bribe question, but an outsider can', () => { let game = source(); for (const recipient of ['e','f']) { const before = structuredClone(game); assert.throws(() => act(game,'h',question(recipient))); assert.deepEqual(game,before); } game = act(game,'h',question('h')); assert.deepEqual(viewGame(game,'a').truthBribeAnswers,['yes','no']); game = act(game,'a',{type:'truthAnswer',answer:'yes'}); const view = viewGame(game,'a'); assert.equal(view.bribePromiseGuide?.action.target,'h'); const html = renderToStaticMarkup(createElement(BribePromiseGuideButton,{game:view,busy:false,send() {}})); assert.match(html,/Pay 3 spice to Harkonnen/); assert.doesNotMatch(html,/disabled=""/); game = act(game,'a',view.bribePromiseGuide!.action); assert.equal(game.players.find(player => player.id === 'a')!.spice,7); assert.equal(game.players.find(player => player.id === 'h')!.bribes,3); }); void test('mixed bribe fields reject group allies while accepting the outsider', () => { const view = viewGame(source(),'h'); const expression = (recipient:string) => ({kind:'and',terms:[ {kind:'fact',fact:{kind:'spice',compare:'gte',value:1}}, {kind:'action',action:{recipient,minimum:3}}, ]}); assert.ok(mixedBribeInputError(expression('e'),view,'a')); assert.ok(mixedBribeInputError(expression('f'),view,'a')); assert.equal(mixedBribeInputError(expression('h'),view,'a'),null); });