import assert from 'node:assert/strict'; import test from 'node:test'; import { createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine'; import { act, advance, clean, inventory, player } from './fixture-kull-native-ix'; /** Genuine public Basic setup and movement; only unopened card and Spice * order is selected. The live whole answer and both overlapping response * windows arise through original actions, not installed pending state. */ function source() { let game=createGame('KULLBGGUILD',newPlayer('choam','CHOAM','choam'),false,['choam','ix']); for (const id of ['emperor','beneGesserit','guild','harkonnen'] as const) joinGame(game,newPlayer(id,id,id)); game=act(game,'choam',{type:'moduleOption',module:'kullPreview',enabled:true}); for (const seat of game.players) game=act(game,seat.id,{type:'ready'}); game=act(game,'choam',{type:'start'}); assert.equal(game.status,'setup'); assert.ok(game.kullPreview); const originalCards=inventory(game), selected:Game['deck']=[]; for (const kind of ['kull','karama','filler','truth','karama','filler'] as const) { const index=game.deck.findIndex(card=>kind==='kull'?card.id==='ix-kull-wahad': kind==='truth'?card.effect==='truthtrance':kind==='karama'?card.effect==='karama': card.kind==='worthless' && card.id!=='ix-kull-wahad'); assert.ok(index>=0); selected.push(game.deck.splice(index,1)[0]); } game.deck.unshift(...selected); game=advance(game,state=>state.status==='playing'); const [,karama,,truth]=selected.map(card=>card.id); assert.ok(player(game,'emperor').hand.some(card=>card.id===karama)); assert.ok(player(game,'guild').hand.some(card=>card.id===truth)); const safe=game.spiceDeck.findIndex(card=>'territory' in card && !card.discovery); assert.ok(safe>=0); game.spiceDeck.unshift(game.spiceDeck.splice(safe,1)[0]); game=advance(game,state=>state.phase===2 && clean(state) && state.choamCharity?.turn===state.turn); game=act(game,'guild',{type:'card',card:truth}); while (game.truthtrance?.stage==='priority') { const seat=game.players.find(row=>!game.truthtrance!.passed.includes(row.id)); assert.ok(seat); game=act(game,seat.id,{type:'truthPass'}); } game=act(game,'guild',{type:'truthAsk',question:{kind:'turnActions',target:'emperor',turn:game.turn, mixed:{kind:'action',action:{kind:'payment',recipient:'harkonnen',minimum:1}}}}); assert.ok(viewGame(game,'emperor').truthTurnActionAnswers?.includes('yes')); game=act(game,'emperor',{type:'truthAnswer',answer:'yes'}); const promise=game.turnActionPromises?.at(-1); assert.ok(promise); game=advance(game,state=>state.phase===5 && state.active==='emperor' && clean(state)); game=act(game,'emperor',{type:'ship',territory:'polar_sink',sector:0,amount:1,elite:0,allyPayment:0}); assert.equal(game.response?.kind,'guildIncome'); assert.equal(game.decision?.kind,'advisor'); assert.ok(!promise.released); return {game,karama,event:promise.event,originalCards}; } void test('live whole-answer preflight fences Guild income cancellation without consuming a simultaneous BG arrival decision',()=>{ const f=source(); let game=f.game; const before=structuredClone(game); assert.ok(!viewGame(game,'emperor').responseControls?.cancelCards.includes(f.karama)); assert.throws(()=>act(game,'emperor',{type:'card',mode:'cancel',card:f.karama})); assert.deepEqual(game,before); assert.equal(game.pendingKull ?? null,null); assert.deepEqual(game.decision,before.decision); assert.ok(game.turnActionPromises?.some(row=>row.event===f.event && !row.released)); for (let count=0;game.response?.kind==='guildIncome' && count!game.response!.passed.includes(row.id)); assert.ok(seat); game=act(game,seat.id,{type:'passResponse'}); } assert.equal(game.response,null); assert.deepEqual(game.decision,before.decision); game=act(game,'beneGesserit',{type:'decision',accept:false}); assert.equal(game.decision,null); assert.deepEqual(inventory(game),f.originalCards); });