Files
Dune/tests/kull-paid-auction-funding.test.ts
T
Quintine e7520e8679
Verify and publish checkpoint container / container (push) Successful in 10m57s
Continue Kull across paid sale responses
2026-10-29 02:00:59 +11:00

91 lines
5.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { act, advance, clean, createKullRicheseSemutaFixture, player } from './fixture-kull-richese-semuta';
/** These use the public Basic Kull/Richese fixture and its actual purchase of
* Semuta. On turn two, select the unopened first ordinary lot so CHOAM cannot
* fund an alleged second payment with a newly drawn Karama. No live purse,
* hand, bidding state, response or pending transaction is staged. */
function auctionStart(winner:'choam'|'harkonnen') {
const f=createKullRicheseSemutaFixture();
let game=advance(f.game,state=>state.turn===2 && state.phase===3 && !state.response &&
!state.phaseOpening && state.decision?.kind==='richeseDeclaration');
const round=game.richeseBidding; assert.ok(round);
const nonKarama=game.deck.findIndex(card=>card.effect!=='karama'); assert.ok(nonKarama>=0);
game.deck.unshift(game.deck.splice(nonKarama,1)[0]);
game=act(game,'richese',{type:'decision',event:round.event,position:'last'});
game=advance(game,state=>state.phase===3 && !!state.auction &&
state.auction.active===winner && state.auction.bid===0 && clean(state));
assert.equal(game.auction?.cards[game.auction.index].effect==='karama',false);
assert.ok(player(game,winner).spice>0);
assert.ok(player(game,'choam').hand.some(card=>card.id===f.kull));
assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter));
return {...f,game};
}
function paidIncome(winner:'choam'|'harkonnen') {
const f=auctionStart(winner); let game=f.game;
const paid=player(game,winner).spice, lot=game.auction!.cards[game.auction!.index].id;
game=act(game,winner,{type:'bid',amount:paid,allyPayment:0});
game=advance(game,state=>state.response?.kind==='emperorIncome');
assert.equal(game.currentAuctionSale?.origin,'normal');
assert.equal(game.currentAuctionSale?.winner,winner);
assert.equal(game.currentAuctionSale?.amount,paid);
assert.equal(player(game,winner).spice,0);
assert.ok(player(game,winner).hand.some(card=>card.id===lot));
assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter));
return {...f,game,paid,lot};
}
function passDiscard(game:Game):Game {
const pending=game.pendingTreacheryDiscard; assert.ok(pending?.reaction?.stage==='offer');
const event=pending.batch.event;
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 seat=game.players.find(row=>!reaction.passed.includes(row.id)); assert.ok(seat);
game=act(game,seat.id,{type:'semutaPass',event});
}
assert.notEqual(game.pendingTreacheryDiscard?.batch.event,event);
return game;
}
void test('CHOAM may use Kull after spending its entire winning bid',()=>{
const f=paidIncome('choam'); let game=f.game;
const emperorBefore=player(game,'emperor').spice;
assert.ok(!player(game,'choam').hand.some(card=>card.effect==='karama'));
game=act(game,'harkonnen',{type:'card',mode:'cancel',card:f.counter});
const reaction=viewGame(game,'choam').kullReaction; assert.ok(reaction);
assert.ok(reaction.plays.some(play=>play.source==='printed' && play.card.id===f.kull));
game=act(game,'choam',{type:'kullDecision',event:reaction.event,source:'printed',card:f.kull});
game=advance(game,state=>!!state.pendingTreacheryDiscard);
game=passDiscard(game);
assert.equal(player(game,'choam').spice,0);
assert.equal(player(game,'emperor').spice,emperorBefore+f.paid);
assert.ok(player(game,'choam').hand.some(card=>card.id===f.lot));
assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.counter));
});
void test('a winner may attempt to cancel Emperor income after spending its entire bid',()=>{
const f=paidIncome('harkonnen'); let game=f.game;
const emperorBefore=player(game,'emperor').spice;
assert.ok(viewGame(game,'harkonnen').responseControls?.cancelCards.includes(f.counter));
game=act(game,'harkonnen',{type:'card',mode:'cancel',card:f.counter});
const reaction=viewGame(game,'choam').kullReaction; assert.ok(reaction);
assert.ok(reaction.plays.some(play=>play.source==='printed' && play.card.id===f.kull));
game=act(game,'choam',{type:'kullDecision',event:reaction.event,decline:true});
game=advance(game,state=>!!state.pendingTreacheryDiscard);
game=passDiscard(game);
assert.equal(player(game,'harkonnen').spice,0);
assert.equal(player(game,'emperor').spice,emperorBefore);
assert.ok(player(game,'harkonnen').hand.some(card=>card.id===f.lot));
assert.ok(!player(game,'harkonnen').hand.some(card=>card.id===f.counter));
});
void test('an unpaid non-CHOAM Karama-dependent overbid remains rejected',()=>{
const f=auctionStart('harkonnen'), before=structuredClone(f.game);
const amount=player(f.game,'harkonnen').spice+1;
assert.throws(()=>act(f.game,'harkonnen',{type:'bid',amount,allyPayment:0}),/unfunded Karama-dependent overbid/);
assert.deepEqual(f.game,before);
});