232 lines
6.9 KiB
TypeScript
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,
|
|
);
|
|
});
|