196 lines
13 KiB
TypeScript
196 lines
13 KiB
TypeScript
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);
|
||
});
|