Files
Dune/tests/homeworld-payment-income.test.ts
T

232 lines
6.9 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { newPlayer } from '../game/engine';
import {
createHomeworldCustody,
HomeworldCustodyError,
} from '../game/homeworld-custody';
import { homeworldContext } from '../game/homeworld-game';
import { quoteHomeworldPaymentIncome } from '../game/homeworld-payment-income';
function fixture(
faction: 'guild' | 'emperor',
reserves: number,
advanced = true,
) {
const players = [
newPlayer('native', 'Native', faction),
newPlayer('visitor', 'Visitor', 'harkonnen'),
];
players[0].reserves = reserves;
if (players[0].elites) players[0].elites.reserves = 0;
const context = { advanced, players };
return {
...context,
homeworlds: { custody: createHomeworldCustody(homeworldContext(context)) },
};
}
void test('Junction and Kaitain halve one eligible payment below five native forces in either mode', () => {
for (const advanced of [false, true])
for (const [faction, kind] of [
['guild', 'shipment'],
['emperor', 'treachery'],
] as const)
for (const reserves of [0, 4, 5, 20]) {
const context = fixture(faction, reserves, advanced);
assert.deepEqual(
quoteHomeworldPaymentIncome(context, 'native', kind, 5),
{
gross: 5,
income: reserves < 5 ? 3 : 5,
bank: reserves < 5 ? 2 : 0,
low: reserves < 5,
},
);
}
});
void test('odd, even, zero and maximal safe single payment conserve spice', () => {
const context = fixture('guild', 4);
for (const [gross, income] of [
[0, 0],
[1, 1],
[2, 1],
[3, 2],
[5, 3],
[6, 3],
[Number.MAX_SAFE_INTEGER, 4_503_599_627_370_496],
]) {
const quote = quoteHomeworldPaymentIncome(
context,
'native',
'shipment',
gross,
);
assert.equal(quote.income, income);
assert.equal(quote.bank + quote.income, gross);
assert.equal(quote.low, true);
}
});
void test('low Guild and Emperor round a funded odd-odd invoice once, not each contributor', () => {
for (const advanced of [false, true])
for (const [faction, kind] of [
['guild', 'shipment'],
['emperor', 'treachery'],
] as const) {
const context = fixture(faction, 4, advanced);
const contributions = [5, 3];
const gross = contributions.reduce((sum, amount) => sum + amount, 0);
assert.deepEqual(
quoteHomeworldPaymentIncome(context, 'native', kind, gross),
{ gross: 8, income: 4, bank: 4, low: true },
);
}
});
void test('typed Kaitain natives count while Salusa allocation and visiting armies do not', () => {
const context = fixture('emperor', 10);
context.players[0].elites = { reserves: 5, tanks: 0, forces: {}, revived: 0 };
context.homeworlds.custody.salusa = { normal: 1, elite: 5 };
context.players[1].reserves = 0;
context.homeworlds.custody.visitors['homeworld:emperor'] = {
visitor: { normal: 20, elite: 0 },
};
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5).income,
3,
);
context.homeworlds.custody.salusa.elite--;
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5).income,
5,
);
context.homeworlds.custody.salusa.normal++;
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5).income,
3,
);
});
void test('Junction visitors do not raise physical native population or earn a new payment', () => {
const context = fixture('guild', 4);
context.players[1].reserves = 0;
context.homeworlds.custody.visitors['homeworld:guild'] = {
visitor: { normal: 20, elite: 0 },
};
assert.deepEqual(
quoteHomeworldPaymentIncome(context, 'native', 'shipment', 5),
{
gross: 5,
income: 3,
bank: 2,
low: true,
},
);
assert.throws(
() => quoteHomeworldPaymentIncome(context, 'visitor', 'shipment', 5),
/Only the Guild/,
);
});
void test('absent and uninitialized module preserve native income and current population is rechecked', () => {
for (const homeworlds of [undefined, null, { custody: null }]) {
const context = { ...fixture('emperor', 0), homeworlds };
assert.deepEqual(
quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5),
{
gross: 5,
income: 5,
bank: 0,
low: false,
},
);
}
const context = fixture('guild', 4);
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'shipment', 5).low,
true,
);
context.players[0].reserves++;
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'shipment', 5).low,
false,
);
context.players[0].reserves--;
assert.equal(
quoteHomeworldPaymentIncome(context, 'native', 'shipment', 5).low,
true,
);
});
void test('invalid payment, owner, kind and physical custody reject without mutation', () => {
const context = fixture('emperor', 4);
for (const gross of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
const before = JSON.stringify(context);
assert.throws(
() => quoteHomeworldPaymentIncome(context, 'native', 'treachery', gross),
HomeworldCustodyError,
);
assert.equal(JSON.stringify(context), before);
}
for (const [owner, kind] of [
['native', 'shipment'],
['visitor', 'treachery'],
['absent', 'treachery'],
['native', 'unknown'],
] as const) {
const before = JSON.stringify(context);
assert.throws(
() => quoteHomeworldPaymentIncome(context, owner, kind as 'treachery', 5),
HomeworldCustodyError,
);
assert.equal(JSON.stringify(context), before);
}
context.homeworlds.custody.salusa = { normal: 0, elite: 1 };
const before = JSON.stringify(context);
assert.throws(
() => quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5),
HomeworldCustodyError,
);
assert.equal(JSON.stringify(context), before);
});
void test('payment quotes survive JSON, frozen inputs and throwing private getters on every player', () => {
const context = fixture('emperor', 4);
const restored = JSON.parse(JSON.stringify(context)) as typeof context;
const expected = quoteHomeworldPaymentIncome(
restored,
'native',
'treachery',
5,
);
for (const player of context.players) {
for (const field of [
'hand',
'spice',
'traitors',
'prediction',
'knownTraitors',
'faceDancers',
])
Object.defineProperty(player, field, {
get() {
throw new Error(`private ${field}`);
},
});
Object.freeze(player.elites);
Object.freeze(player);
}
Object.freeze(context.players);
Object.freeze(context.homeworlds.custody.salusa);
Object.freeze(context.homeworlds.custody.visitors);
Object.freeze(context.homeworlds.custody);
Object.freeze(context.homeworlds);
Object.freeze(context);
assert.deepEqual(
quoteHomeworldPaymentIncome(context, 'native', 'treachery', 5),
expected,
);
});