Connect Emperor Nexus compulsory funding to native auction settlement

This commit is contained in:
Quintine committed 2026-10-15 06:38:14 +11:00
1 parent 25996ec8a7
commit 022040972c
18 files changed
+1291 -16

No files matched your search

+195
View File
@@ -0,0 +1,195 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import {
createNexusEmperorPaymentFixture, nexusEmperorPaymentCards, nexusEmperorPaymentInventory,
nexusEmperorPaymentPlayer, stepNexusEmperorPayment,
} from './fixture-nexus-emperor-payment';
function rejectsUnchanged(game: Game, actor: string, action: Action): void {
const original = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, original, 'A rejected activation must not mutate its original source');
}
for (const advanced of [false, true]) {
for (const prepaidAid of [0, 1]) for (const cancelIncome of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} genuine allied purchase reuses ${prepaidAid} prepaid spice and ${cancelIncome ? 'cancels' : 'allows'} original Emperor income`, () => {
const f = createNexusEmperorPaymentFixture({ advanced, prepaidAid });
assert.equal(f.game.decision!.player, f.neutral);
assert.deepEqual(f.game.pendingNexusEmperorPayment!.receipt.required, [f.neutral, f.owner]);
for (const actor of [f.neutral, f.winner, f.emperor, f.owner])
assert.equal(viewGame(f.game, actor).nexusEmperorPayment, null,
'No private Emperor offer appears before its holder is current');
rejectsUnchanged(f.game, f.owner, { type: 'nexusEmperorPayment', event: f.event, use: true });
rejectsUnchanged(f.game, f.neutral, { type: 'nexusEmperorPayment', event: f.event, use: true });
const beforeNeutral = structuredClone(f.game);
let game = stepNexusEmperorPayment(f.game, { actor: f.neutral,
action: { type: 'nexusEmperorPayment', event: f.event, use: false } });
assert.equal(game.decision!.kind, 'nexusEmperorPayment');
assert.equal(game.decision!.player, f.owner);
assert.deepEqual(game.players, beforeNeutral.players, 'Neutral Continue changes neither wallet nor card custody');
assert.deepEqual(game.auction, beforeNeutral.auction, 'Neutral Continue cannot create a new bid');
const offer = viewGame(game, f.owner).nexusEmperorPayment!;
assert.equal(offer.canUse, true); assert.equal(offer.blocked, null);
for (const actor of [f.neutral, f.winner, f.emperor])
assert.equal(viewGame(game, actor).nexusEmperorPayment, null);
rejectsUnchanged(game, f.owner, { type: 'nexusEmperorPayment', event: `${f.event}:stale`, use: true });
game = stepNexusEmperorPayment(game, { actor: f.owner,
action: { type: 'nexusEmperorPayment', event: f.event, use: true } });
assert.equal(game.pendingNexusEmperorPayment, null);
assert.equal(game.nexusEmperorPayment!.current, null);
const receipt = game.nexusEmperorPayment!.completed.at(-1)!;
assert.equal(receipt.event, f.event); assert.equal(receipt.stage, 'used');
assert.equal(receipt.owner, f.owner); assert.deepEqual(receipt.passed, [f.neutral]);
assert.equal(game.nexusCards!.cards!.hands[f.owner], null);
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'emperor').length, 1);
assert.equal(game.nexusCards!.cards!.hands[f.neutral], 'fremen');
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner,
'Full forced funding leaves the buyer’s original balance unchanged');
assert.equal(nexusEmperorPaymentPlayer(game, f.emperor).spice, f.wallets.emperor - (f.price - prepaidAid),
'Prepaid credit is already deducted; only the missing spice is newly escrowed');
assert.deepEqual(game.aid[f.emperor], { recipient: f.winner, amount: 0 });
assert.equal(game.auction!.bid, f.price); assert.equal(game.auction!.bidder, f.winner);
assert.equal(game.auction!.allyPayment, f.price);
assert.equal(game.auction!.index, f.game.auction!.index);
assert.equal(game.currentAuctionSale!.origin, 'normal');
assert.equal(game.currentAuctionSale!.winner, f.winner);
assert.equal(game.currentAuctionSale!.amount, f.price); assert.equal(game.currentAuctionSale!.free, false);
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).hand.filter(card => card.id === f.acquired).length, 1);
assert.equal(game.response!.kind, 'emperorIncome', 'Only the original native income cancellation window opens');
assert.equal(game.response!.owner, f.emperor);
assert.ok(!game.pendingKarama);
rejectsUnchanged(game, f.owner, { type: 'nexusEmperorPayment', event: f.event, use: true });
if (cancelIncome) {
game = stepNexusEmperorPayment(game, { actor: f.neutral,
action: { type: 'card', mode: 'cancel', card: f.karama } });
assert.equal(game.discard.filter(card => card.id === f.karama).length, 1);
} else {
while (game.response?.kind === 'emperorIncome') {
const actor = game.players.find(player => !game.response!.passed.includes(player.id))!.id;
game = stepNexusEmperorPayment(game, { actor, action: { type: 'passResponse' } });
}
}
assert.equal(nexusEmperorPaymentPlayer(game, f.emperor).spice,
f.wallets.emperorBeforeAid - (cancelIncome ? f.price : 0),
'Original income is credited once or canceled without refunding the original forced payment');
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner);
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).hand.filter(card => card.id === f.acquired).length, 1);
assert.equal(game.auction!.index, f.game.auction!.index + 1, 'Original successor lot advances exactly once');
assert.equal(game.response!.kind, 'atreidesAuction', 'Original next-lot inspection is restored');
assert.equal(game.nexusEmperorPayment!.completed.length, 1);
assert.deepEqual(nexusEmperorPaymentCards(game), f.originalCards);
nexusEmperorPaymentInventory(game);
},
);
for (const prepaidAid of [0, 1, 2]) void test(
`${advanced ? 'Advanced' : 'Basic'} every real acknowledgement passes and preserves ordinary payment with ${prepaidAid} pledged spice`, () => {
const f = createNexusEmperorPaymentFixture({ advanced, prepaidAid });
let game = f.game;
while (game.decision?.kind === 'nexusEmperorPayment') {
const actor = game.decision.player;
if (actor === f.owner && prepaidAid === f.price) {
assert.equal(viewGame(game, actor).nexusEmperorPayment!.canUse, false);
rejectsUnchanged(game, actor, { type: 'nexusEmperorPayment', event: f.event, use: true });
}
game = stepNexusEmperorPayment(game, { actor,
action: { type: 'nexusEmperorPayment', event: f.event, use: false } });
}
assert.equal(game.decision!.kind, 'auctionPayment'); assert.equal(game.decision!.player, f.winner);
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner);
assert.equal(nexusEmperorPaymentPlayer(game, f.emperor).spice, f.wallets.emperor);
assert.equal(game.nexusCards!.cards!.hands[f.owner], 'emperor');
assert.equal(game.nexusEmperorPayment!.completed.at(-1)!.stage, 'passed');
assert.equal(game.auction!.bid, f.price); assert.equal(game.auction!.allyPayment, prepaidAid);
game = stepNexusEmperorPayment(game, { actor: f.winner, action: { type: 'decision', karama: false } });
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner - f.price + prepaidAid);
assert.equal(nexusEmperorPaymentPlayer(game, f.emperor).spice, f.wallets.emperor);
assert.equal(game.response!.kind, 'emperorIncome');
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).hand.filter(card => card.id === f.acquired).length, 1);
assert.equal(game.nexusEmperorPayment!.completed.length, 1, 'Ordinary settlement does not reopen the same obligation');
nexusEmperorPaymentInventory(game);
},
);
}
void test('real neutral response rejects stale events, wrong actors, extra keys and coercion atomically', () => {
const f = createNexusEmperorPaymentFixture();
for (const actor of [f.owner, f.emperor, f.winner])
rejectsUnchanged(f.game, actor, { type: 'nexusEmperorPayment', event: f.event, use: false });
for (const action of [
{ type: 'nexusEmperorPayment', event: 'not-the-original-event', use: false },
{ type: 'nexusEmperorPayment', event: f.event, use: false, amount: 2 },
{ type: 'nexusEmperorPayment', event: f.event, use: false, policy: 'full' },
{ type: 'nexusEmperorPayment', event: f.event, use: 'false' },
{ type: 'nexusEmperorPayment', event: f.event },
{ type: 'decision', event: f.event, use: false },
]) rejectsUnchanged(f.game, f.neutral, action);
const ownerTurn = stepNexusEmperorPayment(f.game, { actor: f.neutral,
action: { type: 'nexusEmperorPayment', event: f.event, use: false } });
rejectsUnchanged(ownerTurn, f.neutral, { type: 'nexusEmperorPayment', event: f.event, use: false });
rejectsUnchanged(ownerTurn, f.owner, { type: 'nexusEmperorPayment', event: f.event, use: true, amount: 2 });
});
void test('a real closing draw without the irrelevant holder gives only the actual remaining public responder', () => {
const f = createNexusEmperorPaymentFixture({ neutralHolder: false });
assert.equal(f.game.decision!.player, f.owner);
assert.deepEqual(f.game.pendingNexusEmperorPayment!.receipt.required, [f.owner]);
assert.equal(viewGame(f.game, f.owner).nexusEmperorPayment!.canUse, true);
assert.equal(f.game.nexusCards!.cards!.hands[f.neutral], null);
const game = stepNexusEmperorPayment(f.game, { actor: f.owner,
action: { type: 'nexusEmperorPayment', event: f.event, use: true } });
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner);
assert.equal(game.response!.kind, 'emperorIncome');
nexusEmperorPaymentInventory(game);
});
void test('naturally setup-dealt Truthtrance has priority over the original acknowledgement and preserves its event', () => {
const f = createNexusEmperorPaymentFixture({ truthtrance: true });
const card = nexusEmperorPaymentPlayer(f.game, f.neutral).hand.find(held => held.effect === 'truthtrance')!;
assert.ok(card);
let game = stepNexusEmperorPayment(f.game, { actor: f.neutral, action: { type: 'card', card: card.id } });
assert.equal(game.truthtrance!.stage, 'priority');
assert.equal(game.pendingNexusEmperorPayment!.receipt.event, f.event);
rejectsUnchanged(game, f.neutral, { type: 'nexusEmperorPayment', event: f.event, use: false });
rejectsUnchanged(game, f.owner, { type: 'nexusEmperorPayment', event: f.event, use: true });
while (game.truthtrance?.stage === 'priority') {
const actor = game.players.find(player => !game.truthtrance!.passed.includes(player.id))!.id;
game = stepNexusEmperorPayment(game, { actor, action: { type: 'truthPass' } });
}
game = stepNexusEmperorPayment(game, { actor: f.neutral, action: { type: 'truthAsk',
question: { kind: 'fact', target: f.winner, fact: { kind: 'hand', name: 'Karama' } } } });
assert.equal(viewGame(game, f.winner).truthAnswer, 'yes');
game = stepNexusEmperorPayment(game, { actor: f.winner, action: { type: 'truthAnswer', answer: 'yes' } });
assert.equal(game.truthtrance, null);
assert.equal(game.decision!.kind, 'nexusEmperorPayment');
assert.equal(game.decision!.player, f.neutral);
assert.equal(game.pendingNexusEmperorPayment!.receipt.event, f.event);
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner);
assert.equal(nexusEmperorPaymentPlayer(game, f.emperor).spice, f.wallets.emperor);
assert.equal(game.discard.filter(held => held.id === card.id).length, 1);
game = stepNexusEmperorPayment(game, { actor: f.neutral,
action: { type: 'nexusEmperorPayment', event: f.event, use: false } });
game = stepNexusEmperorPayment(game, { actor: f.owner,
action: { type: 'nexusEmperorPayment', event: f.event, use: true } });
assert.equal(game.response!.kind, 'emperorIncome');
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).hand.filter(held => held.id === f.acquired).length, 1);
nexusEmperorPaymentInventory(game);
});
void test('original Easy bot policy passes for an irrelevant real holder then uses its own eligible Emperor Nexus', () => {
const f = createNexusEmperorPaymentFixture();
let game = f.game;
for (const actor of [f.neutral, f.owner]) {
const view = viewGame(game, actor);
view.players.find(player => player.id === actor)!.bot = 'Easy';
const action = botActions(view)[0];
game = stepNexusEmperorPayment(game, { actor, action });
}
assert.equal(game.response!.kind, 'emperorIncome');
assert.equal(nexusEmperorPaymentPlayer(game, f.winner).spice, f.wallets.winner);
assert.equal(game.nexusEmperorPayment!.completed.at(-1)!.stage, 'used');
nexusEmperorPaymentInventory(game);
});