import assert from 'node:assert/strict'; import test from 'node:test'; import { viewGame, type Game } from '../game/engine'; import { act, advance, clean, createKullRicheseSemutaFixture, player } from './fixture-kull-richese-semuta'; /** These use the public Basic Kull/Richese fixture and its actual purchase of * Semuta. On turn two, select the unopened first ordinary lot so CHOAM cannot * fund an alleged second payment with a newly drawn Karama. No live purse, * hand, bidding state, response or pending transaction is staged. */ function auctionStart(winner:'choam'|'harkonnen') { const f=createKullRicheseSemutaFixture(); let game=advance(f.game,state=>state.turn===2 && state.phase===3 && !state.response && !state.phaseOpening && state.decision?.kind==='richeseDeclaration'); const round=game.richeseBidding; assert.ok(round); const nonKarama=game.deck.findIndex(card=>card.effect!=='karama'); assert.ok(nonKarama>=0); game.deck.unshift(game.deck.splice(nonKarama,1)[0]); game=act(game,'richese',{type:'decision',event:round.event,position:'last'}); game=advance(game,state=>state.phase===3 && !!state.auction && state.auction.active===winner && state.auction.bid===0 && clean(state)); assert.equal(game.auction?.cards[game.auction.index].effect==='karama',false); assert.ok(player(game,winner).spice>0); assert.ok(player(game,'choam').hand.some(card=>card.id===f.kull)); assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter)); return {...f,game}; } function paidIncome(winner:'choam'|'harkonnen') { const f=auctionStart(winner); let game=f.game; const paid=player(game,winner).spice, lot=game.auction!.cards[game.auction!.index].id; game=act(game,winner,{type:'bid',amount:paid,allyPayment:0}); game=advance(game,state=>state.response?.kind==='emperorIncome'); assert.equal(game.currentAuctionSale?.origin,'normal'); assert.equal(game.currentAuctionSale?.winner,winner); assert.equal(game.currentAuctionSale?.amount,paid); assert.equal(player(game,winner).spice,0); assert.ok(player(game,winner).hand.some(card=>card.id===lot)); assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter)); return {...f,game,paid,lot}; } function passDiscard(game:Game):Game { const pending=game.pendingTreacheryDiscard; assert.ok(pending?.reaction?.stage==='offer'); 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; } void test('CHOAM may use Kull after spending its entire winning bid',()=>{ const f=paidIncome('choam'); let game=f.game; const emperorBefore=player(game,'emperor').spice; assert.ok(!player(game,'choam').hand.some(card=>card.effect==='karama')); game=act(game,'harkonnen',{type:'card',mode:'cancel',card:f.counter}); const reaction=viewGame(game,'choam').kullReaction; assert.ok(reaction); assert.ok(reaction.plays.some(play=>play.source==='printed' && play.card.id===f.kull)); game=act(game,'choam',{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull}); game=advance(game,state=>!!state.pendingTreacheryDiscard); game=passDiscard(game); assert.equal(player(game,'choam').spice,0); assert.equal(player(game,'emperor').spice,emperorBefore+f.paid); assert.ok(player(game,'choam').hand.some(card=>card.id===f.lot)); assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter)); }); void test('a winner may attempt to cancel Emperor income after spending its entire bid',()=>{ const f=paidIncome('harkonnen'); let game=f.game; const emperorBefore=player(game,'emperor').spice; assert.ok(viewGame(game,'harkonnen').responseControls?.cancelCards.includes(f.counter)); game=act(game,'harkonnen',{type:'card',mode:'cancel',card:f.counter}); const reaction=viewGame(game,'choam').kullReaction; assert.ok(reaction); assert.ok(reaction.plays.some(play=>play.source==='printed' && play.card.id===f.kull)); game=act(game,'choam',{type:'kullDecision',event:reaction.event,decline:true}); game=advance(game,state=>!!state.pendingTreacheryDiscard); game=passDiscard(game); assert.equal(player(game,'harkonnen').spice,0); assert.equal(player(game,'emperor').spice,emperorBefore); assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.lot)); assert.ok(!player(game,'harkonnen').hand.some(card=>card.id===f.counter)); }); void test('an unpaid non-CHOAM Karama-dependent overbid remains rejected',()=>{ const f=auctionStart('harkonnen'), before=structuredClone(f.game); const amount=player(f.game,'harkonnen').spice+1; assert.throws(()=>act(f.game,'harkonnen',{type:'bid',amount,allyPayment:0}),/unfunded Karama-dependent overbid/); assert.deepEqual(f.game,before); });