Files
Dune/tests/residual-poison.test.ts
2026-09-08 07:37:52 +10:00

175 lines
5.6 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { FACTIONS } from '../game/catalog';
import { leaders, type Leader } from '../game/cards';
import { createDukeVidal } from '../game/duke-vidal';
import {
residualPoisonCandidates,
residualPoisonDeath,
} from '../game/residual-poison';
void test('all twelve canonical rosters contribute five independent equally weighted physical discs', () => {
assert.equal(FACTIONS.length, 12);
for (const faction of FACTIONS) {
const roster = leaders(faction.id);
const before = structuredClone(roster);
const pool = residualPoisonCandidates(roster, 'arrakeen');
assert.equal(pool.length, 5, faction.id);
assert.equal(new Set(pool.map((leader) => leader.id)).size, 5);
assert.deepEqual(pool, roster);
assert.notEqual(pool, roster);
for (let i = 0; i < pool.length; i++) assert.notEqual(pool[i], roster[i]);
pool[0].dead = true;
assert.deepEqual(roster, before);
}
});
void test('dead and elsewhere-used leaders are excluded while same-territory reuse remains eligible', () => {
const roster = leaders('atreides');
roster[0].dead = true;
roster[0].deaths = 1;
roster[1].usedAt = 'carthag';
roster[2].usedAt = 'arrakeen';
assert.deepEqual(
residualPoisonCandidates(roster, 'arrakeen').map((leader) => leader.id),
roster.slice(2).map((leader) => leader.id),
);
assert.deepEqual(residualPoisonCandidates([], 'arrakeen'), []);
assert.deepEqual(
residualPoisonCandidates(roster.slice(0, 2), 'arrakeen'),
[],
);
});
void test('already-controlled foreign ghola, captive, Duke and Zoal retain one entry each', () => {
const ghola = { ...leaders('fremen')[0], gholaBy: 't' };
const captive = {
...leaders('emperor')[0],
capturedBy: 'h',
concealed: {
captor: 'h',
controller: 'e',
dead: false,
deaths: 0,
},
};
const duke = { ...createDukeVidal().leader, controller: 'm' };
const zoal = leaders('tleilaxu').find((leader) => leader.name === 'Zoal')!;
assert.ok(zoal);
const input = [ghola, captive, duke, zoal];
const pool = residualPoisonCandidates(input, 'arrakeen');
assert.deepEqual(pool, input);
assert.equal(pool.length, 4);
assert.equal(pool.filter((leader) => leader.id === zoal.id).length, 1);
assert.notEqual(pool[1].concealed, captive.concealed);
pool[1].concealed!.deaths = 9;
assert.equal(captive.concealed.deaths, 0);
});
void test('death is immutable and changes only alive state, death count and current territory use', () => {
const input: Leader = {
...createDukeVidal().leader,
controller: 'm',
capturedBy: 'h',
gholaBy: 't',
deaths: 3,
usedAt: 'arrakeen',
concealed: {
captor: 'h',
controller: 't',
dead: false,
deaths: 2,
usedAt: 'carthag',
},
};
const before = structuredClone(input);
const dead = residualPoisonDeath(input);
const expected = { ...before, dead: true, deaths: 4 };
delete expected.usedAt;
assert.deepEqual(dead, expected);
assert.deepEqual(input, before);
assert.notEqual(dead.concealed, input.concealed);
assert.throws(() => residualPoisonDeath(dead), /already in the Tanks/);
});
void test('JSON-restored candidates and deaths preserve physical identity and independent history', () => {
const input = leaders('richese');
input[1].usedAt = 'arrakeen';
input[2].dead = true;
input[2].deaths = 2;
const restored: Leader[] = JSON.parse(JSON.stringify(input));
const pool = residualPoisonCandidates(restored, 'arrakeen');
const result = residualPoisonDeath(pool[1]);
assert.equal(result.id, restored[1].id);
assert.equal(result.deaths, 1);
assert.equal('usedAt' in result, false);
assert.deepEqual(restored, input);
assert.deepEqual(JSON.parse(JSON.stringify(result)), result);
});
void test('duplicate physical IDs reject even if the duplicate would be ineligible', () => {
const leader = leaders('guild')[0];
for (const duplicate of [
leader,
{ ...leader, dead: true, deaths: 1 },
{ ...leader, usedAt: 'carthag' },
])
assert.throws(
() => residualPoisonCandidates([leader, duplicate], 'arrakeen'),
/duplicate physical/,
);
});
void test('malformed restored counts, sparse pools and death overflow reject without partial results', () => {
const base = leaders('choam')[0];
for (const field of ['deaths', 'strength'] as const)
for (const invalid of [
-1,
0.5,
NaN,
Infinity,
Number.MAX_SAFE_INTEGER + 1,
'1',
null,
]) {
const malformed = { ...base, [field]: invalid } as Leader;
assert.throws(
() => residualPoisonCandidates([malformed], 'arrakeen'),
/valid physical/,
);
assert.throws(() => residualPoisonDeath(malformed), /valid physical/);
}
for (const patch of [{ id: '' }, { dead: 0 }, { usedAt: '' }, { name: null }])
assert.throws(
() =>
residualPoisonCandidates([{ ...base, ...patch } as Leader], 'arrakeen'),
/valid physical/,
);
const sparse: Leader[] = [];
sparse.length = 2;
assert.throws(
() => residualPoisonCandidates(sparse, 'arrakeen'),
/valid physical/,
);
assert.throws(
() => residualPoisonCandidates(null as unknown as Leader[], 'arrakeen'),
/leader pool/,
);
assert.throws(
() => residualPoisonCandidates([base], ' '),
/battle territory/,
);
assert.throws(
() => residualPoisonDeath({ ...base, deaths: Number.MAX_SAFE_INTEGER }),
/overflow/,
);
assert.throws(
() =>
residualPoisonCandidates(
[{ ...base, concealed: { captor: 'h', dead: false, deaths: -1 } }],
'arrakeen',
),
/concealed leader history/,
);
});