Files
Dune/tests/bureaucrat-paired-auction.test.ts

251 lines
16 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { advanceBureaucratPairedFunding } from './fixture-bureaucrat-paired-funding';
import {
createBureaucratPairedAuctionFixture, payBureaucratPairedAuction,
allowPairedAuctionResponses, choosePairedAuctionBureaucrat,
pairedAuctionPlayer, pairedAuctionPhysicalCards,
type BureaucratPairedAuctionFixture, type PairedAuctionSource,
} from './fixture-bureaucrat-paired-auction';
function rejectUnchanged(game:Game,actor:string,action:Action) {
const before=JSON.stringify(game);
assert.throws(()=>applyAction(game,actor,action));
assert.equal(JSON.stringify(game),before);
}
function assertCosts(f:BureaucratPairedAuctionFixture,game:Game) {
assert.equal(pairedAuctionPlayer(game,f.payer).spice,pairedAuctionPlayer(f.beforePledge,f.payer).spice-f.payerShare);
if(f.donor!==f.payee) assert.equal(pairedAuctionPlayer(game,f.donor).spice,
pairedAuctionPlayer(f.beforePledge,f.donor).spice-f.donorShare);
assert.equal(game.aid[f.donor]?.amount ?? 0,0);
assert.equal(pairedAuctionPlayer(game,f.payer).hand.filter(c=>c.id===f.card).length,1);
assert.deepEqual(pairedAuctionPhysicalCards(game),f.physical);
}
function assertOutcome(f:BureaucratPairedAuctionFixture,done:Game,diverted:number) {
assertCosts(f,done);
assert.equal(pairedAuctionPlayer(done,f.payee).spice,
pairedAuctionPlayer(f.beforePledge,f.payee).spice-(f.donor===f.payee?f.donorShare:0)+f.amount-diverted);
const buyer=pairedAuctionPlayer(done,f.payer),before=pairedAuctionPlayer(f.beforeAction,f.payer);
assert.deepEqual(buyer.hand.map(c=>c.id),[...before.hand.map(c=>c.id),f.card,f.beforeAction.deck[0].id]);
assert.equal(done.currentAuctionSale,null);
assert.equal(done.bureaucratPayments?.used.length ?? 0,diverted?1:0);
assert.equal(viewGame(done,f.trainer).bureaucrat.usedThisPhase,diverted===2);
if(f.source==='normal') {
assert.equal(done.auction!.index,f.beforeAction.auction!.index+1);
assert.equal(done.auction!.cards[done.auction!.index].id,f.beforeAction.auction!.cards[f.beforeAction.auction!.index+1].id);
assert.equal(done.auction!.bid,0);
assert.equal(done.auction!.bidder,null);
} else if(f.source==='cache'&&f.position==='first') {
assert.equal(done.richeseAuction,null);
assert.equal(done.auction!.index,0);
assert.equal(done.auction!.cards.length,done.richeseBidding!.normalCount);
assert.equal(done.richeseCache!.some(c=>c.id===f.card),false);
} else if(f.source==='blackMarket') {
assert.equal(done.decision?.kind,'richeseDeclaration');
assert.equal(done.richeseBidding!.blackMarketSold,true);
assert.equal(pairedAuctionPlayer(done,f.richese).hand.some(c=>c.id===f.card),false);
}
}
for(const advanced of [false,true]) {
const sources:PairedAuctionSource[]=advanced?['normal','cache','blackMarket']:['normal','cache'];
for(const source of sources) {
for(const [payerShare,donorShare] of [[5,3],[3,5],[5,5],[0,5]]) {
for(const redirect of [false,true]) {
void test(`${advanced?'Advanced':'Basic'} paired ${source} ${payerShare}+${donorShare}: ${redirect?'Bank two':'full native income'}, one purchase and original H bonus`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,source,payerShare,donorShare});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
assert.equal(pending.decision?.kind,'bureaucratPayment');
assertCosts(f,pending);
assert.equal(pairedAuctionPlayer(pending,f.payee).spice,pairedAuctionPlayer(f.beforeAction,f.payee).spice);
assert.equal(pairedAuctionPlayer(pending,f.payer).hand.length,pairedAuctionPlayer(f.beforeAction,f.payer).hand.length+1);
const done=allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending,redirect));
assertOutcome(f,done,redirect?2:0);
rejectUnchanged(done,f.trainer,{type:'decision',event:pending.bureaucratPaymentEvent,redirect:true});
});
}
}
void test(`${advanced?'Advanced':'Basic'} paired ${source} three plus three never pools a qualifying contribution`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,source,payerShare:3,donorShare:3});
const done=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
assert.equal(viewGame(done,f.trainer).bureaucrat.pending,null);
assertOutcome(f,done,0);
});
void test(`${advanced?'Advanced':'Basic'} paired ${source}: gross eight is public, allied shares and qualifying identity are not`,()=>{
const primary=createBureaucratPairedAuctionFixture({advanced,source,payerShare:5,donorShare:3});
const donor=createBureaucratPairedAuctionFixture({advanced,source,payerShare:3,donorShare:5});
const a=allowPairedAuctionResponses(payBureaucratPairedAuction(primary));
const b=allowPairedAuctionResponses(payBureaucratPairedAuction(donor));
for(const viewer of [primary.trainer,primary.guild,primary.payee]) {
const left=viewGame(a,viewer),right=viewGame(b,viewer);
const {event:_left,...invoice}=left.bureaucrat.pending!;
const {event:_right,...other}=right.bureaucrat.pending!;
assert.equal(invoice.payer,null);
assert.equal(invoice.payee,primary.payee);
assert.equal(invoice.amount,8);
assert.deepEqual(invoice,other);
assert.equal('contributions' in invoice,false);
assert.equal('signature' in invoice,false);
if(source==='normal') {
assert.equal(left.auction!.bid,8);
assert.equal(left.auction!.allyPayment,undefined);
} else {
assert.equal(left.richeseAuction!.revealedBids![primary.payer],8);
assert.deepEqual(left.richeseAuction!.revealedBids,right.richeseAuction!.revealedBids);
assert.equal('richeseFunding' in left,false);
assert.equal('contributions' in left.richeseAuction!,false);
}
}
assertOutcome(primary,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(a)),2);
assertOutcome(donor,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(b)),2);
});
void test(`${advanced?'Advanced':'Basic'} paired ${source}: cross-actor and stale choices cannot debit, refund or duplicate a native purchase`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,source,payerShare:5,donorShare:5});
const bid=applyAction(f.game,f.payer,f.action);
rejectUnchanged(bid,f.donor,{type:'pledgeAid',amount:3});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
rejectUnchanged(pending,f.donor,{type:'decision',event:pending.bureaucratPaymentEvent,redirect:true});
rejectUnchanged(pending,f.trainer,{type:'decision',event:'old-richese-invoice',redirect:true});
rejectUnchanged(pending,f.payer,f.action);
assertOutcome(f,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending)),2);
});
}
void test(`${advanced?'Advanced':'Basic'} original ordinary Emperor Karama cancellation banks the full funded invoice without a Bureaucrat use`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,payerShare:5,donorShare:5,counter:true});
const paid=payBureaucratPairedAuction(f);
assert.equal(paid.response?.kind,'emperorIncome');
assertCosts(f,paid);
assert.equal(viewGame(paid,f.trainer).bureaucrat.pending,null);
const done=allowPairedAuctionResponses(applyAction(paid,f.trainer,{type:'card',card:f.counterCard,mode:'cancel'}));
assert.equal(pairedAuctionPlayer(done,f.emperor).spice,pairedAuctionPlayer(f.beforeAction,f.emperor).spice);
assert.equal(pairedAuctionPlayer(done,f.payer).hand.length,pairedAuctionPlayer(f.beforeAction,f.payer).hand.length+2);
assert.equal(done.discard.filter(c=>c.id===f.counterCard).length,1);
assert.equal(done.bureaucratPayments?.used.length ?? 0,0);
assert.equal(done.auction!.index,1);
assertCosts(f,done);
});
void test(`${advanced?'Advanced':'Basic'} original Karama purchases remain normal-only, not a free cache invoice`,()=>{
const normal=createBureaucratPairedAuctionFixture({advanced,counter:true});
const done=allowPairedAuctionResponses(applyAction(normal.game,normal.trainer,{type:'card',card:normal.counterCard,mode:'purchase'}));
assert.equal(pairedAuctionPlayer(done,normal.trainer).spice,pairedAuctionPlayer(normal.game,normal.trainer).spice);
assert.equal(pairedAuctionPlayer(done,normal.emperor).spice,pairedAuctionPlayer(normal.game,normal.emperor).spice);
assert.equal(pairedAuctionPlayer(done,normal.trainer).hand.filter(c=>c.id===normal.card).length,1);
assert.equal(done.discard.filter(c=>c.id===normal.counterCard).length,1);
assert.equal(done.auction!.index,1);
assert.equal(done.bureaucratPayments?.used.length ?? 0,0);
const cache=createBureaucratPairedAuctionFixture({advanced,source:'cache',counter:true});
rejectUnchanged(cache.game,cache.trainer,{type:'card',card:cache.counterCard,mode:'purchase'});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(cache));
assertOutcome(cache,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending)),2);
});
void test(`${advanced?'Advanced':'Basic'} actual last-cache closing advances without another funded debit or purchase`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,source:'cache',method:'onceAround',position:'last',payerShare:5,donorShare:5});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
const paid=allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending));
assertOutcome(f,paid,2);
const closed=advanceBureaucratPairedFunding(paid,state=>state.phase>3&&!state.phaseOpening&&!state.response&&!state.decision);
assert.equal(closed.turn,paid.turn);
assert.equal(pairedAuctionPlayer(closed,'p').spice,pairedAuctionPlayer(paid,'p').spice);
assert.equal(pairedAuctionPlayer(closed,'f').spice,pairedAuctionPlayer(paid,'f').spice);
assert.equal(pairedAuctionPlayer(closed,'r').spice,pairedAuctionPlayer(paid,'r').spice);
assert.deepEqual(pairedAuctionPlayer(closed,'p').hand,pairedAuctionPlayer(paid,'p').hand);
assert.equal(closed.bureaucratPayments?.used.length,1);
assert.equal(viewGame(closed,'b').bureaucrat.usedThisPhase,false);
assert.equal(closed.currentAuctionSale,null);
assert.equal(closed.auction,null);
assert.equal(closed.richeseAuction,null);
});
for(const donor of ['b','e'] as const) {
for(const [payerShare,donorShare] of [[3,5],[5,3]]) {
void test(`${advanced?'Advanced':'Basic'} original worm alliance: ${donor==='b'?'trainer':'Emperor payee'} contribution cannot qualify or mask H five`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced,donor,payerShare,donorShare});
const allowed=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
const qualifies=payerShare===5;
assert.equal(viewGame(allowed,f.trainer).bureaucrat.pending!==null,qualifies);
const done=qualifies?allowPairedAuctionResponses(choosePairedAuctionBureaucrat(allowed)):allowed;
assertOutcome(f,done,qualifies?2:0);
});
}
}
}
void test('Basic original Richese opening cannot declare a Black Market lot',()=>{
const f=createBureaucratPairedAuctionFixture({source:'cache'});
assert.equal(f.opening.decision?.kind,'richeseDeclaration');
rejectUnchanged(f.opening,f.richese,{type:'decision',event:f.opening.richeseBidding!.event,
card:pairedAuctionPlayer(f.opening,f.richese).hand[0].id,method:'silent'});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
assertOutcome(f,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending)),2);
});
void test('sealed native Richese bid conceals the amount until original bidders finish',()=>{
const f=createBureaucratPairedAuctionFixture({advanced:true,source:'cache',payerShare:3,donorShare:5});
const submitted=applyAction(f.game,f.payer,f.action);
for(const viewer of [f.trainer,f.richese,f.guild]) {
const lot=viewGame(submitted,viewer).richeseAuction!;
assert.equal(lot.revealedBids,null);
assert.equal(lot.ownBid,null);
assert.equal(lot.ownAllyPayment,0);
}
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
assert.equal(viewGame(pending,f.trainer).richeseAuction!.revealedBids![f.payer],8);
assertOutcome(f,allowPairedAuctionResponses(choosePairedAuctionBureaucrat(pending)),2);
});
for(const difficulty of DIFFICULTIES) {
void test(`${difficulty} original paired Bureaucrat bot resolves Richese seller invoice and releases native compulsory declaration`,()=>{
const f=createBureaucratPairedAuctionFixture({advanced:true,source:'blackMarket',payerShare:5,donorShare:5});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
const view=viewGame(pending,f.trainer);
view.players.find(p=>p.id===f.trainer)!.bot=difficulty;
const action=botActions(view)[0];
const done=allowPairedAuctionResponses(applyAction(pending,f.trainer,action));
assertOutcome(f,done,action.redirect?2:0);
});
}
void test('one physical use covers both five contributions; original bonus cancellation leads to a genuine last-space purchase and cache closing',()=>{
const f=createBureaucratPairedAuctionFixture({advanced:true,payerShare:5,donorShare:5,counter:true});
const pending=allowPairedAuctionResponses(payBureaucratPairedAuction(f));
let game=choosePairedAuctionBureaucrat(pending);
assert.equal(game.response?.kind,'harkonnenBonus');
game=allowPairedAuctionResponses(applyAction(game,f.trainer,{type:'card',card:f.counterCard,mode:'cancel'}));
assertCosts(f,game);
assert.equal(pairedAuctionPlayer(game,f.emperor).spice,pairedAuctionPlayer(f.beforeAction,f.emperor).spice+8);
assert.equal(pairedAuctionPlayer(game,f.payer).hand.length,3);
assert.equal(game.discard.filter(c=>c.id===f.counterCard).length,1);
assert.equal(game.bureaucratPayments!.used.length,1);
const used=structuredClone(game.bureaucratPayments!.used);
while(pairedAuctionPlayer(game,f.payer).hand.length<handLimit(pairedAuctionPlayer(game,f.payer))) {
game=advanceBureaucratPairedFunding(game,s=>!!s.auction&&s.auction.active===f.payer&&!s.response&&!s.decision);
const before=structuredClone(game),buyer=pairedAuctionPlayer(before,f.payer),index=before.auction!.index;
const card=before.auction!.cards[index].id;
game=applyAction(game,f.payer,{type:'bid',amount:1,allyPayment:0});
for(let n=0;!game.currentAuctionSale&&game.auction?.index===index&&n<game.players.length+2;n++) {
if(game.decision?.kind==='auctionPayment') {game=applyAction(game,f.payer,{type:'decision',karama:false});break;}
game=applyAction(game,game.auction!.active,{type:'passBid'});
}
game=allowPairedAuctionResponses(game);
const bought=pairedAuctionPlayer(game,f.payer);
assert.equal(bought.spice,buyer.spice-1);
assert.equal(bought.hand.filter(c=>c.id===card).length,1);
assert.equal(bought.hand.length,Math.min(handLimit(buyer),buyer.hand.length+2));
if(buyer.hand.length===handLimit(buyer)-1) {
assert.deepEqual(game.deck,before.deck,'Last-space purchase has no overflow bonus draw');
assert.equal(bought.hand.length,buyer.hand.length+1);
}
assert.equal(pairedAuctionPlayer(game,f.emperor).spice,pairedAuctionPlayer(before,f.emperor).spice+1);
assert.deepEqual(game.bureaucratPayments!.used,used);
assert.equal(viewGame(game,f.trainer).bureaucrat.pending,null);
}
assert.equal(pairedAuctionPlayer(game,f.payer).hand.length,8);
game=advanceBureaucratPairedFunding(game,s=>s.turn===2&&s.phase===4);
assert.equal(game.auction,null);
assert.equal(game.currentAuctionSale,null);
assert.equal(game.richeseAuction,null);
assert.deepEqual(pairedAuctionPhysicalCards(game),f.physical);
});