161 lines
10 KiB
TypeScript
161 lines
10 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 {
|
|
advanceNexusEmperorBank, createNexusEmperorBankFixture, nexusEmperorBankInventory,
|
|
nexusEmperorBankPlayer, nextNexusEmperorBankStep, stepNexusEmperorBank,
|
|
type NexusEmperorBankFixture, type NexusEmperorBankOptions,
|
|
} from './fixture-nexus-emperor-bank';
|
|
|
|
function rejectsUnchanged(game: Game, actor: string, action: Action): void {
|
|
const original = structuredClone(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, original, 'Reject before any wallet, card or native lot mutation');
|
|
}
|
|
/** Consume actual original sale/bonus and the next original inspection, not a new auction frame. */
|
|
function finishOriginalLot(f: NexusEmperorBankFixture, game: Game): Game {
|
|
const index = f.game.auction!.index;
|
|
for (let n = 0; n < 100; n++) {
|
|
if (game.auction && game.auction.index === index + 1 &&
|
|
!game.currentAuctionSale && !game.response && !game.phaseOpening && !game.decision) {
|
|
assert.deepEqual(viewGame(game, 'atreides').auction!.card, game.auction.cards[game.auction.index]);
|
|
assert.equal(nexusEmperorBankPlayer(game, f.owner).hand.length,
|
|
nexusEmperorBankPlayer(f.game, f.owner).hand.length + 1 +
|
|
Number(nexusEmperorBankPlayer(game, f.owner).faction === 'harkonnen'));
|
|
assert.equal(nexusEmperorBankPlayer(game, f.owner).hand.filter(c => c.id === f.card.id).length, 1);
|
|
return game;
|
|
}
|
|
assert.notEqual(game.response?.kind, 'emperorIncome', 'No bank-funded replacement income');
|
|
assert.notEqual(game.decision?.kind, 'auctionPayment', 'The acquired lot cannot ask for payment twice');
|
|
game = stepNexusEmperorBank(game, nextNexusEmperorBankStep(game));
|
|
}
|
|
throw Error('Original BANK lot did not reach its native next lot');
|
|
}
|
|
|
|
const natives = ['ixians', 'tleilaxu', 'richese', 'choam', 'ecaz', 'moritani'] as const;
|
|
const scenarios: NexusEmperorBankOptions[] = [
|
|
{ advanced: false }, { advanced: true },
|
|
...natives.flatMap(profile => [false, true].map(advanced => ({ profile, advanced }))),
|
|
{ advanced: true, homeworlds: true, tech: true },
|
|
...(['ixians', 'richese', 'ecaz'] as const).map(profile =>
|
|
({ profile, advanced: true, homeworlds: true, tech: true })),
|
|
];
|
|
|
|
for (const scenario of scenarios) for (const use of [false, true]) void test(
|
|
`${scenario.advanced ? 'Advanced' : 'Basic'} ${scenario.profile ?? 'classic'} original BANK${scenario.homeworlds ? ' Homeworld/Tech' : ''}: ${use ? 'Use retains the positive bid' : 'Pay debits the original bid'}`, () => {
|
|
const f = createNexusEmperorBankFixture(scenario), original = structuredClone(f.game);
|
|
const player = nexusEmperorBankPlayer(f.game, f.owner), offer = viewGame(f.game, f.owner).nexusEmperorSecretAlly;
|
|
assert.ok(offer?.purchase && !offer.purchase.blocked);
|
|
assert.ok(f.price > 0 && player.spice >= f.price);
|
|
assert.equal(player.ally, null);
|
|
assert.ok(!f.game.players.some(p => p.faction === 'emperor'));
|
|
assert.deepEqual(viewGame(f.game, 'atreides').auction!.card, f.card);
|
|
const expectedBonus = player.faction === 'harkonnen' ? structuredClone(f.game.deck[0]) : null;
|
|
const action: Action = use ? { type: 'nexusEmperorPurchase', event: offer.event }
|
|
: { type: 'decision', karama: false };
|
|
let game = stepNexusEmperorBank(f.game, { actor: f.owner, action });
|
|
assert.deepEqual(f.game, original);
|
|
assert.deepEqual(nexusEmperorBankPlayer(game, f.owner).hand.find(c => c.id === f.card.id), f.card);
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], use ? null : 'emperor');
|
|
assert.equal(game.nexusCards!.cards!.discard.filter(c => c === 'emperor').length, use ? 1 : 0);
|
|
game = finishOriginalLot(f, game);
|
|
for (const p of game.players)
|
|
assert.equal(p.spice, f.wallets[p.id] - (p.id === f.owner && !use ? f.price : 0),
|
|
'Only ordinary Pay debits the buyer; no other seat receives bank money');
|
|
if (expectedBonus) assert.deepEqual(nexusEmperorBankPlayer(game, f.owner).hand.find(c => c.id === expectedBonus.id), expectedBonus);
|
|
assert.deepEqual(game.techTokens, original.techTokens);
|
|
assert.deepEqual(game.homeworlds, original.homeworlds);
|
|
if (scenario.profile === 'richese') {
|
|
assert.ok(f.game.richeseRemoved!.length > 0, 'The original unbid cache lot was resolved before the bank pool');
|
|
assert.equal(f.game.richeseBidding!.stage, 'normal');
|
|
assert.deepEqual(game.richeseCache, original.richeseCache);
|
|
assert.deepEqual(game.richeseRemoved, original.richeseRemoved);
|
|
}
|
|
nexusEmperorBankInventory(game);
|
|
if (use) rejectsUnchanged(game, f.owner, action);
|
|
},
|
|
);
|
|
|
|
for (const profile of natives) void test(`${profile}: a real irrelevant closing face still pauses at the same public payment boundary`, () => {
|
|
const held = createNexusEmperorBankFixture({ profile });
|
|
const irrelevant = createNexusEmperorBankFixture({ profile, nexusCard: 'guild' });
|
|
assert.deepEqual(irrelevant.game.decision, held.game.decision);
|
|
assert.equal(viewGame(irrelevant.game, irrelevant.owner).nexusEmperorSecretAlly, null);
|
|
rejectsUnchanged(irrelevant.game, irrelevant.owner, { type: 'nexusEmperorPurchase', event: irrelevant.event });
|
|
const game = finishOriginalLot(irrelevant, stepNexusEmperorBank(irrelevant.game,
|
|
{ actor: irrelevant.owner, action: { type: 'decision', karama: false } }));
|
|
assert.equal(nexusEmperorBankPlayer(game, irrelevant.owner).spice, irrelevant.wallets[irrelevant.owner] - irrelevant.price);
|
|
assert.equal(game.nexusCards!.cards!.hands[irrelevant.owner], 'guild');
|
|
nexusEmperorBankInventory(game);
|
|
});
|
|
|
|
void test('native BANK purchase rejects foreign, stale and extra-cost inputs before changing original custody', () => {
|
|
const f = createNexusEmperorBankFixture({ profile: 'ixians', advanced: true });
|
|
for (const p of f.game.players.filter(p => p.id !== f.owner))
|
|
rejectsUnchanged(f.game, p.id, { type: 'nexusEmperorPurchase', event: f.event });
|
|
for (const action of [
|
|
{ type: 'nexusEmperorPurchase', event: 'stale' },
|
|
{ type: 'nexusEmperorPurchase', event: f.event, price: 0 },
|
|
{ type: 'nexusEmperorPurchase', event: f.event, allyPayment: 1 },
|
|
{ type: 'nexusEmperorPurchase', event: f.event, card: f.card.id },
|
|
{ type: 'nexusEmperorPurchase', event: f.event, use: true },
|
|
]) rejectsUnchanged(f.game, f.owner, action);
|
|
const wallet = nexusEmperorBankPlayer(f.chronology.beforeBid, f.owner).spice;
|
|
rejectsUnchanged(f.chronology.beforeBid, f.owner, { type: 'bid', amount: wallet + 1, allyPayment: 0 });
|
|
nexusEmperorBankInventory(f.game);
|
|
});
|
|
|
|
void test('a genuinely setup-dealt Truthtrance takes priority over the original native bank purchase', () => {
|
|
const f = createNexusEmperorBankFixture({ profile: 'tleilaxu', advanced: true });
|
|
const truth = nexusEmperorBankPlayer(f.game, 'atreides').hand.find(c => c.effect === 'truthtrance');
|
|
assert.ok(truth, 'Truthtrance must come from the original setup deal');
|
|
let game = stepNexusEmperorBank(f.game, { actor: 'atreides', action: { type: 'card', card: truth.id } });
|
|
assert.equal(game.truthtrance!.stage, 'priority');
|
|
rejectsUnchanged(game, f.owner, { type: 'nexusEmperorPurchase', event: f.event });
|
|
while (game.truthtrance?.stage === 'priority') {
|
|
const actor = game.players.find(p => !game.truthtrance!.passed.includes(p.id))!.id;
|
|
game = stepNexusEmperorBank(game, { actor, action: { type: 'truthPass' } });
|
|
}
|
|
const name = nexusEmperorBankPlayer(game, f.owner).hand[0].name;
|
|
game = stepNexusEmperorBank(game, { actor: 'atreides', action: { type: 'truthAsk',
|
|
question: { kind: 'fact', target: f.owner, fact: { kind: 'hand', name } } } });
|
|
assert.equal(viewGame(game, f.owner).truthAnswer, 'yes');
|
|
game = stepNexusEmperorBank(game, { actor: f.owner, action: { type: 'truthAnswer', answer: 'yes' } });
|
|
const offer = viewGame(game, f.owner).nexusEmperorSecretAlly;
|
|
assert.ok(offer?.purchase && !offer.purchase.blocked);
|
|
game = finishOriginalLot(f, stepNexusEmperorBank(game, { actor: f.owner,
|
|
action: { type: 'nexusEmperorPurchase', event: offer.event } }));
|
|
assert.equal(nexusEmperorBankPlayer(game, f.owner).spice, f.wallets[f.owner]);
|
|
assert.equal(game.discard.filter(c => c.id === truth.id).length, 1);
|
|
nexusEmperorBankInventory(game);
|
|
});
|
|
|
|
void test('minimal Easy policy legally completes the actual native bank boundary for both private faces', () => {
|
|
for (const nexusCard of ['emperor', 'guild'] as const) {
|
|
const f = createNexusEmperorBankFixture({ profile: 'choam', advanced: true, nexusCard });
|
|
const view = viewGame(f.game, f.owner);
|
|
view.players.find(p => p.id === f.owner)!.bot = 'Easy';
|
|
const action = botActions(view)[0]; assert.ok(action);
|
|
const game = finishOriginalLot(f, stepNexusEmperorBank(f.game, { actor: f.owner, action }));
|
|
assert.equal(nexusEmperorBankPlayer(game, f.owner).spice, f.wallets[f.owner] - (nexusCard === 'emperor' ? 0 : f.price));
|
|
assert.equal(game.nexusCards!.cards!.hands[f.owner], nexusCard === 'emperor' ? null : 'guild');
|
|
nexusEmperorBankInventory(game);
|
|
}
|
|
});
|
|
|
|
void test('native bank evidence stays historical after a real later purchase changes wallet and hand', () => {
|
|
const f = createNexusEmperorBankFixture({ profile: 'richese', advanced: true, homeworlds: true, tech: true });
|
|
let game = finishOriginalLot(f, stepNexusEmperorBank(f.game,
|
|
{ actor: f.owner, action: { type: 'nexusEmperorPurchase', event: f.event } }));
|
|
while (game.auction!.active !== f.owner)
|
|
game = stepNexusEmperorBank(game, { actor: game.auction!.active, action: { type: 'passBid' } });
|
|
const card = structuredClone(game.auction!.cards[game.auction!.index]);
|
|
game = stepNexusEmperorBank(game, { actor: f.owner, action: { type: 'bid', amount: 1, allyPayment: 0 } });
|
|
game = advanceNexusEmperorBank(game, g => g.decision?.kind === 'auctionPayment');
|
|
game = stepNexusEmperorBank(game, { actor: f.owner, action: { type: 'decision', karama: false } });
|
|
assert.equal(nexusEmperorBankPlayer(game, f.owner).spice, f.wallets[f.owner] - 1);
|
|
assert.deepEqual(nexusEmperorBankPlayer(game, f.owner).hand.find(c => c.id === card.id), card);
|
|
nexusEmperorBankInventory(game);
|
|
});
|