217 lines
12 KiB
TypeScript
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));
|
|
});
|