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, finishRevival, inventory, player } from './fixture-kull-revival-semuta'; import { createSemutaRevivalBenefitsForcesFixture } from './fixture-semuta-revival-benefits-forces'; /** Real Basic setup, cache purchase, shipment, Storm losses and Revival * window. Only original Truth and Semuta cards are reassigned. The no-Kull * variant stages the already available option off; leader cases stage the * original five-leader cohort dead so a normal CHOAM return is legal. * No forces, wallet or pending actions are staged. */ let nativeSource: ReturnType | undefined; function source(kull:boolean, leaderNeeded:boolean) { const fixture=nativeSource ??= createSemutaRevivalBenefitsForcesFixture({kind:'choamRevival',advanced:false}); const game=structuredClone(fixture.beforeRevival); assert.ok(game.kullPreview && game.semutaPreview); if (!kull) game.kullPreview=false; const originalCards=inventory(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,'atreides'); const leaders=player(game,'choam').leaders; const leader=[...leaders].sort((a,b)=>a.strength-b.strength)[0]; assert.ok(leader); if (leaderNeeded) for (const row of leaders) {row.dead=true; row.deaths=1;} assert.deepEqual(inventory(game),originalCards); return {game,truth:truth.id,leader:leader.id,originalCards}; } function react(game:Game,truth:string,claim:boolean):Game { const pending=game.pendingTreacheryDiscard; assert.ok(pending); assert.equal(pending.reaction?.stage,'offer'); assert.ok(pending.batch.entries.some(entry=>entry.card.id===truth)); const event=pending.batch.event; if (claim) { assert.equal(viewGame(game,'richese').semutaReaction?.canCommit,true); game=act(game,'richese',{type:'semutaCommit',event}); if (game.pendingTreacheryDiscard?.reaction?.stage==='select') game=act(game,'richese',{type:'semutaSelect',event,card:truth}); } else for (let count=0;game.pendingTreacheryDiscard?.batch.event===event && count!reaction.passed.includes(seat.id)); assert.ok(actor); game=act(game,actor.id,{type:'semutaPass',event}); } assert.notEqual(game.pendingTreacheryDiscard?.batch.event,event); return game; } for (const kull of [false,true]) for (const kind of ['revival','revivalExpression','mixedRevival','leaderRevival'] as const) for (const claim of [false,true]) void test(`${kull?'Kull':'non-Kull'} ${kind} remains bound after Semuta ${claim?'claims':'passes'} Truthtrance`,()=>{ const f=source(kull,kind==='leaderRevival'); let game=act(f.game,'atreides',{type:'card',card:f.truth}); while (game.truthtrance?.stage==='priority') { const next=game.players.find(seat=>!game.truthtrance!.passed.includes(seat.id)); assert.ok(next); game=act(game,next.id,{type:'truthPass'}); } const forces={kind:'forces' as const,counter:'total' as const,minimum:1}; const question=kind==='revival' ? {kind,target:'choam',turn:game.turn,counter:'total',minimum:1} : kind==='revivalExpression' ? {kind,target:'choam',turn:game.turn,claim:forces} : kind==='leaderRevival' ? {kind,target:'choam',turn:game.turn,leader:f.leader} : {kind,target:'choam',turn:game.turn,mixed:{kind:'and',terms:[ {kind:'fact',fact:{kind:'spice',compare:'eq',value:player(game,'choam').spice}}, {kind:'action',action:forces}, ]}}; game=act(game,'atreides',{type:'truthAsk',question}); const view=viewGame(game,'choam'); const answers=kind==='revival'?view.truthRevivalAnswers: kind==='revivalExpression'?view.truthRevivalExpressionAnswers: kind==='mixedRevival'?view.truthMixedRevivalAnswers:view.truthLeaderRevivalAnswers; assert.ok(answers?.includes('yes')); game=act(game,'choam',{type:'truthAnswer',answer:'yes'}); game=react(game,f.truth,claim); const after=viewGame(game,'choam'); const guide=kind==='revival'?after.revivalPromiseGuide: kind==='leaderRevival'?after.leaderRevivalPromiseGuide:after.revivalExpressionPromiseGuide; assert.ok(guide); game=act(game,'choam',guide.action); game=finishRevival(game); if (kind==='revival') assert.ok(game.revivalPromises?.some(row=>row.player==='choam' && row.fulfilled)); else if (kind==='leaderRevival') assert.ok(game.leaderRevivalPromises?.some(row=>row.player==='choam' && row.fulfilled)); else assert.ok(game.revivalExpressionPromises?.some(row=>row.player==='choam' && row.fulfilled)); assert.equal(player(game,'richese').hand.some(card=>card.id===f.truth),claim); assert.deepEqual(inventory(game),f.originalCards); }); for (const kull of [false,true]) for (const claim of [false,true]) void test(`${kull?'Kull':'non-Kull'} constant mixed Revival survives Semuta ${claim?'claim':'pass'} without a residual promise`,()=>{ const f=source(kull,false); let game=act(f.game,'atreides',{type:'card',card:f.truth}); while (game.truthtrance?.stage==='priority') { const next=game.players.find(seat=>!game.truthtrance!.passed.includes(seat.id)); assert.ok(next); game=act(game,next.id,{type:'truthPass'}); } game=act(game,'atreides',{type:'truthAsk',question:{kind:'mixedRevival',target:'choam',turn:game.turn, mixed:{kind:'or',terms:[ {kind:'fact',fact:{kind:'spice',compare:'eq',value:player(game,'choam').spice}}, {kind:'action',action:{kind:'forces',counter:'total',minimum:1}}, ]}}}); assert.ok(viewGame(game,'choam').truthMixedRevivalAnswers?.includes('yes')); game=act(game,'choam',{type:'truthAnswer',answer:'yes'}); const record=game.mixedRevivalQuestions?.at(-1); assert.ok(record); assert.equal(record.binding,false); assert.ok(!game.revivalExpressionPromises?.some(row=>row.event===record.event)); game=react(game,f.truth,claim); assert.deepEqual(game.mixedRevivalQuestions?.at(-1),record); assert.equal(player(game,'richese').hand.some(card=>card.id===f.truth),claim); assert.deepEqual(inventory(game),f.originalCards); });