Files
Dune/tests/kull-richese-revival-truth-semuta.test.ts
T
Quintine 4dfdd500e0
Verify and publish checkpoint container / container (push) Successful in 10m31s
Support Revival answers with Richese Semuta
2026-10-29 01:12:37 +11:00

113 lines
6.8 KiB
TypeScript

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<typeof createSemutaRevivalBenefitsForcesFixture> | 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.length<handLimit(seat)); seat.hand.push(card);
};
move(SEMUTA_DRUG_ID,'richese');
const truth=zones.flat().find(card=>card.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<game.players.length;count++) {
const reaction=game.pendingTreacheryDiscard!.reaction; assert.ok(reaction?.stage==='offer');
const actor=game.players.find(seat=>!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);
});