Files
Dune/tests/ix-homeworld-revival.test.ts

104 lines
6.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { newPlayer, type Game } from '../game/engine';
import { createHomeworldCustody, quoteHomeworldCustody } from '../game/homeworld-custody';
import { homeworldContext } from '../game/homeworld-game';
import {
ixHomeworldRevivalEnabled, quoteIxSuboidBonus, IxHomeworldRevivalError,
type IxSuboidBonusInput,
} from '../game/ix-homeworld-revival';
/** Controlled physical population context, not an earned game chronology. */
function populationContext(population = 5, advanced = false): Pick<Game, 'advanced' | 'players' | 'homeworlds'> {
const players = [newPlayer('ix', 'Ixians', 'ixians'), newPlayer('other', 'Guild', 'guild')];
const native = players[0];
native.reserves = population;
native.tanks = 20 - population;
native.elites!.reserves = Math.min(population, 2);
native.elites!.tanks = 7 - native.elites!.reserves;
return { advanced, players, homeworlds: { custody: createHomeworldCustody(homeworldContext({ advanced, players })) } };
}
void test('native Ix High activates at five current counters in Basic and Advanced without another gate', () => {
for (const advanced of [false, true]) for (const population of [0, 4, 5, 6, 14]) {
const context = populationContext(population, advanced), before = structuredClone(context);
assert.equal(ixHomeworldRevivalEnabled(context, 'ix'), population >= 5);
assert.deepEqual(context, before);
}
});
void test('absent Homeworld custody and nonnative viewers do not receive the Ix revival benefit', () => {
const context = populationContext();
assert.equal(ixHomeworldRevivalEnabled(context, 'other'), false);
assert.equal(ixHomeworldRevivalEnabled(context, 'missing'), false);
assert.equal(ixHomeworldRevivalEnabled({ ...context, homeworlds: undefined }, 'ix'), false);
assert.equal(ixHomeworldRevivalEnabled({ ...context, homeworlds: { custody: null } }, 'ix'), false);
});
void test('foreign visitors do not raise Ix population and native counters visiting elsewhere do not count', () => {
const context = populationContext(4);
const visit = quoteHomeworldCustody(homeworldContext(context), context.homeworlds!.custody!, [
{ homeworld: 'homeworld:guild', player: 'other', withdraw: { normal: 8, elite: 0 }, deposit: { normal: 0, elite: 0 } },
{ homeworld: 'homeworld:ixians', player: 'other', withdraw: { normal: 0, elite: 0 }, deposit: { normal: 8, elite: 0 } },
]);
for (const next of visit.players) context.players.find(player => player.id === next.id)!.reserves = next.reserves;
context.homeworlds!.custody = visit.state;
assert.equal(ixHomeworldRevivalEnabled(context, 'ix'), false);
assert.equal(ixHomeworldRevivalEnabled(context, 'other'), false);
const high = populationContext(5);
const departed = quoteHomeworldCustody(homeworldContext(high), high.homeworlds!.custody!, [
{ homeworld: 'homeworld:ixians', player: 'ix', withdraw: { normal: 1, elite: 0 }, deposit: { normal: 0, elite: 0 } },
{ homeworld: 'homeworld:guild', player: 'ix', withdraw: { normal: 0, elite: 0 }, deposit: { normal: 1, elite: 0 } },
]);
for (const next of departed.players) high.players.find(player => player.id === next.id)!.reserves = next.reserves;
high.homeworlds!.custody = departed.state;
assert.equal(ixHomeworldRevivalEnabled(high, 'ix'), false);
});
const group = (patch: Partial<IxSuboidBonusInput> = {}): IxSuboidBonusInput => ({
enabled: true, amount: 1, elite: 1, free: 0, freeElite: 0, normalTanks: 6, ...patch,
});
void test('each paid Cyborg adds two physical Suboids while free Cyborgs and ordinary returns add none', () => {
assert.equal(quoteIxSuboidBonus(group()), 2);
assert.equal(quoteIxSuboidBonus(group({ amount: 2, elite: 2 })), 4);
assert.equal(quoteIxSuboidBonus(group({ amount: 1, elite: 1, free: 1, freeElite: 1 })), 0);
assert.equal(quoteIxSuboidBonus(group({ amount: 3, elite: 0, free: 1, normalTanks: 6 })), 0);
assert.equal(quoteIxSuboidBonus(group({ enabled: false })), 0);
});
void test('mixed free type allocation counts only paid Cyborgs and reserves the declared ordinary returns first', () => {
const input = group({ amount: 4, elite: 3, free: 2, freeElite: 1, normalTanks: 7 });
const before = structuredClone(input);
assert.equal(quoteIxSuboidBonus(input), 4);
assert.deepEqual(input, before);
assert.equal(quoteIxSuboidBonus(group({ amount: 3, elite: 2, free: 1, freeElite: 0, normalTanks: 5 })), 4);
assert.equal(quoteIxSuboidBonus(group({ amount: 4, elite: 2, free: 1, freeElite: 1, normalTanks: 5 })), 2);
});
void test('physical Suboid shortages give a partial or zero bonus without replacing the declared group', () => {
assert.equal(quoteIxSuboidBonus(group({ normalTanks: 1 })), 1);
assert.equal(quoteIxSuboidBonus(group({ normalTanks: 0 })), 0);
assert.equal(quoteIxSuboidBonus(group({ amount: 3, elite: 1, normalTanks: 3 })), 1);
assert.equal(quoteIxSuboidBonus(group({ amount: 3, elite: 1, normalTanks: 2 })), 0);
assert.equal(quoteIxSuboidBonus(group({ amount: 20, elite: 7, free: 1, normalTanks: 13 })), 0);
});
void test('malformed counts and impossible free type allocations reject even when the benefit is disabled', () => {
const invalid: Partial<IxSuboidBonusInput>[] = [
{ amount: 0 }, { amount: 21 }, { elite: 2 }, { free: 2 },
{ freeElite: 1 }, { amount: 2, elite: 0, free: 1, freeElite: 1 },
{ amount: 2, elite: 2, free: 1, freeElite: 0 },
{ amount: 3, elite: 1, normalTanks: 1 },
];
for (const field of ['amount', 'elite', 'free', 'freeElite', 'normalTanks'] as const)
for (const value of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) invalid.push({ [field]: value });
for (const enabled of [false, true]) for (const patch of invalid) {
const input = group({ ...patch, enabled }), before = structuredClone(input);
assert.throws(() => quoteIxSuboidBonus(input), IxHomeworldRevivalError);
assert.deepEqual(input, before);
}
assert.throws(() => quoteIxSuboidBonus({ ...group(), enabled: 'yes' as unknown as boolean }), IxHomeworldRevivalError);
});