Files
Dune/tests/banker-overlay-default.test.ts
T

191 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction } from '../game/engine';
import {
advanceBankerOverlayToMentat, advanceBankerOverlayToPhase, bankerOverlayDeferred,
bankerOverlayInventory, bankerOverlayLedger, bankerOverlayPlayer, bankerOverlayWallets,
claimBankerOverlaySemuta, createBankerOverlayFixture, openBankerOverlayHajr,
passBankerOverlaySemuta, shipBankerOverlayIncome, type BankerOverlayOptions,
} from './fixture-banker-overlay-default';
const profiles: BankerOverlayOptions[] = [
{ advanced: false, tech: false, strongholds: false },
{ advanced: true, tech: false, strongholds: false },
{ advanced: true, tech: true, strongholds: false },
{ advanced: true, tech: true, strongholds: true },
];
for (const profile of profiles) {
const label = `${profile.advanced ? 'Advanced' : 'Basic'}${profile.tech ? ' Tech' : ''}${profile.strongholds ? ' Strongholds' : ''}`;
void test(`${label} Semuta + Skills: the ordinary Richese Banker keeps its printed income for a real Guild self-shipment paid to the Bank until the native Mentat Pause`, () => {
const fixture = createBankerOverlayFixture(profile);
const before = bankerOverlayWallets(fixture.game);
const payer = before.find(row => row.id === fixture.payer)!;
const owner = before.find(row => row.id === fixture.owner)!;
assert.equal(fixture.kind, 'bank');
assert.equal(fixture.amount, 4);
assert.equal(fixture.cost, 4, 'Four normal forces into empty sand cost four spice at the Guild half rate');
assert.equal(fixture.payee, 'bank');
assert.equal(fixture.grant, true);
assert.equal(fixture.game.semutaPreview, true);
assert.equal(fixture.game.spiceBankerIncomePreview, true);
assert.equal(bankerOverlayLedger(fixture.game, fixture.owner).preview, true);
assert.deepEqual(bankerOverlayLedger(fixture.game, fixture.owner).deferred, [],
'No income exists before the real payment');
assert.equal(payer.shipped, false);
assert.equal(fixture.game.active, fixture.payer);
const paid = shipBankerOverlayIncome(fixture);
const ledger = bankerOverlayLedger(paid, fixture.owner);
const receipt = paid.spiceBankerIncome!.sources.at(-1)!;
assert.equal(paid.spiceBankerIncome!.sources.length, 1, 'Only the one real Bank payment is recorded');
assert.equal(paid.spiceBankerIncome!.collections.length, 0);
assert.equal(receipt.source.kind, 'shipment');
assert.equal(receipt.source.turn, 1);
assert.equal(receipt.source.phase, 5);
assert.deepEqual(receipt.source.bankLegs, [{ payer: fixture.payer, amount: fixture.cost }],
'The deferred invoice keeps the exact real Bank leg of this payment');
assert.equal(receipt.grant!.owner, fixture.owner, 'Another payer paid the Bank, so the printed grant is Richese’s');
assert.equal(receipt.grant!.amount, 1);
assert.deepEqual(receipt.source.trainer!.assignment,
{ skill: 'spice-banker', leader: fixture.trainer, owner: fixture.owner });
assert.equal(receipt.source.trainer!.faceUp, true, 'The actual Banker trainer must be face up');
assert.equal(receipt.source.trainer!.survived, true);
assert.deepEqual(ledger.deferred, [{ owner: fixture.owner, amount: 1 }],
'The printed spice sits in front of the shield, unavailable until the Mentat Pause');
assert.deepEqual(ledger.usedThisPhase, [fixture.owner]);
assert.equal(bankerOverlayDeferred(paid, fixture.owner), 1);
// The original payment itself is unchanged and already spent this phase.
const after = bankerOverlayWallets(paid);
assert.equal(after.find(row => row.id === fixture.payer)!.spice, payer.spice - 4);
assert.equal(after.find(row => row.id === fixture.payer)!.reserves, payer.reserves - 4);
assert.equal(after.find(row => row.id === fixture.payer)!.shipped, true);
assert.equal(after.find(row => row.id === fixture.payer)!.moved, payer.moved);
assert.equal(after.find(row => row.id === fixture.owner)!.spice, owner.spice,
'Deferred income is not spendable and must not change the Banker’s wallet');
assert.equal(bankerOverlayPlayer(paid, fixture.payer).forces[fixture.key], fixture.amount);
assert.equal(paid.response, null, 'The Guild’s own shipment owes no Guild income response');
assert.throws(() => applyAction(paid, fixture.payer, fixture.shipment),
'The same native shipment cannot repeat its payment or income');
// The printed overlay continuation runs on a real fresh discard, then the
// original ordinary-card continuation resumes with the same income untouched.
const paused = openBankerOverlayHajr(paid, fixture);
const beforeOverlay = bankerOverlayLedger(paused, fixture.owner);
const claimed = claimBankerOverlaySemuta(paused, fixture);
assert.deepEqual(bankerOverlayLedger(claimed, fixture.owner), beforeOverlay,
'Claiming through the original Semuta overlay must not create, move or duplicate Banker income');
assert.deepEqual(claimed.spiceBankerIncome, paused.spiceBankerIncome);
const collection = advanceBankerOverlayToPhase(claimed, 7, fixture.plan);
assert.deepEqual(bankerOverlayLedger(collection, fixture.owner).deferred, [{ owner: fixture.owner, amount: 1 }],
'The printed spice must still be deferred through the ordinary Collection phase');
const balance = bankerOverlayPlayer(collection, fixture.owner).spice;
const mentat = advanceBankerOverlayToMentat(collection, fixture.plan);
const mentatLedger = bankerOverlayLedger(mentat, fixture.owner);
assert.deepEqual(mentatLedger.deferred, [], 'The actual Mentat Pause collects the deferred spice');
assert.deepEqual(mentatLedger.usedThisPhase, []);
assert.equal(bankerOverlayPlayer(mentat, fixture.owner).spice, balance + 1,
'The Mentat Pause credits exactly the one printed spice');
assert.equal(mentat.spiceBankerIncome!.collections.length, 1);
assert.deepEqual(mentat.spiceBankerIncome!.collections[0].credits, [{ owner: fixture.owner, amount: 1 }]);
assert.deepEqual(mentat.spiceBankerIncome!.collections[0].grants, [receipt.cursor]);
assert.equal(mentat.spiceBankerIncome!.sources.length, 1);
assert.equal(mentat.pendingTreacheryDiscard, null);
assert.equal(mentat.response, null);
assert.equal(mentat.decision, null);
assert.equal(mentat.status, 'playing');
// Original overlay and card custody complete: the claimed card stays claimed.
assert.equal(bankerOverlayPlayer(mentat, fixture.owner).hand.filter(card => card.id === fixture.hajr).length, 1);
assert.equal(mentat.discard.filter(card => card.id === fixture.semuta).length, 1);
assert.deepEqual(bankerOverlayInventory(mentat), bankerOverlayInventory(fixture.game));
});
}
void test('Basic Semuta + Skills: the same real Guild self-shipment below the printed band records no grant', () => {
const fixture = createBankerOverlayFixture({ kind: 'below', semuta: 'pass' });
assert.equal(fixture.amount, 3);
assert.equal(fixture.cost, 3);
assert.equal(fixture.payee, 'bank');
assert.equal(fixture.grant, false);
const before = bankerOverlayWallets(fixture.game);
const payer = before.find(row => row.id === fixture.payer)!;
const owner = before.find(row => row.id === fixture.owner)!;
const paid = shipBankerOverlayIncome(fixture);
assert.equal(paid.spiceBankerIncome!.sources.length, 1,
'The real Bank payment is still recorded, without the printed grant');
assert.equal(paid.spiceBankerIncome!.sources[0].grant, null);
assert.deepEqual(paid.spiceBankerIncome!.sources[0].source.bankLegs, [{ payer: fixture.payer, amount: 3 }]);
assert.deepEqual(bankerOverlayLedger(paid, fixture.owner).deferred, []);
assert.deepEqual(bankerOverlayLedger(paid, fixture.owner).usedThisPhase, []);
assert.equal(bankerOverlayPlayer(paid, fixture.payer).spice, payer.spice - 3);
const paused = openBankerOverlayHajr(paid, fixture);
const passed = passBankerOverlaySemuta(paused, fixture);
assert.deepEqual(passed.spiceBankerIncome, paid.spiceBankerIncome,
'Declining the printed overlay must not create Banker income');
const collection = advanceBankerOverlayToPhase(passed, 7, fixture.plan);
const balance = bankerOverlayPlayer(collection, fixture.owner).spice;
assert.equal(balance, owner.spice,
'This Richese holds no board forces, so ordinary Collection does not change its wallet');
const mentat = advanceBankerOverlayToMentat(collection, fixture.plan);
assert.equal(bankerOverlayPlayer(mentat, fixture.owner).spice, balance,
'The Mentat Pause credits nothing without a printed grant');
assert.equal(mentat.spiceBankerIncome!.collections.length, 1);
assert.deepEqual(mentat.spiceBankerIncome!.collections[0].credits, []);
assert.deepEqual(mentat.spiceBankerIncome!.collections[0].grants, []);
assert.equal(mentat.discard.filter(card => card.id === fixture.hajr).length, 1);
assert.equal(bankerOverlayPlayer(mentat, fixture.owner).hand.filter(card => card.id === fixture.semuta).length, 1);
assert.deepEqual(bankerOverlayInventory(mentat), bankerOverlayInventory(fixture.game));
});
void test('Basic Semuta + Skills: another payer’s real four-spice shipment to the seated Guild player is not a Bank payment', () => {
const fixture = createBankerOverlayFixture({ kind: 'player' });
assert.equal(fixture.payer, 'a');
assert.equal(fixture.payerFaction, 'atreides');
assert.equal(fixture.amount, 2);
assert.equal(fixture.cost, 4, 'Two normal forces at the full desert price still pay four spice');
assert.equal(fixture.payee, 'guild');
assert.equal(fixture.grant, false);
const before = bankerOverlayWallets(fixture.game);
const atreides = before.find(row => row.id === 'a')!;
const guild = before.find(row => row.id === 'g')!;
const paid = shipBankerOverlayIncome(fixture);
assert.equal(paid.spiceBankerIncome!.sources.length, 0,
'A price paid to a player is not a Bank payment, so no invoice is recorded');
assert.equal(paid.spiceBankerIncome!.collections.length, 0);
assert.deepEqual(bankerOverlayLedger(paid, fixture.owner).deferred, []);
assert.equal(bankerOverlayPlayer(paid, 'a').spice, atreides.spice - 4);
assert.equal(bankerOverlayPlayer(paid, 'g').spice, guild.spice + 4,
'The seated Guild still receives its ordinary shipment income');
assert.equal(bankerOverlayPlayer(paid, 'a').shipped, true);
assert.equal(bankerOverlayPlayer(paid, 'a').forces[fixture.key], 2);
const paused = openBankerOverlayHajr(paid, fixture);
const claimed = claimBankerOverlaySemuta(paused, fixture);
assert.equal(claimed.spiceBankerIncome!.sources.length, 0);
const collection = advanceBankerOverlayToPhase(claimed, 7, fixture.plan);
const balance = bankerOverlayPlayer(collection, fixture.owner).spice;
const mentat = advanceBankerOverlayToMentat(collection, fixture.plan);
assert.equal(bankerOverlayPlayer(mentat, fixture.owner).spice, balance);
assert.deepEqual(mentat.spiceBankerIncome!.collections[0].credits, []);
assert.equal(mentat.spiceBankerIncome!.collections.length, 1);
});
void test('Basic Semuta + Skills: supplied Banker authority on the ordinary native shipment is rejected without change', () => {
const fixture = createBankerOverlayFixture({ semuta: 'pass' });
const before = structuredClone(fixture.game);
for (const key of ['actor', 'bankerIncome', 'paymentSource', 'spiceBankerIncome', 'bankLegs', 'trainer']) {
assert.throws(() => applyAction(fixture.game, fixture.payer,
{ ...fixture.shipment, [key]: 'supplied' }));
assert.deepEqual(fixture.game, before);
}
const paid = shipBankerOverlayIncome(fixture);
assert.deepEqual(paid.spiceBankerIncome!.sources.at(-1)!.source.bankLegs,
[{ payer: fixture.payer, amount: fixture.cost }],
'Only the original native transaction can supply the deferred Bank legs');
assert.throws(() => applyAction(paid, fixture.payer, fixture.shipment));
assert.equal(paid.spiceBankerIncome!.sources.length, 1);
});