Files
Dune/tests/advanced-emperor-auction-contribution.test.ts
T
Quintine 6cec4ed78b
Verify and publish checkpoint container / container (push) Canceled after 0s
Offer Emperor gifts before allied auction payment
2026-10-29 06:21:49 +11:00

135 lines
7.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, type Action, type Game } from '../game/engine';
import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
import { advanceTruthAlliance, createTruthAllianceFixture, truthAllianceClean } from './fixture-truth-alliance';
const act = (game: Game, id: string, action: Action) => applyAction(game, id, action);
const seat = (game: Game, id: string) => { const result = game.players.find(player => player.id === id); assert.ok(result); return result; };
/** Original public H/A/E setup and real second-turn Nexus; only the optional
* empty group ledger is staged. Test wallets and the cancellation card swap
* below are explicitly controlled, not claimed native earnings. */
function auction(advanced: boolean, group: boolean): Game {
const source = createTruthAllianceFixture({ advanced, consent: 'none' });
let game = structuredClone(source.game);
if (group) game.coalitionState = emptyCoalitionState('three-short');
game = act(game, 'harkonnen', { type: 'alliance', target: 'emperor' });
game = act(game, 'emperor', { type: 'alliance', target: 'harkonnen' });
if (group) {
const pair = coalitionOf(game.players, game.coalitionState, 'harkonnen'); assert.ok(pair);
game = act(game, 'harkonnen', { type: 'coalitionPropose', groupId: pair.id,
entrant: 'atreides', expectedRevision: game.coalitionState!.revision });
for (const id of ['emperor', 'atreides']) {
const offer = game.coalitionState!.proposal!;
game = act(game, id, { type: 'coalitionAnswer', event: offer.event, nexus: offer.nexus,
turn: offer.turn, accept: true, expectedRevision: game.coalitionState!.revision });
}
}
game = advanceTruthAlliance(game, state => state.phase === 3 && !!state.auction && truthAllianceClean(state));
for (let count = 0; game.auction && game.auction.active !== 'harkonnen' && count < game.players.length; count++)
game = act(game, game.auction.active, { type: 'passBid' });
assert.equal(game.auction?.active, 'harkonnen');
// Set only the wallets needed to observe the original physical bid and payer
// legs; no auction, card, seat, alliance, or pending receipt is installed.
seat(game, 'harkonnen').spice = 2;
seat(game, 'emperor').spice = 10;
seat(game, 'atreides').spice = 10;
return game;
}
function passResponses(game: Game): Game {
for (let count = 0; game.response && count < 20; count++) {
const actor = game.players.find(player => !game.response!.passed.includes(player.id)); assert.ok(actor);
game = act(game, actor.id, { type: 'passResponse' });
}
assert.equal(game.response, null);
return game;
}
/** Reach native paid sale, leaving any resulting Emperor-income response for
* the caller. Do not invent the special purchase or repeat an invoice. */
function bid(game: Game, action: Action): Game {
game = act(game, 'harkonnen', action);
for (let count = 0; !game.response && !game.decision && game.auction?.bidder === 'harkonnen' && count < 12; count++)
game = act(game, game.auction.active, { type: 'passBid' });
if (game.pendingEmperorAuctionGift)
game = act(game, game.pendingEmperorAuctionGift.owner,
{ type: 'emperorAuctionGiftContinue', event: game.pendingEmperorAuctionGift.event });
if (!game.response && game.decision?.kind === 'auctionPayment')
game = act(game, 'harkonnen', { type: 'decision', karama: false });
return game;
}
for (const amount of [2, 6]) {
void test(`Advanced ordinary pair routes the Emperor's ${amount}-spice contribution to the bank`, () => {
let game = auction(true, false);
const hand = seat(game, 'harkonnen').hand.length;
seat(game, 'harkonnen').spice = amount === 2 ? 4 : 0;
game = act(game, 'emperor', { type: 'pledgeAid', amount });
game = bid(game, { type: 'bid', amount: 6, allyPayment: amount });
game = passResponses(game);
assert.ok(seat(game, 'harkonnen').hand.length > hand);
assert.equal(seat(game, 'harkonnen').spice, 0);
assert.equal(seat(game, 'emperor').spice, 10 - amount + (6 - amount));
});
}
for (const amount of [2, 6]) {
void test(`Advanced three-member bid routes the Emperor's ${amount}-spice share to the bank`, () => {
let game = auction(true, true);
seat(game, 'harkonnen').spice = amount === 6 ? 0 : 1;
game = act(game, 'emperor', { type: 'coalitionPledge', recipient: 'harkonnen', amount });
if (amount === 2) game = act(game, 'atreides', { type: 'coalitionPledge', recipient: 'harkonnen', amount: 3 });
game = bid(game, { type: 'bid', amount: 6, allyPayment: 0, coalitionContributions: amount === 6
? [{ donor: 'emperor', amount: 6 }]
: [{ donor: 'emperor', amount: 2 }, { donor: 'atreides', amount: 3 }] });
game = passResponses(game);
assert.equal(seat(game, 'harkonnen').spice, 0);
assert.equal(seat(game, 'emperor').spice, 10 - amount + (6 - amount));
assert.equal(seat(game, 'atreides').spice, amount === 2 ? 7 : 10);
});
}
void test('Advanced direct Emperor gift belongs to the buyer and is not treated as an Emperor contribution', () => {
let game = auction(true, false);
game = act(game, 'emperor', { type: 'emperorGift', amount: 2 });
game = passResponses(game);
assert.equal(seat(game, 'emperor').spice, 8);
assert.equal(seat(game, 'harkonnen').spice, 4);
game = bid(game, { type: 'bid', amount: 4, allyPayment: 0 });
game = passResponses(game);
assert.equal(seat(game, 'harkonnen').spice, 0);
assert.equal(seat(game, 'emperor').spice, 12);
});
void test('Basic keeps the earlier full-Emperor-income handling for the same ordinary pair bid', () => {
let game = auction(false, false);
seat(game, 'harkonnen').spice = 4;
game = act(game, 'emperor', { type: 'pledgeAid', amount: 2 });
game = bid(game, { type: 'bid', amount: 6, allyPayment: 2 });
game = passResponses(game);
assert.equal(seat(game, 'harkonnen').spice, 0);
assert.equal(seat(game, 'emperor').spice, 14);
});
void test('canceling the remaining Advanced Emperor income keeps both payer debits and the purchased card', () => {
let game = auction(true, false);
seat(game, 'harkonnen').spice = 4;
// Controlled conservation supplement: swap one original dealt Harkonnen
// card for an original unauctioned Karama, with no card duplicated.
const index = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(index >= 0);
const [karama] = game.deck.splice(index, 1), [swapped] = seat(game, 'harkonnen').hand.splice(0, 1);
seat(game, 'harkonnen').hand.push(karama); game.deck.push(swapped);
const hand = seat(game, 'harkonnen').hand.length;
game = act(game, 'emperor', { type: 'pledgeAid', amount: 2 });
game = bid(game, { type: 'bid', amount: 6, allyPayment: 2 });
assert.equal(game.response?.kind, 'emperorIncome');
game = act(game, 'harkonnen', { type: 'card', mode: 'cancel', card: karama.id });
game = passResponses(game);
assert.equal(seat(game, 'harkonnen').spice, 0);
assert.equal(seat(game, 'emperor').spice, 8);
assert.ok(seat(game, 'harkonnen').hand.length >= hand);
assert.equal(game.discard.filter(card => card.id === karama.id).length, 1);
});