import assert from 'node:assert/strict'; import test from 'node:test'; import { handLimit, viewGame, type Game } from '../game/engine'; import { SEMUTA_DRUG_ID } from '../game/semuta-drug'; import { act, advance, attempt, clean, createKullRicheseSemutaFixture, inventory, player } from './fixture-kull-richese-semuta'; /** Genuine Basic setup, real cache purchase and movement window. Only the * existing physical counter Karama is moved from Harkonnen to Richese and * an original Truthtrance is moved to Harkonnen. No purse, force, pending * response, clock, promise or fresh discard is installed. */ function source() { const f=createKullRicheseSemutaFixture(), game=structuredClone(f.game); const zones=[game.deck,game.discard,...(game.richeseCache?[game.richeseCache]:[]), ...(game.richeseRemoved?[game.richeseRemoved]:[]),...game.players.map(seat=>seat.hand)]; const move=(id:string,recipient:string)=>{ const zone=zones.find(cards=>cards.some(card=>card.id===id)); assert.ok(zone); const [card]=zone.splice(zone.findIndex(row=>row.id===id),1); const seat=player(game,recipient); assert.ok(seat.hand.lengthcard.effect==='truthtrance'); assert.ok(truth); move(truth.id,'harkonnen'); assert.ok(player(game,'richese').hand.some(card=>card.id===SEMUTA_DRUG_ID)); assert.deepEqual(inventory(game),f.originalCards); return {...f,game,truth:truth.id}; } function passDiscard(game:Game, card:string):Game { const batch=game.pendingTreacheryDiscard; assert.ok(batch?.reaction?.stage==='offer'); assert.ok(batch.batch.entries.some(entry=>entry.card.id===card)); const event=batch.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('Richese may counter Kull then use Semuta on the resumed original Karama while preserving a whole answer',()=>{ const f=source(); let game=act(f.game,'harkonnen',{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'}); } const mixed={kind:'or',terms:[ {kind:'action',action:{kind:'cardPlay',effect:'semutaDrug'}}, {kind:'action',action:{kind:'payment',recipient:'emperor',minimum:1}}, ]}; game=act(game,'harkonnen',{type:'truthAsk',question:{kind:'turnActions',target:'richese',turn:game.turn,mixed}}); assert.ok(viewGame(game,'richese').truthTurnActionAnswers?.includes('yes')); game=act(game,'richese',{type:'truthAnswer',answer:'yes'}); const promise=game.turnActionPromises?.at(-1); assert.ok(promise); const event=promise.event; // A currently affordable payment is a real alternative, so passing the // Truth discard does not evade the complete OR answer. game=passDiscard(game,f.truth); game=act(game,'emperor',attempt(f)); const reaction=viewGame(game,'choam').kullReaction; assert.ok(reaction); game=act(game,'choam',{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull}); assert.ok(viewGame(game,'richese').responseControls?.cancelCards?.includes(f.counter)); game=act(game,'richese',{type:'card',mode:'cancel',card:f.counter}); assert.ok(!game.turnActionPromises!.find(row=>row.event===event)?.released); // The physical counter belongs to Richese and cannot be its own Semuta // target. Its neutral discard completes first, then the suspended original // Emperor Karama produces a distinct, claimable fresh discard. game=passDiscard(game,f.counter); if (!game.pendingTreacheryDiscard) game=advance(game,state=>!!state.pendingTreacheryDiscard); const pending=game.pendingTreacheryDiscard; assert.ok(pending?.reaction?.stage==='offer'); assert.ok(pending.batch.entries.some(entry=>entry.card.id===f.karama)); assert.equal(viewGame(game,'richese').semutaReaction?.canCommit,true); game=act(game,'richese',{type:'semutaCommit',event:pending.batch.event}); if (game.pendingTreacheryDiscard?.reaction?.stage==='select') game=act(game,'richese',{type:'semutaSelect',event:pending.batch.event,card:f.karama}); game=advance(game,clean); const result=game.turnActionPromises!.find(row=>row.event===event); assert.ok(result); assert.equal(result.cardPlays?.semutaDrug?.played,true); assert.equal(result.fulfilled,true); assert.ok(player(game,'richese').hand.some(card=>card.id===f.karama)); assert.equal(game.karamaShipping?.owner,'emperor'); assert.deepEqual(inventory(game),f.originalCards); });