Files
Dune/tests/richese-self-purchase-rounds.test.ts
T

217 lines
12 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import {
allowRicheseBureaucratResponses, chooseRicheseBureaucrat,
richeseBureaucratCards, richeseBureaucratPlayer,
} from './fixture-bureaucrat-richese-funding';
import {
createRicheseSelfPurchaseFixture, payRicheseSelfPurchase,
type RicheseSelfPurchaseFixture,
} from './fixture-richese-self-purchase';
import { nextHomeworldEcazStep } from './fixture-homeworld-ecaz-occupy';
// Provisional shared Richese payment inheritance, not an official ruling on
// positive seller participation. The common producer retains the native setup,
// real Mentat bribe/worm alliance and conserved original counter-card swap.
function retained(game: Game, f: RicheseSelfPurchaseFixture) {
assert.deepEqual(richeseBureaucratPlayer(game, f.seller).hand.map(c => c.id), f.originalHand,
'Paid retention neither removes/reappends the offered card nor makes an acquisition');
assert.equal(richeseBureaucratPlayer(game, f.seller).hand.filter(c => c.id === f.card).length, 1);
for (const player of game.players)
assert.deepEqual(player.hand.map(c => c.id), richeseBureaucratPlayer(f.beforeAction, player.id).hand.map(c => c.id));
assert.deepEqual(richeseBureaucratCards(game), f.physical);
assert.deepEqual(game.richeseCache, f.beforeAction.richeseCache);
}
function rejectUnchanged(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before, 'Rejected input preserves custody, wallets, prepaid aid and the original opportunity');
}
function emperorAllowance(game: Game, f: RicheseSelfPurchaseFixture) {
retained(game, f);
assert.equal(game.response?.kind, 'emperorIncome');
assert.equal(game.response!.owner, f.emperor);
assert.ok(viewGame(game, f.counterActor).responseControls?.cancelCards.includes(f.counterCard!));
assert.equal(viewGame(game, f.trainer!).bureaucrat.pending, null,
'Native Emperor Karama allowance precedes any optional Bureaucrat diversion');
assert.equal(richeseBureaucratPlayer(game, f.emperor!).spice,
richeseBureaucratPlayer(f.beforeAction, f.emperor!).spice);
assert.equal(richeseBureaucratPlayer(game, f.seller).spice,
richeseBureaucratPlayer(f.beforeAction, f.seller).spice - f.primary);
assert.equal(game.currentAuctionSale!.winner, f.seller);
assert.equal(game.currentAuctionSale!.seller, f.seller);
assert.equal(game.currentAuctionSale!.origin, 'blackMarket');
assert.equal(game.currentAuctionSale!.amount, f.price);
assert.equal(game.currentAuctionSale!.free, false);
const invoice = game.currentAuctionSale!.bureaucratPayment!;
assert.equal(invoice.payer, f.seller);
assert.equal(invoice.payee, f.emperor);
assert.equal(invoice.amount, f.price);
}
function declaration(game: Game, f: RicheseSelfPurchaseFixture, sold: boolean) {
retained(game, f);
assert.equal(game.phase, 3);
assert.equal(game.richeseBidding!.blackMarketSold, sold);
assert.equal(game.decision?.kind, 'richeseDeclaration');
assert.equal(game.decision!.player, f.seller);
assert.equal(game.currentAuctionSale, null);
assert.equal(game.richeseAuction, null);
assert.equal(game.response, null);
}
void test('provisional Normal own5 retention pays Emperor once, permits native Karama then Bureaucrat2, and resumes original normal controls', () => {
const f = createRicheseSelfPurchaseFixture({ method: 'normal', price: 5, emperor: true, counter: true });
let game = f.game;
for (let n = 0; game.richeseAuction!.active !== f.seller && n < game.players.length; n++) {
const actor = game.richeseAuction!.active!;
game = applyAction(game, actor, { type: 'richeseBid', event: f.action.event, amount: null });
}
assert.equal(game.richeseAuction!.active, f.seller);
rejectUnchanged(game, f.seller, { ...f.action, event: game.richeseBidding!.event });
rejectUnchanged(game, f.seller, { ...f.action, amount: 6 });
game = payRicheseSelfPurchase({ ...f, game });
emperorAllowance(game, f);
game = allowRicheseBureaucratResponses(game);
assert.equal(game.decision?.kind, 'bureaucratPayment');
const event = game.bureaucratPaymentEvent!;
for (const player of game.players) {
const pending = viewGame(game, player.id).bureaucrat.pending!;
assert.equal(pending.payee, f.emperor);
assert.equal(pending.amount, 5);
assert.equal(pending.payer, f.seller);
}
game = chooseRicheseBureaucrat(game, true);
declaration(game, f, true);
for (const player of game.players) {
const delta = -Number(player.id === f.seller) * 5
+ Number(player.id === f.emperor) * 3;
assert.equal(player.spice, richeseBureaucratPlayer(f.beforeAction, player.id).spice + delta);
}
assert.equal(viewGame(game, f.trainer!).bureaucrat.usedThisPhase, true);
rejectUnchanged(game, f.seller, f.action);
rejectUnchanged(game, f.trainer!, { type: 'decision', event, redirect: true });
const normalCount = game.richeseBidding!.normalCount!;
game = applyAction(game, f.seller,
{ type: 'decision', event: game.richeseBidding!.event, position: 'last' });
game = allowRicheseBureaucratResponses(game);
assert.equal(game.richeseBidding!.stage, 'normal');
assert.equal(game.auction!.cards.length, normalCount);
const next = nextHomeworldEcazStep(game);
assert.ok(next);
assert.equal(next.action.type, 'passBid');
assert.equal(next.actor, game.auction!.active, 'Use the original next controller, not a fixed Storm opener');
game = applyAction(game, next.actor, next.action);
retained(game, f);
assert.equal(game.richeseBidding!.blackMarketSold, true);
assert.equal(game.richeseBidding!.normalCount, normalCount, 'Continuing the auction does not count retention twice');
});
void test('provisional Once Around outbids a real1 with R5/ally3, while all-other-pass remains free unsold without an owner opportunity', () => {
const f = createRicheseSelfPurchaseFixture({ method: 'onceAround', direction: 'counterclockwise',
price: 8, allyPayment: 3, emperor: true, counter: true });
const free = payRicheseSelfPurchase({ ...f, actions: [] }, { openingBid: 0 });
declaration(free, f, false);
for (const player of free.players)
assert.equal(player.spice, richeseBureaucratPlayer(f.beforeAction, player.id).spice);
assert.equal(free.aid[f.donor!].amount, 3, 'Free unsold retention consumes none of the real prepaid aid');
assert.equal(viewGame(free, f.trainer!).bureaucrat.usedThisPhase, false);
rejectUnchanged(free, f.seller, f.action);
let game = f.game;
const openingActor = game.richeseAuction!.active!;
assert.notEqual(openingActor, f.seller);
game = applyAction(game, openingActor, { type: 'richeseBid', event: f.action.event, amount: 1, allyPayment: 0 });
assert.equal(game.richeseAuction!.bidder, openingActor);
assert.equal(game.richeseAuction!.bid, 1);
for (let n = 0; game.richeseAuction!.active !== f.seller && n < game.players.length; n++)
game = applyAction(game, game.richeseAuction!.active!, { type: 'richeseBid', event: f.action.event, amount: null });
assert.equal(game.richeseAuction!.active, f.seller);
rejectUnchanged(game, openingActor, { type: 'richeseBid', event: f.action.event, amount: 2 });
rejectUnchanged(game, f.seller, { ...f.action, amount: 9 });
rejectUnchanged(game, f.seller, { ...f.action, allyPayment: 4 });
game = applyAction(game, f.seller, f.action);
emperorAllowance(game, f);
assert.equal(game.aid[f.donor!].amount, 0);
assert.deepEqual(game.currentAuctionSale!.bureaucratPayment!.contributions,
[{ payer: f.seller, amount: 5 }, { payer: f.donor!, amount: 3 }]);
game = allowRicheseBureaucratResponses(game);
assert.equal(game.decision?.kind, 'bureaucratPayment');
const source = game.bureaucratPayments!.pending!.source;
assert.equal(source.payee, f.emperor);
assert.equal(source.amount, 8);
for (const player of game.players) {
const pending = viewGame(game, player.id).bureaucrat.pending!;
assert.equal(pending.payer, null, 'Actual allied payment shares remain private');
assert.equal(pending.payee, f.emperor);
assert.equal(pending.amount, 8, 'Public payment is the whole original price');
}
game = chooseRicheseBureaucrat(game, false);
declaration(game, f, true);
for (const player of game.players) {
const delta = -Number(player.id === f.seller) * 5 - Number(player.id === f.donor) * 3
+ Number(player.id === f.emperor) * 8;
assert.equal(player.spice, richeseBureaucratPlayer(f.beforePledge, player.id).spice + delta,
'Only actual retained funding is paid; the outbid bidder is not charged and Richese is not refunded');
}
assert.equal(game.aid[f.donor!].amount, 0);
assert.equal(viewGame(game, f.trainer!).bureaucrat.usedThisPhase, false);
assert.equal(free.richeseBidding!.normalCount, game.richeseBidding!.normalCount! + 1,
'Exactly the positive sale, not free retention, removes one ordinary lot');
rejectUnchanged(game, f.seller, f.action);
game = applyAction(game, f.seller,
{ type: 'decision', event: game.richeseBidding!.event, position: 'first' });
game = allowRicheseBureaucratResponses(game);
assert.equal(game.decision?.kind, 'richeseCache');
assert.equal(game.decision!.player, f.seller);
assert.equal(game.richeseBidding!.blackMarketSold, true);
assert.equal(game.auction, null, 'Cache-first continues to the actual cache choice before dealing ordinary cards');
retained(game, f);
});
void test('original no-Emperor Nexus table pays for retained ownership without inventing a free seller acquisition', () => {
let game = createGame('RETAINNEXUS', newPlayer('richese', 'Richese', 'richese'), true, ['choam']);
joinGame(game, newPlayer('atreides', 'Atreides', 'atreides'));
joinGame(game, newPlayer('guild', 'Guild', 'guild'));
game = applyAction(game, game.host, { type: 'moduleOption', module: 'nexusCards', enabled: true });
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
game = applyAction(game, game.host, { type: 'start', advancedPreview: true });
for (let n = 0; game.decision?.kind !== 'richeseBlackMarket' && n < 300; n++) {
let step: { actor: string; action: Action } | undefined;
if (game.status === 'setup') {
for (const player of game.players) {
const view = viewGame(game, player.id);
view.players.find(p => p.id === player.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (action) { step = { actor: player.id, action }; break; }
}
} else step = nextHomeworldEcazStep(game) ?? undefined;
assert.ok(step);
game = applyAction(game, step.actor, step.action);
}
assert.equal(game.decision?.kind, 'richeseBlackMarket');
const card = richeseBureaucratPlayer(game, 'richese').hand[0].id;
game = applyAction(game, 'richese', { type: 'decision', event: game.richeseBidding!.event, card, method: 'silent' });
game = allowRicheseBureaucratResponses(game);
const before = structuredClone(game), event = game.richeseAuction!.event;
game = applyAction(game, 'richese', { type: 'richeseBid', event, amount: 1, allyPayment: 0 });
for (const actor of before.richeseAuction!.order) if (actor !== 'richese')
game = applyAction(game, actor, { type: 'richeseBid', event, amount: 0 });
game = allowRicheseBureaucratResponses(game);
assert.equal(game.decision?.kind, 'richeseDeclaration');
assert.equal(game.richeseBidding!.blackMarketSold, true);
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, richeseBureaucratPlayer(before, 'richese').spice - 1);
for (const player of game.players) {
assert.deepEqual(player.hand, richeseBureaucratPlayer(before, player.id).hand);
if (player.id !== 'richese') assert.equal(player.spice, richeseBureaucratPlayer(before, player.id).spice);
}
assert.deepEqual(game.nexusCards, before.nexusCards);
assert.deepEqual(richeseBureaucratCards(game), richeseBureaucratCards(before));
});