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

305 lines
8.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
createRemainingCohort,
validateCohortSelection,
type MovementCohort,
type MovementMarker,
} from '../game/ornithopter';
void test('a multi-sector first group leaves independent ordinary and elite counts, including same-origin residual groups', () => {
const all = { 'shield_wall:7': 4, 'shield_wall:8': 3, 'arrakeen:10': 2 };
const elites = { 'shield_wall:7': 2, 'shield_wall:8': 1 };
const selected = { 'shield_wall:7': 2, 'shield_wall:8': 1 };
const typed = { 'shield_wall:7': 1 };
const before = structuredClone({ all, elites, selected, typed });
const remaining = createRemainingCohort(all, elites, selected, typed);
assert.deepEqual(remaining, {
forces: { 'shield_wall:7': 2, 'shield_wall:8': 2, 'arrakeen:10': 2 },
elites: { 'shield_wall:7': 1, 'shield_wall:8': 1 },
});
assert.doesNotThrow(() =>
validateCohortSelection(
remaining,
remaining.forces,
remaining.elites,
{ 'shield_wall:7': 2, 'shield_wall:8': 1 },
{ 'shield_wall:7': 1, 'shield_wall:8': 1 },
),
);
assert.deepEqual({ all, elites, selected, typed }, before);
});
void test('first-group arrivals cannot enlarge a destination quota or become a second group at an empty original destination', () => {
const remaining = createRemainingCohort(
{ origin: 4, destination: 2 },
{ origin: 1 },
{ origin: 3 },
{ origin: 1 },
);
const current = { origin: 1, destination: 5 },
elites = { destination: 1 };
assert.doesNotThrow(() =>
validateCohortSelection(remaining, current, elites, { destination: 2 }, {}),
);
assert.throws(
() =>
validateCohortSelection(
remaining,
current,
elites,
{ destination: 3 },
{},
),
/unmoved cohort/,
);
assert.throws(
() =>
validateCohortSelection(
remaining,
current,
elites,
{ destination: 1 },
{ destination: 1 },
),
/unmoved cohort/,
);
const empty = createRemainingCohort({ origin: 4 }, {}, { origin: 4 }, {});
assert.throws(
() =>
validateCohortSelection(
empty,
{ destination: 4 },
{},
{ destination: 1 },
{},
),
/unmoved cohort/,
);
});
void test('typed quotas reject ordinary-for-elite substitutions even when total force counts fit', () => {
assert.throws(
() => createRemainingCohort({ a: 3 }, { a: 2 }, { a: 2 }, {}),
/ordinary or elite/,
);
assert.throws(
() => createRemainingCohort({ a: 3 }, { a: 1 }, { a: 2 }, { a: 2 }),
/ordinary or elite/,
);
const cohort = createRemainingCohort({ a: 4 }, { a: 2 }, { a: 1 }, {});
assert.deepEqual(cohort, { forces: { a: 3 }, elites: { a: 2 } });
assert.throws(
() => validateCohortSelection(cohort, { a: 3 }, { a: 2 }, { a: 2 }, {}),
/unmoved cohort/,
);
assert.throws(
() => validateCohortSelection(cohort, { a: 3 }, {}, { a: 2 }, { a: 1 }),
/current physical/,
);
assert.throws(
() => validateCohortSelection(cohort, { a: 2 }, { a: 2 }, { a: 1 }, {}),
/current physical/,
);
});
void test('current losses limit remaining selections without increasing their original allowance', () => {
const cohort = createRemainingCohort({ a: 5, b: 4 }, { b: 2 }, { a: 2 }, {});
assert.throws(
() =>
validateCohortSelection(cohort, { a: 1, b: 4 }, { b: 2 }, { a: 2 }, {}),
/current physical/,
);
assert.doesNotThrow(() =>
validateCohortSelection(
cohort,
{ a: 1, b: 3 },
{ b: 1 },
{ a: 1, b: 3 },
{ b: 1 },
),
);
});
void test('marker selection binds exact identity, event and origin and cannot be reused with a new event', () => {
const marker = {
tokenId: 'opaque-id',
event: 'before-move',
from: 'shield_wall:8',
};
const unmoved = createRemainingCohort({ a: 1 }, {}, { a: 1 }, {}, marker);
assert.deepEqual(unmoved, { forces: {}, elites: {}, noField: marker });
assert.notEqual(unmoved.noField, marker);
assert.doesNotThrow(() =>
validateCohortSelection(unmoved, {}, {}, {}, {}, marker),
);
for (const changed of [
{ ...marker, event: 'new-event' },
{ ...marker, tokenId: 'another' },
{ ...marker, from: 'arrakeen:10' },
]) {
assert.throws(
() => validateCohortSelection(unmoved, {}, {}, {}, {}, changed),
/not an unmoved/,
);
assert.throws(
() => createRemainingCohort({}, {}, {}, {}, marker, changed),
/not an unmoved/,
);
}
const moved = createRemainingCohort(
{ a: 2 },
{},
{ a: 1 },
{},
marker,
marker,
);
assert.equal(moved.noField, undefined);
for (const selected of [
marker,
{ ...marker, event: 'after-move', from: 'arrakeen:10' },
])
assert.throws(
() => validateCohortSelection(moved, { a: 1 }, {}, {}, {}, selected),
/not an unmoved/,
);
});
void test('marker cohort calculation neither reads nor projects any hidden denomination', () => {
for (const value of [0, 3, 5]) {
const marker = {
tokenId: 'same-opaque',
event: 'same-event',
from: 'polar_sink:0',
value,
};
const expected = {
forces: {},
elites: {},
noField: {
tokenId: marker.tokenId,
event: marker.event,
from: marker.from,
},
};
assert.deepEqual(createRemainingCohort({}, {}, {}, {}, marker), expected);
}
const marker = {
tokenId: 'opaque',
event: 'event',
from: 'polar_sink:0',
get value(): never {
throw new Error('Hidden denomination read');
},
};
const cohort = createRemainingCohort({}, {}, {}, {}, marker);
assert.doesNotThrow(() =>
validateCohortSelection(cohort, {}, {}, {}, {}, marker),
);
assert.equal(JSON.stringify(cohort).includes('value'), false);
});
void test('JSON-restored cohorts validate without mutation and outputs do not alias input records', () => {
const forces = { a: 3, b: 0 },
elites = { a: 1 },
selected = { a: 1 },
selectedElites = {};
const cohort = createRemainingCohort(
forces,
elites,
selected,
selectedElites,
);
const restored: MovementCohort = JSON.parse(JSON.stringify(cohort));
const before = structuredClone(restored);
validateCohortSelection(restored, { a: 2 }, { a: 1 }, { a: 2 }, { a: 1 });
assert.deepEqual(restored, before);
cohort.forces.a = 99;
cohort.elites.a = 99;
assert.deepEqual(forces, { a: 3, b: 0 });
assert.deepEqual(elites, { a: 1 });
assert.deepEqual(selected, { a: 1 });
assert.deepEqual(restored, { forces: { a: 2 }, elites: { a: 1 } });
});
void test('malformed counts, impossible typed pools and restored markers fail before mutation', () => {
for (const count of [
-1,
0.5,
NaN,
Infinity,
Number.MAX_SAFE_INTEGER + 1,
'1',
undefined,
]) {
const bad = { a: count } as Record<string, number>;
assert.throws(() => createRemainingCohort(bad, {}, {}, {}), /safe integer/);
assert.throws(
() => createRemainingCohort({ a: 2 }, {}, bad, {}),
/safe integer/,
);
assert.throws(
() =>
validateCohortSelection({ forces: bad, elites: {} }, {}, {}, {}, {}),
/safe integer/,
);
assert.throws(
() =>
validateCohortSelection({ forces: {}, elites: {} }, bad, {}, {}, {}),
/safe integer/,
);
}
for (const bad of [null, [], 2])
assert.throws(
() =>
createRemainingCohort(
bad as unknown as Record<string, number>,
{},
{},
{},
),
/record/,
);
assert.throws(
() =>
createRemainingCohort({ a: Number.MAX_SAFE_INTEGER, b: 1 }, {}, {}, {}),
/total/,
);
assert.throws(
() => createRemainingCohort({ '': 1 }, {}, {}, {}),
/named locations/,
);
assert.throws(
() => createRemainingCohort({ a: 1 }, { a: 2 }, {}, {}),
/elite count/,
);
assert.throws(
() => createRemainingCohort({ a: 1 }, {}, { a: 1 }, { b: 1 }),
/elite count/,
);
for (const marker of [
null,
{},
{ tokenId: 'id', event: '', from: 'a' },
{ tokenId: 'id', event: 'event', from: 1 },
]) {
const invalid = marker as unknown as MovementMarker;
assert.throws(
() => createRemainingCohort({}, {}, {}, {}, invalid),
/marker/,
);
assert.throws(
() =>
validateCohortSelection(
{ forces: {}, elites: {}, noField: invalid },
{},
{},
{},
{},
),
/marker/,
);
}
});