287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
forceCountCounterError,
|
|
forceCountFactMatches,
|
|
forceCountFactText,
|
|
parseForceCountFact,
|
|
type ForceCountFact,
|
|
type ForceCountPlayer,
|
|
} from '../game/truthtrance-force-count';
|
|
|
|
const locations = [
|
|
{ territory: 'arrakeen', sector: 10 },
|
|
{ territory: 'imperial_basin', sector: 9 },
|
|
] as const;
|
|
|
|
function fact(
|
|
zone: ForceCountFact['zone'],
|
|
counter: ForceCountFact['counter'] = 'total',
|
|
compare: ForceCountFact['compare'] = 'eq',
|
|
value = 0,
|
|
): ForceCountFact {
|
|
return { kind: 'forceCount', zone, counter, compare, value };
|
|
}
|
|
|
|
void test('parser accepts exact live pools and rejects extra, mutually exclusive, unknown, and unsafe fields immutably', () => {
|
|
const input = {
|
|
kind: 'forceCount',
|
|
zone: { kind: 'location', territory: 'arrakeen', sector: 10 },
|
|
counter: 'elite',
|
|
compare: 'gte',
|
|
value: 2,
|
|
};
|
|
const before = structuredClone(input);
|
|
assert.deepEqual(parseForceCountFact(input, locations), input);
|
|
assert.deepEqual(input, before);
|
|
for (const zone of [
|
|
{ kind: 'reserves' },
|
|
{ kind: 'tanks' },
|
|
{ kind: 'board' },
|
|
{ kind: 'territory', territory: 'imperial_basin' },
|
|
{ kind: 'location', territory: 'imperial_basin', sector: 9 },
|
|
])
|
|
assert.deepEqual(
|
|
parseForceCountFact({ ...input, zone }, locations).zone,
|
|
zone,
|
|
);
|
|
|
|
for (const malformed of [
|
|
{ ...input, privateHint: true },
|
|
{ ...input, zone: { kind: 'reserves', territory: 'arrakeen' } },
|
|
{ ...input, zone: { kind: 'board', territory: 'arrakeen' } },
|
|
{ ...input, zone: { kind: 'territory', territory: 'unknown' } },
|
|
{ ...input, zone: { kind: 'territory', territory: 'arrakeen', sector: 10 } },
|
|
{
|
|
...input,
|
|
zone: {
|
|
kind: 'location',
|
|
territory: 'arrakeen',
|
|
sector: 10,
|
|
pool: 'reserves',
|
|
},
|
|
},
|
|
{ ...input, zone: { kind: 'location', territory: 'arrakeen', sector: 9 } },
|
|
{ ...input, zone: { kind: 'location', territory: 'unknown', sector: 10 } },
|
|
{
|
|
...input,
|
|
zone: { kind: 'location', territory: 'arrakeen', sector: 10.5 },
|
|
},
|
|
{ ...input, zone: { kind: 'homeworld', territory: 'caladan', sector: 0 } },
|
|
])
|
|
assert.throws(() => parseForceCountFact(malformed, locations));
|
|
|
|
for (const counter of ['special', '', 1, null, undefined])
|
|
assert.throws(() => parseForceCountFact({ ...input, counter }, locations));
|
|
for (const compare of ['gt', '=', '', 1, null, undefined])
|
|
assert.throws(() => parseForceCountFact({ ...input, compare }, locations));
|
|
for (const value of [
|
|
-1,
|
|
0.5,
|
|
NaN,
|
|
Infinity,
|
|
Number.MAX_SAFE_INTEGER + 1,
|
|
'2',
|
|
null,
|
|
])
|
|
assert.throws(() => parseForceCountFact({ ...input, value }, locations));
|
|
});
|
|
|
|
void test('board pool sums only the supplied active locations and preserves typed counters', () => {
|
|
const p: ForceCountPlayer = { faction: 'ixians', reserves: 6, tanks: 2,
|
|
forces: { 'arrakeen:10': 4, 'imperial_basin:9': 3, 'hidden_mobile_stronghold:0': 6, 'cistern:0': 5, 'invalid:0': 10 },
|
|
elites: { reserves: 0, tanks: 0, forces: { 'arrakeen:10': 1, 'imperial_basin:9': 2, 'hidden_mobile_stronghold:0': 3 } } };
|
|
const zone = { kind: 'board' } as const;
|
|
for (const [counter, n] of [['total', 7], ['normal', 4], ['elite', 3]] as const)
|
|
assert.equal(forceCountFactMatches(p, fact(zone, counter, 'eq', n), locations), true);
|
|
const withHms = [...locations, { territory: 'hidden_mobile_stronghold', sector: 0 }];
|
|
assert.equal(forceCountFactMatches(p, fact(zone, 'total', 'eq', 13), withHms), true);
|
|
assert.equal(forceCountFactMatches(p, fact(zone, 'elite', 'eq', 6), withHms), true);
|
|
assert.throws(() => forceCountFactMatches(p, fact(zone)), /board locations/);
|
|
assert.throws(() => forceCountFactMatches(p, fact(zone), []), /board locations/);
|
|
assert.throws(() => forceCountFactMatches(p, fact(zone), [...locations, locations[0]]), /duplicate/);
|
|
assert.throws(() => forceCountFactMatches(p, fact(zone), [{ territory: 'arrakeen', sector: 999 }]), /malformed/);
|
|
assert.equal(forceCountFactText(fact(zone, 'total', 'eq', 7)),
|
|
'you currently have exactly 7 physical forces on the current Arrakis board');
|
|
});
|
|
|
|
void test('territory-wide facts sum physical sectors and their typed subsets without counting nearby or interior territories', () => {
|
|
const player: ForceCountPlayer = {
|
|
faction: 'ixians', reserves: 9, tanks: 0,
|
|
forces: { 'wind_pass:14': 4, 'wind_pass:15': 3, 'imperial_basin:9': 5, 'hidden_mobile_stronghold:0': 2 },
|
|
elites: { reserves: 1, tanks: 0, forces: { 'wind_pass:14': 1, 'wind_pass:15': 2 } },
|
|
};
|
|
const zone = { kind: 'territory', territory: 'wind_pass' } as const;
|
|
for (const [counter, value] of [['total', 7], ['normal', 4], ['elite', 3]] as const) {
|
|
assert.equal(forceCountFactMatches(player, fact(zone, counter, 'eq', value)), true);
|
|
assert.equal(forceCountFactMatches(player, fact(zone, counter, 'gte', value + 1)), false);
|
|
}
|
|
assert.equal(forceCountFactText(fact(zone, 'total', 'eq', 7)),
|
|
'you currently have exactly 7 physical forces in Wind Pass across all sectors');
|
|
assert.throws(() => forceCountFactMatches({ ...player,
|
|
elites: { ...player.elites!, forces: { 'wind_pass:14': 5 } } }, fact(zone)), /exceeds/);
|
|
assert.throws(() => forceCountFactMatches({ ...player,
|
|
forces: { ...player.forces, 'wind_pass:14': null } } as unknown as ForceCountPlayer, fact(zone)), /nonnegative/);
|
|
assert.throws(() => forceCountFactMatches({ ...player,
|
|
elites: { ...player.elites!, forces: { 'wind_pass:15': null } } } as unknown as ForceCountPlayer, fact(zone)), /nonnegative/);
|
|
});
|
|
|
|
void test('current totals include elites while normal counts subtract the tracked physical subset in every pool', () => {
|
|
const player: ForceCountPlayer = {
|
|
faction: 'ixians',
|
|
reserves: 10,
|
|
tanks: 4,
|
|
forces: { 'arrakeen:10': 6 },
|
|
elites: {
|
|
reserves: 3,
|
|
tanks: 1,
|
|
forces: { 'arrakeen:10': 2 },
|
|
},
|
|
};
|
|
const before = structuredClone(player);
|
|
for (const [zone, totals] of [
|
|
[{ kind: 'reserves' }, [10, 7, 3]],
|
|
[{ kind: 'tanks' }, [4, 3, 1]],
|
|
[{ kind: 'location', territory: 'arrakeen', sector: 10 }, [6, 4, 2]],
|
|
[{ kind: 'location', territory: 'imperial_basin', sector: 9 }, [0, 0, 0]],
|
|
] as const)
|
|
for (const [index, counter] of (
|
|
['total', 'normal', 'elite'] as const
|
|
).entries()) {
|
|
const value = totals[index];
|
|
assert.equal(
|
|
forceCountFactMatches(player, fact(zone, counter, 'eq', value)),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
forceCountFactMatches(player, fact(zone, counter, 'gte', value)),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
forceCountFactMatches(player, fact(zone, counter, 'lte', value)),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
forceCountFactMatches(player, fact(zone, counter, 'gte', value + 1)),
|
|
false,
|
|
);
|
|
}
|
|
assert.deepEqual(player, before);
|
|
});
|
|
|
|
void test('ordinary factions need no fabricated elite ledger, while split factions fail closed for denomination questions', () => {
|
|
const ordinary: ForceCountPlayer = {
|
|
faction: 'atreides',
|
|
reserves: 8,
|
|
tanks: 2,
|
|
forces: { 'arrakeen:10': 5 },
|
|
};
|
|
assert.equal(forceCountCounterError(ordinary, 'normal'), null);
|
|
assert.equal(
|
|
forceCountFactMatches(
|
|
ordinary,
|
|
fact({ kind: 'reserves' }, 'normal', 'eq', 8),
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
forceCountFactMatches(
|
|
ordinary,
|
|
fact({ kind: 'reserves' }, 'elite', 'eq', 0),
|
|
),
|
|
true,
|
|
);
|
|
|
|
const untracked: ForceCountPlayer = { ...ordinary, faction: 'emperor' };
|
|
assert.equal(forceCountCounterError(untracked, 'total'), null);
|
|
assert.match(forceCountCounterError(untracked, 'elite')!, /unavailable/);
|
|
assert.equal(
|
|
forceCountFactMatches(
|
|
untracked,
|
|
fact({ kind: 'reserves' }, 'total', 'eq', 8),
|
|
),
|
|
true,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
forceCountFactMatches(
|
|
untracked,
|
|
fact({ kind: 'reserves' }, 'normal', 'eq', 8),
|
|
),
|
|
/unavailable/,
|
|
);
|
|
});
|
|
|
|
void test('matcher validates only the queried pool and rejects malformed selected totals or elite subsets', () => {
|
|
const base: ForceCountPlayer = {
|
|
faction: 'ixians',
|
|
reserves: 7,
|
|
tanks: 2,
|
|
forces: { 'arrakeen:10': 4 },
|
|
elites: { reserves: 2, tanks: 1, forces: { 'arrakeen:10': 1 } },
|
|
};
|
|
const unrelated = {
|
|
...structuredClone(base),
|
|
tanks: -3,
|
|
forces: null,
|
|
} as unknown as ForceCountPlayer;
|
|
assert.equal(
|
|
forceCountFactMatches(
|
|
unrelated,
|
|
fact({ kind: 'reserves' }, 'normal', 'eq', 5),
|
|
),
|
|
true,
|
|
);
|
|
|
|
for (const reserves of [-1, 1.5, NaN, Number.MAX_SAFE_INTEGER + 1])
|
|
assert.throws(() =>
|
|
forceCountFactMatches({ ...base, reserves }, fact({ kind: 'reserves' })),
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
forceCountFactMatches(
|
|
{ ...base, elites: { ...base.elites!, reserves: 8 } },
|
|
fact({ kind: 'reserves' }, 'total', 'eq', 7),
|
|
),
|
|
/exceeds/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
forceCountFactMatches(
|
|
{ ...base, elites: { ...base.elites!, forces: { 'arrakeen:10': 5 } } },
|
|
fact({ kind: 'location', territory: 'arrakeen', sector: 10 }, 'elite'),
|
|
),
|
|
/exceeds/,
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
forceCountFactMatches(
|
|
{ ...base, elites: { ...base.elites!, tanks: -1 } },
|
|
fact({ kind: 'tanks' }, 'total'),
|
|
),
|
|
/nonnegative/,
|
|
);
|
|
});
|
|
|
|
void test('question text uses printed territory display and states current pool, denomination, and comparison', () => {
|
|
assert.equal(
|
|
forceCountFactText(fact({ kind: 'reserves' }, 'total', 'eq', 1)),
|
|
'you currently have exactly 1 physical force in your reserves',
|
|
);
|
|
assert.equal(
|
|
forceCountFactText(fact({ kind: 'tanks' }, 'normal', 'lte', 2)),
|
|
'you currently have at most 2 normal forces in the Tleilaxu Tanks',
|
|
);
|
|
assert.equal(
|
|
forceCountFactText(
|
|
fact(
|
|
{ kind: 'location', territory: 'arrakeen', sector: 10 },
|
|
'elite',
|
|
'gte',
|
|
3,
|
|
),
|
|
),
|
|
'you currently have at least 3 elite forces in Arrakeen, sector 10',
|
|
);
|
|
});
|