Files
Dune/tests/nexus-richese.test.ts
T

268 lines
7.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { FACTIONS, type FactionId } from '../game/catalog';
import {
quoteNexusRicheseShipment,
createNexusRichese,
validateNexusRichese,
type NexusRicheseContext,
type NexusRicheseReceipt,
} from '../game/nexus-richese';
const context: NexusRicheseContext = {
turn: 3,
players: [
{ id: 'p', faction: 'atreides' },
{ id: 'q', faction: 'guild' },
{ id: 'r', faction: 'fremen' },
],
};
const clone = <T>(x: T): T => JSON.parse(JSON.stringify(x));
void test('Richese Secret Ally prices one while retaining one through five actual physical forces for every supported faction and terrain', () => {
for (const faction of FACTIONS.map((f) => f.id).filter(
(f) => 'richese' !== f,
))
for (const type of ['stronghold', 'sand', 'rock', 'polar'])
for (let amount = 1; amount <= 5; amount++) {
const player = { faction, halfRate: faction === 'guild' },
before = clone(player);
const q = quoteNexusRicheseShipment(player, type, amount);
assert.deepEqual(q, {
physicalAmount: amount,
pricedAmount: 1,
cost:
faction === 'fremen'
? 0
: faction === 'guild' || type === 'stronghold'
? 1
: 2,
source: 'nexusRichese',
});
assert.deepEqual(player, before);
}
});
void test('an existing Karama half rate uses the one-force basis; suppressed Guild half rate and native Fremen free transport remain distinct', () => {
assert.equal(
quoteNexusRicheseShipment(
{ faction: 'emperor', halfRate: false },
'sand',
5,
).cost,
2,
);
assert.equal(
quoteNexusRicheseShipment({ faction: 'emperor', halfRate: true }, 'sand', 5)
.cost,
1,
);
assert.equal(
quoteNexusRicheseShipment({ faction: 'guild', halfRate: false }, 'rock', 5)
.cost,
2,
);
assert.equal(
quoteNexusRicheseShipment(
{ faction: 'guild', halfRate: true },
'stronghold',
5,
).cost,
1,
);
for (const halfRate of [false, true])
assert.equal(
quoteNexusRicheseShipment({ faction: 'fremen', halfRate }, 'sand', 5)
.cost,
0,
);
});
void test('quote refuses empty, fractional, excess or malformed physical groups and non-Arrakis price domains without mutating inputs', () => {
const player = Object.freeze({
faction: 'emperor' as const,
halfRate: false,
});
for (const amount of [
0,
-1,
6,
1.5,
NaN,
Infinity,
Number.MAX_SAFE_INTEGER,
'3',
null,
undefined,
])
assert.throws(() =>
quoteNexusRicheseShipment(player, 'sand', amount as number),
);
for (const type of ['homeworld', 'reserves', 'unknown', ''])
assert.throws(() => quoteNexusRicheseShipment(player, type, 3));
assert.throws(() =>
quoteNexusRicheseShipment(
{ faction: 'richese', halfRate: false },
'sand',
3,
),
);
assert.throws(() =>
quoteNexusRicheseShipment(
{ faction: 'unknown' as FactionId, halfRate: false },
'sand',
3,
),
);
assert.throws(() =>
quoteNexusRicheseShipment(
{ faction: 'emperor', halfRate: 'yes' as unknown as boolean },
'sand',
3,
),
);
});
void test('Richese receipt is bound to its real absent-faction owner and turn while canonicalizing public seat order', () => {
for (const p of context.players) {
const r = createNexusRichese(context, p.id);
assert.equal(r.event, JSON.stringify(['nexusRichese', 3, p.id]));
assert.equal(r.phase, 5);
assert.equal(r.mode, 'secretAlly');
validateNexusRichese(context, clone(r));
assert.deepEqual(
createNexusRichese(
{ ...context, players: [...context.players].reverse() },
p.id,
),
r,
);
assert.notEqual(
createNexusRichese({ ...context, turn: 4 }, p.id).event,
r.event,
);
}
const copied = clone(context),
r = createNexusRichese(copied, 'p');
copied.players[0].id = 'changed';
assert.equal(r.roster[0].id, 'p');
assert.throws(() => createNexusRichese(context, 'foreign'));
assert.throws(() =>
createNexusRichese(
{
...context,
players: [...context.players, { id: 's', faction: 'richese' }],
},
'p',
),
);
});
void test('historical Richese receipt survives later turns and alliances without reading hands, funds, No-Field values or other secrets', () => {
const r = createNexusRichese(context, 'p'),
before = clone(r);
const fail = () => {
throw new Error('Private field read');
};
const players = context.players.map((p) =>
Object.defineProperties(
{ ...p },
{
hand: { enumerable: true, get: fail },
spice: { enumerable: true, get: fail },
ally: { enumerable: true, get: fail },
noField: { enumerable: true, get: fail },
traitors: { enumerable: true, get: fail },
},
),
);
const later = Object.defineProperties(
{ turn: 5, players },
{ nexusCards: { get: fail }, response: { get: fail } },
);
validateNexusRichese(later, r);
createNexusRichese(later, 'q');
assert.deepEqual(r, before);
const pricePlayer = Object.defineProperties(
{ faction: 'fremen' as const, halfRate: false },
{ spice: { get: fail }, reserves: { get: fail }, noField: { get: fail } },
);
assert.equal(
quoteNexusRicheseShipment(pricePlayer, 'sand', 5).physicalAmount,
5,
);
});
void test('Richese historical validation rejects changed evidence, deleted fields and malformed roster without rewriting it', () => {
const original = createNexusRichese(context, 'p');
const edits: ((r: NexusRicheseReceipt) => void)[] = [
(r) => {
r.owner = 'q';
},
(r) => {
r.turn++;
},
(r) => {
r.event = 'old';
},
(r) => {
r.phase = 4 as 5;
},
(r) => {
r.mode = 'cunning' as 'secretAlly';
},
(r) => {
r.signature = 'changed';
},
(r) => {
r.roster.reverse();
},
(r) => {
r.roster[0].faction = 'emperor';
},
(r) => {
r.roster[0].id = 'other';
},
(r) => {
r.roster.pop();
},
(r) => {
Object.assign(r, { hiddenHand: [] });
},
(r) => {
Reflect.deleteProperty(r, 'version');
},
(r) => {
Reflect.deleteProperty(r, 'signature');
},
(r) => {
Object.assign(r.roster[0], { extra: true });
},
];
for (const edit of edits) {
const r = clone(original);
edit(r);
const before = clone(r);
assert.throws(() => validateNexusRichese(context, r));
assert.deepEqual(r, before);
}
assert.throws(() => validateNexusRichese({ ...context, turn: 2 }, original));
for (const id of ['__proto__', 'prototype', 'constructor', ''])
assert.throws(() =>
createNexusRichese({ turn: 1, players: [{ id, faction: 'guild' }] }, id),
);
for (const invalid of [
{ ...context, turn: 0 },
{ ...context, turn: 1.5 },
{ ...context, players: [] },
{ ...context, players: [context.players[0], context.players[0]] },
{
...context,
players: [
{ id: 'p', faction: 'atreides' },
{ id: 'q', faction: 'atreides' },
],
},
] as NexusRicheseContext[])
assert.throws(() => createNexusRichese(invalid, 'p'));
const proto = Object.assign(
Object.create({ extra: true }) as NexusRicheseReceipt,
original,
);
assert.throws(() => validateNexusRichese(context, proto));
});