Files
Dune/tests/semuta-choam-karama.test.ts
T

97 lines
6.8 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {applyAction,viewGame,type Action,type Game} from '../game/engine';
import {botActions} from '../game/bots';
import {choamPowerAction,choamPowerPlays} from '../game/choam-power-options';
import {CHOAM_NEXUS_EFFECTS} from '../game/nexus-choam';
import {createSemutaNexusCunningProactive,createSemutaNexusCunningStart,gamePlayer,holdSemutaNexusCunningCard,reactSemutaNexusCunning,semutaNexusCunningInventory,type SemutaNexusCunningFixture} from './fixture-semuta-nexus-cunning';
import {createSemutaNexusCunningReactive,quoteSemutaNexusCunningReactive} from './fixture-semuta-nexus-cunning-reactive';
type Effect='kulon'|'gamont'|'baliset'|'laLaLa'|'jubba';
function fixture(effect:Effect,printed=false){
const options={advanced:true,fullHand:true,skills:effect==='gamont',tech:effect==='gamont',strongholds:effect==='gamont'};
let game:Game, action:Action, source:SemutaNexusCunningFixture, selection:Record<string,unknown>;
if(effect==='kulon'||effect==='gamont'){
const f=createSemutaNexusCunningProactive(effect,{...options,elite:effect==='gamont'});
game=f.game;action=f.action;source=f;selection=f.selection;
}else{
const f=createSemutaNexusCunningReactive(effect,{start:createSemutaNexusCunningStart(options),karama:true});
game=f.game;action=quoteSemutaNexusCunningReactive(game,f);source=f.start;selection=f.selection;
}
let fuel=source.cost;
if(printed){
fuel=holdSemutaNexusCunningCard(game,source.owner,c=>c.kind==='worthless'&&c.name===CHOAM_NEXUS_EFFECTS[effect],source.staging);
const view=viewGame(game,source.owner);
const play=choamPowerPlays(view,effect).find(p=>p.source==='printed'&&p.card.id===fuel.id);
assert.ok(play);
const selected=choamPowerAction(view,play,selection);assert.ok(selected);action=selected;
}
const karama=gamePlayer(game,source.other).hand.find(c=>c.effect==='karama');assert.ok(karama);
return {game,action,source,fuel,karama:karama.id,cancel:{type:'card',mode:'cancel',card:karama.id},printed};
}
function originalOutcome(game:Game){
return {
phase:game.phase,turn:game.turn,active:game.active,ready:game.ready,
decision:game.decision,response:game.response,pendingMove:game.pendingChoamMove,pendingRevival:game.pendingRevival,
storm:game.storm,stormResolution:game.stormResolution,blocked:game.choamWorthlessBlocked,
range:game.choamMovement,restrictions:game.choamBaliset,revivalRules:game.revivalRules,mentat:game.choamMentatPending,
players:game.players.map(p=>({id:p.id,spice:p.spice,forces:p.forces,reserves:p.reserves,tanks:p.tanks,elites:p.elites,moved:p.moved,revived:p.revived,free:p.freeForcesRevived})),
};
}
for(const effect of ['kulon','gamont','baliset','laLaLa','jubba'] as const)void test(`Semuta claims or passes printed Karama before original Cunning ${effect} cancellation and one native suffix`,()=>{
const f=fixture(effect), beforeWallets=f.game.players.map(p=>p.spice), cards=semutaNexusCunningInventory(f.game);
const declared=applyAction(f.game,f.source.owner,f.action);
const ordinary=structuredClone(declared);delete ordinary.semutaPreview;
const baseline=applyAction(ordinary,f.source.other,f.cancel);
const pending=applyAction(declared,f.source.other,f.cancel);
const entry=pending.pendingTreacheryDiscard!.batch.entries[0];
assert.equal(entry.card.id,f.karama);assert.equal(entry.card.effect,'karama');assert.equal(entry.publicFace,true);assert.equal(entry.discardedBy,f.source.other);
assert.equal(pending.nexusChoamHistory!.at(-1)!.stage,'pending');
assert.ok(gamePlayer(pending,f.source.owner).hand.some(c=>c.id===f.fuel.id));
assert.ok(!pending.discard.some(c=>c.id===f.fuel.id));
assert.equal(viewGame(pending,f.source.holder).semutaReaction!.canCommit,true);
assert.throws(()=>applyAction(pending,f.source.owner,{type:'decision',decline:true}));
for(const claim of [false,true]){
const done=reactSemutaNexusCunning(pending,f.source,claim);
assert.deepEqual(originalOutcome(done),originalOutcome(baseline));
assert.equal(done.nexusChoamHistory!.at(-1)!.stage,'canceled');
assert.equal(done.nexusCards!.cards!.hands[f.source.owner],null);
assert.equal(done.nexusCards!.cards!.discard.filter(c=>c==='choam').length,1);
assert.ok(gamePlayer(done,f.source.owner).hand.some(c=>c.id===f.fuel.id));
assert.equal(gamePlayer(done,f.source.holder).hand.length,4);
assert.equal(gamePlayer(done,f.source.holder).hand.some(c=>c.id===f.karama),claim);
assert.equal(gamePlayer(done,f.source.holder).hand.some(c=>c.id===f.source.semuta),!claim);
assert.equal(done.discard.filter(c=>c.id===f.karama).length,Number(!claim));
assert.deepEqual(semutaNexusCunningInventory(done),cards);
assert.deepEqual(done.players.map(p=>p.spice),baseline.players.map(p=>p.spice));
assert.throws(()=>applyAction(done,f.source.other,f.cancel));
assert.throws(()=>applyAction(done,f.source.holder,{type:'semutaCommit',event:pending.pendingTreacheryDiscard!.batch.event}));
}
assert.deepEqual(pending.players.map(p=>p.spice),beforeWallets,'Only the printed cost is committed before the native suffix');
});
for(const effect of ['kulon','jubba'] as const)void test(`Printed ${effect} retains its named fuel and held Nexus while cancellation Karama reacts`,()=>{
const f=fixture(effect,true), declared=applyAction(f.game,f.source.owner,f.action);
const ordinary=structuredClone(declared);delete ordinary.semutaPreview;
const baseline=applyAction(ordinary,f.source.other,f.cancel), pending=applyAction(declared,f.source.other,f.cancel);
const done=reactSemutaNexusCunning(pending,f.source,true);
assert.deepEqual(originalOutcome(done),originalOutcome(baseline));
assert.equal(done.nexusCards!.cards!.hands[f.source.owner],'choam');
assert.ok(gamePlayer(done,f.source.owner).hand.some(c=>c.id===f.fuel.id));
assert.ok(gamePlayer(done,f.source.holder).hand.some(c=>c.id===f.karama));
assert.ok(!done.discard.some(c=>c.id===f.fuel.id));
assert.deepEqual(semutaNexusCunningInventory(done),semutaNexusCunningInventory(f.game));
});
void test('All four legal Semuta profiles finish the printed native cancellation cost without claiming retained Cunning fuel',()=>{
const f=fixture('baliset'), pending=applyAction(applyAction(f.game,f.source.owner,f.action),f.source.other,f.cancel);
for(const difficulty of ['Easy','Medium','Hard','Brutal'] as const){
const own=viewGame(pending,f.source.holder);own.players.find(p=>p.id===f.source.holder)!.bot=difficulty;
const action=botActions(own)[0];assert.equal(action.type,'semutaCommit');
const done=applyAction(pending,f.source.holder,action);
assert.ok(gamePlayer(done,f.source.holder).hand.some(c=>c.id===f.karama));
assert.ok(gamePlayer(done,f.source.owner).hand.some(c=>c.id===f.fuel.id));
assert.equal(done.nexusChoamHistory!.at(-1)!.stage,'canceled');
assert.equal(gamePlayer(done,f.source.target).moved,1);
assert.deepEqual(semutaNexusCunningInventory(done),semutaNexusCunningInventory(f.game));
}
});