113 lines
6.8 KiB
TypeScript
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);
|
|
});
|