144 lines
8.1 KiB
TypeScript
144 lines
8.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { handLimit, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
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';
|
|
|
|
/** The native source purchases Semuta, ships four forces and loses them to
|
|
* Storm before this real Revival opportunity. As in the existing Kull
|
|
* Semuta fixture, only original physical cards are reassigned among zones;
|
|
* no phase, wallet, force, promise, response or pending revival is installed. */
|
|
let nativeSource: ReturnType<typeof createSemutaRevivalBenefitsForcesFixture> | undefined;
|
|
function fixture(answer: 'yes' | 'no' = 'yes') {
|
|
const source = nativeSource ??= createSemutaRevivalBenefitsForcesFixture({kind:'choamRevival', advanced:false});
|
|
let game = structuredClone(source.beforeRevival);
|
|
assert.ok(game.kullPreview && game.semutaPreview);
|
|
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, id);
|
|
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');
|
|
move('ix-kull-wahad', 'choam');
|
|
const truth = zones.flat().find(card=>card.effect === 'truthtrance'); assert.ok(truth);
|
|
move(truth.id, 'atreides');
|
|
const counter = zones.flat().find(card=>card.effect === 'karama' && card.id !== source.cost &&
|
|
!card.id.startsWith('richese-')); assert.ok(counter);
|
|
move(counter.id, 'tleilaxu');
|
|
assert.deepEqual(inventory(game),originalCards);
|
|
assert.equal(player(game,'choam').tanks,4);
|
|
game = act(game,'atreides',{type:'card',card:truth.id});
|
|
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:'revival',target:'choam',
|
|
turn:game.turn,counter:'total',minimum:3}});
|
|
assert.ok(viewGame(game,'choam').truthRevivalAnswers?.includes(answer));
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game,'choam'); view.players.find(seat=>seat.id === 'choam')!.bot = difficulty;
|
|
const action = botActions(view)[0]; assert.ok(action,difficulty);
|
|
assert.equal(action.type,'truthAnswer',difficulty);
|
|
assert.ok(view.truthRevivalAnswers?.includes(action.answer as 'yes'|'no'),difficulty);
|
|
assert.doesNotThrow(()=>act(game,'choam',action),difficulty);
|
|
}
|
|
game = act(game,'choam',{type:'truthAnswer',answer});
|
|
const promise = game.revivalPromises!.find(row=>row.player === 'choam'); assert.ok(promise);
|
|
// Pass the physical Truthtrance's Semuta reaction, retaining Semuta for the
|
|
// subsequent Kull or counter discard.
|
|
if (game.pendingTreacheryDiscard) game = semuta(game,truth.id,false);
|
|
assert.ok(game.revivalPromises?.some(row=>row.event === promise.event && !row.released));
|
|
return {game,source,originalCards,event:promise.event,kull:'ix-kull-wahad',
|
|
karama:source.cost,counter:counter.id};
|
|
}
|
|
|
|
function semuta(game:Game, card:string, claim:boolean):Game {
|
|
const pending = game.pendingTreacheryDiscard; assert.ok(pending);
|
|
assert.equal(pending.reaction?.stage,'offer');
|
|
assert.equal(pending.batch.entries[0].card.id,card);
|
|
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});
|
|
} 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 next = game.players.find(seat=>!reaction.passed.includes(seat.id)); assert.ok(next);
|
|
game = act(game,next.id,{type:'semutaPass',event});
|
|
}
|
|
}
|
|
assert.notEqual(game.pendingTreacheryDiscard?.batch.event,event);
|
|
return game;
|
|
}
|
|
|
|
function pending(f:ReturnType<typeof fixture>) {
|
|
let game = f.game;
|
|
const guide = viewGame(game,'choam').revivalPromiseGuide; assert.ok(guide);
|
|
assert.equal(guide.action.type,'revive'); assert.equal(guide.action.amount,3);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(game,'choam'); view.players.find(seat=>seat.id === 'choam')!.bot = difficulty;
|
|
const action = botActions(view).find(row=>row.revivalPromiseEvent === guide.action.revivalPromiseEvent);
|
|
assert.ok(action,difficulty); assert.doesNotThrow(()=>act(game,'choam',action),difficulty);
|
|
}
|
|
game = act(game,'choam',guide.action);
|
|
for (let count=0;count<20 && game.response?.kind !== 'choamRevival';count++) {
|
|
if (game.decision?.kind === 'revivalStop' || game.decision?.kind === 'choamFreeRevival')
|
|
game = act(game,game.decision.player,{type:'decision',decline:true});
|
|
else throw Error('The original revival did not reach its native benefit response.');
|
|
}
|
|
assert.equal(game.response?.kind,'choamRevival'); assert.ok(game.pendingRevival);
|
|
game = act(game,'emperor',{type:'card',mode:'cancel',card:f.karama});
|
|
assert.equal(game.pendingKull?.stage,'offer');
|
|
const reaction = viewGame(game,'choam').kullReaction; assert.ok(reaction);
|
|
game = act(game,'choam',{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull});
|
|
assert.equal(game.pendingKull?.stage,'counter');
|
|
return game;
|
|
}
|
|
|
|
for (const claim of [false,true]) void test(`Revival Yes survives Kull success and Semuta ${claim?'claim':'pass'}`,()=>{
|
|
const f = fixture(); let game = pending(f);
|
|
const promise = structuredClone(game.revivalPromises!.find(row=>row.event === f.event)); assert.ok(promise);
|
|
for (let count=0;count<game.players.length && !game.pendingTreacheryDiscard;count++) {
|
|
assert.ok(game.response); const next = game.players.find(seat=>!game.response!.passed.includes(seat.id)); assert.ok(next);
|
|
game = act(game,next.id,{type:'passResponse'});
|
|
}
|
|
assert.deepEqual(game.revivalPromises!.find(row=>row.event === f.event),promise);
|
|
game = semuta(game,f.kull,claim);
|
|
game = finishRevival(game);
|
|
assert.equal(game.revivalPromises!.find(row=>row.event === f.event)?.fulfilled,true);
|
|
assert.equal(player(game,'choam').tanks,1);
|
|
assert.equal(player(game,'richese').hand.some(card=>card.id === f.kull),claim);
|
|
assert.deepEqual(inventory(game),f.originalCards);
|
|
});
|
|
|
|
for (const claim of [false,true]) void test(`Revival Yes survives Kull counter and Semuta ${claim?'claim':'pass'}`,()=>{
|
|
const f = fixture(); let game = pending(f);
|
|
assert.ok(viewGame(game,'tleilaxu').responseControls?.cancelCards.includes(f.counter));
|
|
game = act(game,'tleilaxu',{type:'card',mode:'cancel',card:f.counter});
|
|
assert.ok(!game.revivalPromises!.find(row=>row.event === f.event)?.released);
|
|
game = semuta(game,f.counter,claim);
|
|
if (game.pendingTreacheryDiscard) game = semuta(game,f.karama,false);
|
|
game = finishRevival(game);
|
|
assert.equal(game.revivalPromises!.find(row=>row.event === f.event)?.fulfilled,true);
|
|
assert.equal(player(game,'choam').tanks,1);
|
|
assert.equal(player(game,'richese').hand.some(card=>card.id === f.counter),claim);
|
|
assert.deepEqual(inventory(game),f.originalCards);
|
|
});
|
|
|
|
void test('Revival No retains its cap in the Kull and Richese profile',()=>{
|
|
const f = fixture('no');
|
|
const before = structuredClone(f.game);
|
|
assert.throws(()=>act(f.game,'choam',{type:'revive',amount:3,elite:0}));
|
|
assert.deepEqual(f.game,before);
|
|
assert.ok(!f.game.revivalPromises!.find(row=>row.event === f.event)?.released);
|
|
});
|