import assert from 'node:assert/strict'; import test from 'node:test'; import { createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine'; import { windowCardOpportunity } from '../game/window-card-play'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { act, advance, clean, inventory, player } from './fixture-kull-richese-semuta'; /** Genuine Basic setup and Richese cache purchase. Only unopened deal and * Spice positions are selected; all answers, payments and auctions are real. */ function source() { let game=createGame('KULLWHOLEAUCTION',newPlayer('choam','CHOAM','choam'),false,['choam','ix']); for (const id of ['richese','emperor','harkonnen','guild'] as const) joinGame(game,newPlayer(id,id,id)); game=act(game,'choam',{type:'moduleOption',module:'kullPreview',enabled:true}); for (const p of game.players) game=act(game,p.id,{type:'ready'}); game=act(game,'choam',{type:'start'}); assert.equal(game.status,'setup'); assert.ok(game.kullPreview && game.semutaPreview); const originalCards=inventory(game), selected:Game['deck']=[]; for (const kind of ['kull','filler','karama','karama','filler','truth'] 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 [kull,,karama,counter,,truth]=selected.map(card=>card.id); for (const [id,card] of [['choam',kull],['emperor',karama],['harkonnen',counter],['guild',truth]] as const) assert.ok(player(game,id).hand.some(row=>row.id===card)); 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=>player(state,'richese').hand.some(card=>card.id===SEMUTA_DRUG_ID) && !!state.auction && state.auction.active==='emperor' && clean(state)); const window=windowCardOpportunity(game,'karamaAuction'); assert.ok(window); const lot=game.auction!.cards[game.auction!.index].id; assert.deepEqual(inventory(game),originalCards); return {game,kull,karama,truth,window,lot,originalCards}; } function passDiscard(game:Game):Game { const pending=game.pendingTreacheryDiscard; assert.ok(pending?.reaction); const event=pending.batch.event; for (let count=0;game.pendingTreacheryDiscard?.batch.event===event && count!reaction.passed.includes(row.id)); assert.ok(seat); game=act(game,seat.id,{type:'semutaPass',event}); } assert.notEqual(game.pendingTreacheryDiscard?.batch.event,event); return game; } function answer(f:ReturnType,mixed:unknown,value:'yes'|'no') { let game=act(f.game,'guild',{type:'card',card:f.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}}); assert.ok(viewGame(game,'emperor').truthTurnActionAnswers?.includes(value)); game=act(game,'emperor',{type:'truthAnswer',answer:value}); const row=game.turnActionPromises!.at(-1); assert.ok(row); if (game.pendingTreacheryDiscard) game=passDiscard(game); return {game,event:row.event}; } void test('a whole-answer Karama purchase quote is independent of the unopened lot face',()=>{ const f=source(); const mixed={kind:'or',terms:[ {kind:'action',action:{kind:'cardPlay',effect:'ghola'}}, {kind:'action',action:{kind:'payment',recipient:'guild',minimum:2}}, ]}; const {game,event}=answer(f,mixed,'yes'); const makeVariant=(effect:'ghola'|'other'):Game=>{ const trial=structuredClone(game), auction=trial.auction; assert.ok(auction); const matches=(card:Game['deck'][number])=>effect==='ghola' ? card.effect==='ghola' : card.effect!=='ghola'; if (!matches(auction.cards[auction.index])) { const deckIndex=trial.deck.findIndex(matches); if (deckIndex>=0) [auction.cards[auction.index],trial.deck[deckIndex]]= [trial.deck[deckIndex],auction.cards[auction.index]]; else { const other=auction.cards.findIndex((card,index)=>index>auction.index && matches(card)); assert.ok(other>=0,'Need two original unopened faces for the two variants'); [auction.cards[auction.index],auction.cards[other]]=[auction.cards[other],auction.cards[auction.index]]; } } return trial; }; const variants=[makeVariant('ghola'),makeVariant('other')]; assert.notEqual(variants[0].auction!.cards[variants[0].auction!.index].id, variants[1].auction!.cards[variants[1].auction!.index].id); assert.equal(variants[0].auction!.cards[variants[0].auction!.index].effect,'ghola'); for (let trial of variants) { assert.deepEqual(inventory(trial),f.originalCards); assert.deepEqual(player(trial,'emperor').hand,player(game,'emperor').hand); trial=act(trial,'emperor',{type:'card',mode:'purchase',card:f.karama}); assert.ok(viewGame(trial,'choam').kullReaction); assert.ok(!trial.turnActionPromises!.find(row=>row.event===event)?.released); } });