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

360 lines
10 KiB
TypeScript
Raw Permalink 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 { newPlayer } from '../game/engine';
import { createHomeworldCustody } from '../game/homeworld-custody';
import { homeworldContext } from '../game/homeworld-game';
import {
quoteHomeworldRevivalDeployment,
type HomeworldRevivalDeploymentGroup,
type HomeworldRevivalDeploymentSource,
} from '../game/homeworld-revival-deployment';
function fixture(
faction: 'fremen' | 'tleilaxu' | 'emperor',
reserves: number,
advanced = true,
) {
const players = [
newPlayer('p', 'Native', faction),
newPlayer('v', 'Visitor', 'harkonnen'),
];
players[0].reserves = reserves;
if (faction === 'fremen' || faction === 'emperor')
players[0].elites = {
reserves: 0,
tanks: faction === 'fremen' ? 3 : 5,
forces: {},
revived: 0,
};
const context = { advanced, players };
return {
...context,
homeworlds: { custody: createHomeworldCustody(homeworldContext(context)) },
};
}
function deposited(
before: ReturnType<typeof fixture>,
group: HomeworldRevivalDeploymentGroup,
) {
const after = structuredClone(before);
after.players[0].reserves += group.amount;
if (after.players[0].elites) after.players[0].elites.reserves += group.elite;
if (after.homeworlds.custody.salusa)
after.homeworlds.custody.salusa.elite += group.elite;
return after;
}
void test('high Southern Hemisphere returns only the newly revived Fedaykin subset for each supported revival source', () => {
for (const advanced of [false, true])
for (const source of ['normal', 'emperorExtra', 'ghola', 'nexusTleilaxu'] as const) {
const before = fixture('fremen', 3, advanced);
before.players[0].elites!.reserves = 1;
const group = { amount: 4, elite: 2, free: source === 'normal' ? 2 : 0 };
const after = deposited(before, group);
assert.deepEqual(
quoteHomeworldRevivalDeployment(before, after, 'p', source, group),
{
kind: 'fedaykin',
normal: 0,
elite: 2,
beforePopulation: 3,
afterPopulation: 7,
blocked: null,
},
);
}
});
void test('high Tleilax selects only actual normal Free-Revived forces from a mixed paid/free group', () => {
for (const advanced of [false, true]) {
const before = fixture('tleilaxu', 9, advanced);
const group = { amount: 5, elite: 0, free: 2 };
const after = deposited(before, group);
assert.deepEqual(
quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', group),
{
kind: 'tleilax',
normal: 2,
elite: 0,
beforePopulation: 9,
afterPopulation: 14,
blocked: null,
},
);
for (const source of ['emperorExtra', 'ghola'] as const)
assert.equal(
quoteHomeworldRevivalDeployment(before, after, 'p', source, group),
null,
);
assert.equal(
quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', {
...group,
free: 0,
}),
null,
);
}
});
void test('reaching the high threshold grants the same return’s redeployment while remaining low grants nothing', () => {
for (const [faction, start, elite] of [
['fremen', 2, 1],
['tleilaxu', 8, 0],
] as const) {
const before = fixture(faction, start);
const group = { amount: 1, elite, free: 1 };
const result = quoteHomeworldRevivalDeployment(
before,
deposited(before, group),
'p',
'normal',
group,
)!;
assert.ok(result);
assert.equal(result.beforePopulation, start);
assert.equal(result.afterPopulation, start + 1);
// Provisional composition policy: the population resulting from this
// return decides the ability, so the crossing return itself may redeploy.
assert.equal(result.blocked, null);
assert.equal(result.elite, faction === 'fremen' ? elite : 0);
assert.equal(result.normal, faction === 'tleilaxu' ? group.free : 0);
const low = fixture(faction, start - 1);
assert.equal(
quoteHomeworldRevivalDeployment(
low,
deposited(low, group),
'p',
'normal',
group,
),
null,
);
}
});
void test('normal-only Fremen returns and correctly routed Emperor deposits create no deployment permission', () => {
for (const [faction, group] of [
['fremen', { amount: 2, elite: 0, free: 2 }],
['emperor', { amount: 3, elite: 2, free: 1 }],
] as const) {
const before = fixture(faction, 5);
assert.equal(
quoteHomeworldRevivalDeployment(
before,
deposited(before, group),
'p',
'normal',
group,
),
null,
);
}
});
void test('foreign visitors cannot supply a native threshold or masquerade as the newly returned group', () => {
const before = fixture('fremen', 1);
before.players[1].reserves = 0;
before.homeworlds.custody.visitors['homeworld:fremen'] = {
v: { normal: 20, elite: 0 },
};
const group = { amount: 1, elite: 1, free: 1 };
const after = deposited(before, group);
assert.equal(
quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', group),
null,
);
after.homeworlds.custody.visitors['homeworld:fremen'].v.normal--;
after.players[1].reserves++;
assert.throws(
() => quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', group),
/original typed native reserve deposit/,
);
});
void test('unchanged, mismatched and incorrectly typed deposits reject without changing either snapshot', () => {
const before = fixture('fremen', 3);
const group = { amount: 2, elite: 1, free: 1 };
const edits: ((after: ReturnType<typeof fixture>) => void)[] = [
(after) => {
after.players[0].reserves--;
},
(after) => {
after.players[0].elites!.reserves--;
},
(after) => {
after.players[1].reserves--;
},
(after) => {
after.players[0].faction = 'emperor';
},
(after) => {
after.advanced = false;
},
];
for (const edit of edits) {
const after = deposited(before, group);
edit(after);
const snapshots = structuredClone({ before, after });
assert.throws(() =>
quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', group),
);
assert.deepEqual({ before, after }, snapshots);
}
assert.throws(
() => quoteHomeworldRevivalDeployment(before, before, 'p', 'normal', group),
/original typed native reserve deposit/,
);
assert.throws(() =>
quoteHomeworldRevivalDeployment(
before,
deposited(before, group),
'absent',
'normal',
group,
),
);
});
void test('invalid source and physical group counts reject even when no deployment would be eligible', () => {
const before = fixture('fremen', 3);
const valid = { amount: 2, elite: 1, free: 1 };
const after = deposited(before, valid);
for (const group of [
null,
undefined,
[],
{},
{ ...valid, amount: 0 },
{ ...valid, amount: -1 },
{ ...valid, amount: 21 },
{ ...valid, amount: 0.5 },
{ ...valid, amount: Infinity },
{ ...valid, elite: -1 },
{ ...valid, elite: 3 },
{ ...valid, elite: NaN },
{ ...valid, free: -1 },
{ ...valid, free: 3 },
{ ...valid, free: 0.5 },
]) {
const snapshots = structuredClone({ before, after });
assert.throws(
() =>
quoteHomeworldRevivalDeployment(
before,
after,
'p',
'normal',
group as HomeworldRevivalDeploymentGroup,
),
/exact newly returned/,
);
assert.deepEqual({ before, after }, snapshots);
}
assert.throws(
() =>
quoteHomeworldRevivalDeployment(
before,
after,
'p',
'unknown' as HomeworldRevivalDeploymentSource,
valid,
),
/valid source/,
);
});
void test('absent or uninitialized Homeworlds grant nothing and enabling or deleting custody mid-return rejects', () => {
const before = fixture('fremen', 3);
const group = { amount: 1, elite: 1, free: 1 };
const after = deposited(before, group);
for (const homeworlds of [undefined, null, { custody: null }]) {
assert.equal(
quoteHomeworldRevivalDeployment(
{ ...before, homeworlds },
{ ...after, homeworlds },
'p',
'normal',
group,
),
null,
);
assert.throws(
() =>
quoteHomeworldRevivalDeployment(
before,
{ ...after, homeworlds },
'p',
'normal',
group,
),
/module cannot change/,
);
assert.throws(
() =>
quoteHomeworldRevivalDeployment(
{ ...before, homeworlds },
after,
'p',
'normal',
group,
),
/module cannot change/,
);
}
});
void test('public typed deposit comparison tolerates property and player ordering without reading private state', () => {
const before = fixture('fremen', 3);
const group = { amount: 2, elite: 1, free: 1 };
const after = deposited(before, group);
after.players.reverse();
const expected = quoteHomeworldRevivalDeployment(
before,
after,
'p',
'normal',
group,
);
assert.deepEqual(
quoteHomeworldRevivalDeployment(
JSON.parse(JSON.stringify(before)),
JSON.parse(JSON.stringify(after)),
'p',
'normal',
group,
),
expected,
);
for (const context of [before, after]) {
for (const player of context.players) {
for (const field of [
'spice',
'hand',
'traitors',
'prediction',
'faceDancers',
'forces',
'tanks',
])
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);
}
Object.freeze(group);
assert.deepEqual(
quoteHomeworldRevivalDeployment(before, after, 'p', 'normal', group),
expected,
);
});