Files
Dune/tests/bureaucrat-qualifying-auction.test.ts
T

263 lines
18 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 { advanceBureaucratQualifyingSplit } from './fixture-bureaucrat-qualifying-split';
import {
createBureaucratQualifyingAuctionFixture, payBureaucratQualifyingAuction,
allowQualifyingAuctionResponses, chooseQualifyingAuction, qualifyingAuctionPlayer,
qualifyingAuctionPhysicalCards, type BureaucratQualifyingAuctionFixture,
} from './fixture-bureaucrat-qualifying-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 assertPaid(fixture: BureaucratQualifyingAuctionFixture, game: Game) {
const payer = qualifyingAuctionPlayer(game, fixture.payer), donor = qualifyingAuctionPlayer(game, fixture.donor);
assert.equal(payer.spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice - fixture.payerShare);
assert.equal(donor.spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.donor).spice - fixture.donorShare);
assert.equal(game.aid[fixture.donor].amount, 0);
assert.equal(payer.hand.length, qualifyingAuctionPlayer(fixture.beforeAction, fixture.payer).hand.length + 1);
assert.equal(payer.hand.filter(card => card.id === fixture.reservedCard).length, 1);
assert.equal(qualifyingAuctionPlayer(game, fixture.emperor).spice, qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice);
assert.deepEqual(qualifyingAuctionPhysicalCards(game), fixture.physical);
assert.equal(game.auction!.index, fixture.beforeAction.auction!.index);
assert.deepEqual(game.auction!.cards, fixture.sourceSetup.auction!.cards);
for (const original of fixture.beforeAction.players) {
const actual = qualifyingAuctionPlayer(game, original.id);
assert.deepEqual(actual.forces, original.forces);
assert.equal(actual.reserves, original.reserves);
}
}
function assertCompleted(fixture: BureaucratQualifyingAuctionFixture, pending: Game, done: Game, diverted: number) {
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice,
qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice + fixture.amount - diverted);
for (const actor of [fixture.payer, fixture.donor, fixture.trainer])
assert.equal(qualifyingAuctionPlayer(done, actor).spice, qualifyingAuctionPlayer(pending, actor).spice);
const payer = qualifyingAuctionPlayer(done, fixture.payer);
assert.deepEqual(payer.hand.map(card => card.id), [
...qualifyingAuctionPlayer(pending, fixture.payer).hand.map(card => card.id), fixture.bonusCard,
]);
assert.deepEqual(done.deck.map(card => card.id), pending.deck.slice(1).map(card => card.id));
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
assert.equal(done.currentAuctionSale, null);
assert.equal(done.auction!.index, fixture.beforeAction.auction!.index + 1);
assert.equal(done.auction!.cards[done.auction!.index].id, fixture.beforeAction.auction!.cards[fixture.beforeAction.auction!.index + 1].id);
assert.equal(done.auction!.bid, 0);
assert.equal(done.auction!.bidder, null);
assert.equal(done.bureaucratPayments?.used.length ?? 0, diverted ? 1 : 0);
assert.equal(viewGame(done, fixture.trainer).bureaucrat.usedThisPhase, diverted === 2);
}
for (const advanced of [false, true]) {
for (const [payerShare, donorShare] of [[5, 3], [3, 5], [5, 5], [0, 5]]) {
for (const redirect of [true, false]) {
void test(`${advanced ? 'Advanced' : 'Basic'} native ordinary auction ${payerShare}+${donorShare}: ${redirect ? 'Bank two' : 'full Emperor income'} and one Harkonnen bonus`, () => {
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare, donorShare });
const paid = payBureaucratQualifyingAuction(fixture);
assertPaid(fixture, paid);
const pending = allowQualifyingAuctionResponses(paid);
assert.equal(pending.decision?.kind, 'bureaucratPayment');
assertPaid(fixture, pending);
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(JSON.parse(JSON.stringify(pending)) as Game, redirect));
assertCompleted(fixture, pending, done, redirect ? 2 : 0);
rejectUnchanged(done, fixture.trainer, { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
});
}
}
for (const [payerShare, donorShare] of [[3, 3], [4, 3]]) {
void test(`${advanced ? 'Advanced' : 'Basic'} native ordinary auction ${payerShare}+${donorShare} cannot pool sub-five shares`, () => {
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare, donorShare });
const paid = payBureaucratQualifyingAuction(fixture);
assertPaid(fixture, paid);
const done = allowQualifyingAuctionResponses(paid);
assertCompleted(fixture, paid, done, 0);
assert.equal(viewGame(done, fixture.trainer).bureaucrat.pending, null);
});
}
void test(`${advanced ? 'Advanced' : 'Basic'} original Emperor Karama cancellation banks the whole allied invoice, not just the primary contribution`, () => {
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare: 5, donorShare: 5, counter: true });
const paid = payBureaucratQualifyingAuction(fixture);
assertPaid(fixture, paid);
assert.equal(paid.response?.kind, 'emperorIncome');
assert.equal(viewGame(paid, fixture.trainer).bureaucrat.pending, null);
const canceled = applyAction(paid, fixture.trainer, { type: 'card', mode: 'cancel', card: fixture.counterCard });
const done = allowQualifyingAuctionResponses(canceled);
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice, qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice);
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).spice, qualifyingAuctionPlayer(paid, fixture.payer).spice);
assert.equal(qualifyingAuctionPlayer(done, fixture.donor).spice, qualifyingAuctionPlayer(paid, fixture.donor).spice);
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.length, qualifyingAuctionPlayer(paid, fixture.payer).hand.length + 1);
assert.equal(done.discard.filter(card => card.id === fixture.counterCard).length, 1);
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
assert.equal(done.auction!.index, 1);
assert.equal(done.bureaucratPayments?.used.length ?? 0, 0);
assert.equal(viewGame(done, fixture.trainer).bureaucrat.pending, null);
});
}
void test('equal public eight-spice invoices conceal which allied contribution qualified from Bureaucrat and outside audiences', () => {
const primary = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 3 });
const ally = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
const a = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(primary));
const b = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(ally));
for (const viewer of [primary.trainer, primary.guild, primary.emperor]) {
const left = viewGame(a, viewer), right = viewGame(b, viewer);
assert.equal(left.bureaucrat.pending!.payer, null);
assert.equal(left.bureaucrat.pending!.amount, 8);
assert.equal(left.bureaucrat.pending!.payee, primary.emperor);
assert.equal(left.auction!.bid, 8);
assert.equal(left.auction!.bidder, primary.payer);
assert.equal(left.auction!.allyPayment, undefined);
const { event: leftEvent, ...leftPayment } = left.bureaucrat.pending!;
const { event: rightEvent, ...rightPayment } = right.bureaucrat.pending!;
assert.notEqual(leftEvent, rightEvent);
assert.deepEqual(leftPayment, rightPayment);
assert.equal('contributions' in leftPayment, false);
assert.equal('contributions' in rightPayment, false);
assert.deepEqual(left.log.slice(primary.beforePledge.log.length).map(entry => entry.text),
right.log.slice(ally.beforePledge.log.length).map(entry => entry.text));
}
});
void test('a real original pledge is private funding, not a gift or a second income/payment action', () => {
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.donor).spice, 1);
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.payer).spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice);
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.payer).bribes, 0);
assert.equal(fixture.pledged.aid[fixture.donor].amount, 5);
assert.equal(fixture.pledged.decision, null);
assert.equal(fixture.pledged.response, null);
assert.equal(fixture.pledged.bureaucratPayments?.used.length ?? 0, 0);
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
assert.equal(pending.decision?.kind, 'bureaucratPayment');
assertPaid(fixture, pending);
});
void test('stale and cross-actor choices cannot spend, refund, duplicate, or modify the reserved ordinary lot', () => {
const fixture = createBureaucratQualifyingAuctionFixture();
const bid = applyAction(fixture.game, fixture.payer, fixture.action);
rejectUnchanged(bid, fixture.donor, { type: 'pledgeAid', amount: fixture.donorShare - 1 });
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
rejectUnchanged(pending, fixture.donor, { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
rejectUnchanged(pending, fixture.trainer, { type: 'decision', event: 'old-auction-invoice', redirect: true });
rejectUnchanged(pending, fixture.payer, { type: 'bid', amount: 1 });
rejectUnchanged(pending, fixture.donor, { type: 'pledgeAid', amount: 0 });
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
assertCompleted(fixture, pending, done, 2);
});
void test('an original purchase using the last hand space has no premature or overflow Harkonnen bonus', () => {
const fixture = createBureaucratQualifyingAuctionFixture({ lastHandSpace: true });
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
assertPaid(fixture, pending);
assert.equal(qualifyingAuctionPlayer(pending, fixture.payer).hand.length, handLimit(qualifyingAuctionPlayer(pending, fixture.payer)));
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
assert.deepEqual(qualifyingAuctionPlayer(done, fixture.payer).hand, qualifyingAuctionPlayer(pending, fixture.payer).hand);
assert.deepEqual(done.deck, pending.deck);
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice, qualifyingAuctionPlayer(pending, fixture.emperor).spice + 6);
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
assert.equal(done.auction!.index, 1);
assert.equal(done.bureaucratPayments!.used.length, 1);
});
for (const difficulty of DIFFICULTIES) {
void test(`${difficulty} Bureaucrat bot resolves the actual concealed allied invoice and hands control to the native next lot`, () => {
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 5 });
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
const view = viewGame(pending, fixture.trainer);
view.players.find(player => player.id === fixture.trainer)!.bot = difficulty;
const action = botActions(view)[0];
const done = allowQualifyingAuctionResponses(applyAction(pending, fixture.trainer, action));
assertCompleted(fixture, pending, done, action.redirect ? 2 : 0);
const next = advanceBureaucratQualifyingSplit(done, game => !game.decision && !game.response && !!game.auction?.peekKnown);
assert.equal(next.auction!.index, 1);
assert.equal(next.active, next.auction!.active);
assert.ok(next.order.includes(next.active!));
assert.equal(viewGame(next, fixture.trainer).bureaucrat.pending, null);
});
}
void test('one split use covers both qualifying shares, blocks a later same-phase native payment, and refreshes only at a real phase boundary', () => {
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 5 });
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
let game = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
const use = structuredClone(game.bureaucratPayments!.used);
const secondLot = game.auction!.cards[1].id;
game = advanceBureaucratQualifyingSplit(game, state => !!state.auction && state.auction.active === fixture.guild && !state.decision && !state.response);
const guildBefore = qualifyingAuctionPlayer(game, fixture.guild).spice;
const emperorBefore = qualifyingAuctionPlayer(game, fixture.emperor).spice;
game = applyAction(game, fixture.guild, { type: 'bid', amount: 5 });
for (let n = 0; game.auction?.index === 1 && !game.response && !game.decision && n < game.players.length + 2; n++)
game = applyAction(game, game.auction!.active, { type: 'passBid' });
if (game.decision?.kind === 'auctionPayment') game = applyAction(game, fixture.guild, { type: 'decision', karama: false });
game = allowQualifyingAuctionResponses(game);
assert.equal(viewGame(game, fixture.trainer).bureaucrat.pending, null);
assert.deepEqual(game.bureaucratPayments!.used, use);
assert.equal(qualifyingAuctionPlayer(game, fixture.guild).spice, guildBefore - 5);
assert.equal(qualifyingAuctionPlayer(game, fixture.guild).hand.filter(card => card.id === secondLot).length, 1);
assert.equal(qualifyingAuctionPlayer(game, fixture.emperor).spice, emperorBefore + 5);
assert.equal(game.auction!.index, 2);
game = advanceBureaucratQualifyingSplit(game, state => state.phase === 5 && !state.phaseOpening && !state.response && !state.decision);
assert.equal(game.turn, pending.turn);
assert.equal(viewGame(game, fixture.trainer).bureaucrat.usedThisPhase, false);
const fresh = applyAction(game, fixture.emperor, { type: 'bribe', target: fixture.guild, amount: 5 });
assert.equal(fresh.decision?.kind, 'bureaucratPayment');
assert.notEqual(fresh.bureaucratPaymentEvent, pending.bureaucratPaymentEvent);
const done = chooseQualifyingAuction(fresh);
assert.equal(qualifyingAuctionPlayer(done, fixture.guild).bribes, 3);
assert.equal(done.bureaucratPayments!.used.length, 2);
assert.equal(viewGame(done, fixture.trainer).bureaucrat.usedThisPhase, true);
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
});
for (const donor of ['b', 'e'] as const) {
for (const [payerShare, donorShare] of [[3, 5], [5, 3]]) {
void test(`native worm alliance with ${donor === 'b' ? 'Bureaucrat owner' : 'Emperor payee'} excludes only that contributor's threshold`, () => {
const fixture = createBureaucratQualifyingAuctionFixture({ donor, payerShare, donorShare });
assert.equal(qualifyingAuctionPlayer(fixture.alliance, fixture.payer).ally, donor);
assert.equal(qualifyingAuctionPlayer(fixture.alliance, donor).ally, fixture.payer);
const paid = payBureaucratQualifyingAuction(fixture);
assertPaid(fixture, paid);
const allowed = allowQualifyingAuctionResponses(paid);
const qualifies = payerShare === 5;
assert.equal(allowed.decision?.kind, qualifies ? 'bureaucratPayment' : undefined);
const done = qualifies ? allowQualifyingAuctionResponses(chooseQualifyingAuction(allowed)) : allowed;
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).spice,
qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice - payerShare);
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice,
qualifyingAuctionPlayer(fixture.beforePledge, fixture.emperor).spice - (donor === 'e' ? donorShare : 0) +
fixture.amount - (qualifies ? 2 : 0));
if (donor === 'b') assert.equal(qualifyingAuctionPlayer(done, donor).spice,
qualifyingAuctionPlayer(fixture.beforePledge, donor).spice - donorShare);
assert.equal(done.bureaucratPayments?.used.length ?? 0, qualifies ? 1 : 0);
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.filter(card => card.id === fixture.reservedCard).length, 1);
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.filter(card => card.id === fixture.bonusCard).length, 1);
assert.equal(done.auction!.index, 1);
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
});
}
}
void test('declining a native split invoice leaves the original card available for a distinct payment in the same Bidding phase', () => {
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending, false));
assertCompleted(fixture, pending, done, 0);
const next = advanceBureaucratQualifyingSplit(done, state => !!state.auction?.peekKnown &&
!state.phaseOpening && !state.response && !state.decision);
assert.equal(next.auction!.index, 1);
const paid = applyAction(next, fixture.emperor, { type: 'bribe', target: fixture.guild, amount: 5 });
assert.equal(paid.phase, pending.phase);
assert.equal(paid.decision?.kind, 'bureaucratPayment');
assert.notEqual(paid.bureaucratPaymentEvent, pending.bureaucratPaymentEvent);
const redirected = chooseQualifyingAuction(paid);
assert.equal(qualifyingAuctionPlayer(redirected, fixture.guild).bribes, 3);
assert.equal(redirected.bureaucratPayments!.used.length, 1);
assert.equal(redirected.auction!.index, 1);
assert.equal(qualifyingAuctionPlayer(redirected, fixture.payer).spice, qualifyingAuctionPlayer(done, fixture.payer).spice);
assert.deepEqual(qualifyingAuctionPhysicalCards(redirected), fixture.physical);
});