Files
Dune/tests/richese-no-field.test.ts
T

567 lines
17 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
createRicheseNoField,
validateRicheseNoField,
deployRicheseNoField,
deployRicheseNoFieldPair,
moveRicheseNoField,
revealRicheseNoField,
shipAlliedRicheseNoField,
projectRicheseNoField,
type RicheseNoField,
type NoFieldValue,
type NoFieldRevealCause,
} from '../game/richese-no-field';
const ids = ['opaque-a', 'opaque-b', 'opaque-c'];
const location = { territory: 'imperial_basin', sector: 10 };
const fresh = () => createRicheseNoField(ids);
function deployed(value: NoFieldValue = 5) {
const state = fresh();
const tokenId = state.tokens.find((token) => token.value === value)!.id;
return deployRicheseNoField(state, {
tokenId,
controller: 'richese',
location,
});
}
function reveal(
state: RicheseNoField,
reserves = 20,
cause: NoFieldRevealCause = 'voluntary',
) {
return revealRicheseNoField(state, {
tokenId: state.deployed!.tokenId,
reserves,
cause,
});
}
void test('the model has exactly three distinct physical tokens zero/three/five outside force custody', () => {
const state = fresh();
assert.deepEqual(state.tokens, [
{ id: ids[0], value: 0 },
{ id: ids[1], value: 3 },
{ id: ids[2], value: 5 },
]);
assert.equal(state.deployed, null);
assert.equal(state.lastShipped, null);
assert.deepEqual(projectRicheseNoField(state, false), {
deployed: null,
lastUsed: null,
});
assert.throws(() => createRicheseNoField(['x', 'y']), /three/);
assert.throws(() => createRicheseNoField(['x', 'x', 'z']), /distinct/);
assert.throws(() => createRicheseNoField(['x', '', 'z']), /identities/);
assert.deepEqual(ids, ['opaque-a', 'opaque-b', 'opaque-c']);
});
void test('deserialized inventory, history and deployed references are validated before operations and projection', () => {
const corruptions = [
(s: RicheseNoField) => {
s.tokens.pop();
},
(s: RicheseNoField) => {
s.tokens[1].id = s.tokens[0].id;
},
(s: RicheseNoField) => {
s.tokens[1].value = 0;
},
(s: RicheseNoField) => {
s.tokens[0].value = 9 as NoFieldValue;
},
(s: RicheseNoField) => {
s.lastShipped = 'missing';
},
(s: RicheseNoField) => {
s.lastShipped = ids[0];
},
(s: RicheseNoField) => {
s.deployed!.tokenId = 'missing';
},
(s: RicheseNoField) => {
s.deployed!.controller = '';
},
(s: RicheseNoField) => {
s.deployed!.location.sector = 19;
},
(s: RicheseNoField) => {
delete (s as Partial<RicheseNoField>).deployed;
},
];
for (const corrupt of corruptions) {
const state: RicheseNoField = JSON.parse(JSON.stringify(deployed()));
corrupt(state);
assert.throws(() => validateRicheseNoField(state));
assert.throws(() => projectRicheseNoField(state, false));
assert.throws(() => moveRicheseNoField(state, ids[2], location));
assert.throws(() =>
revealRicheseNoField(state, {
tokenId: ids[2],
reserves: 20,
cause: 'battle',
}),
);
}
});
void test('one deployed marker is enforced even when it represents zero forces', () => {
for (const value of [0, 3, 5] as const) {
const state = deployed(value);
const snapshot = structuredClone(state);
assert.throws(
() =>
deployRicheseNoField(state, {
tokenId: ids[1],
controller: 'ally',
location,
}),
/existing/,
);
assert.throws(
() =>
shipAlliedRicheseNoField(
state,
{ tokenId: ids[1], controller: 'ally', location },
20,
),
/existing/,
);
assert.deepEqual(state, snapshot);
assert.equal(
projectRicheseNoField(state, false).deployed!.effectiveForces,
1,
);
}
});
void test('concealed public views are identical for all denominations and expose neither supply nor physical IDs', () => {
const views = ([0, 3, 5] as const).map((value) =>
projectRicheseNoField(deployed(value), false),
);
assert.deepEqual(views[0], views[1]);
assert.deepEqual(views[1], views[2]);
for (const view of views) {
const json = JSON.stringify(view);
for (const id of ids) assert.equal(json.includes(id), false);
assert.equal('tokens' in view, false);
assert.equal('private' in view, false);
assert.equal(view.lastUsed, null);
}
const own = projectRicheseNoField(deployed(3), true);
assert.equal(
own.private.tokens.find(
(token) => token.id === own.private.deployed!.tokenId,
)!.value,
3,
);
});
void test('moving preserves hidden identity/history and exact sector without materializing reserves', () => {
const state = deployed();
const moved = moveRicheseNoField(state, ids[2], {
territory: 'imperial_basin',
sector: 9,
});
assert.equal(state.deployed!.location.sector, 10);
assert.equal(moved.deployed!.location.sector, 9);
assert.equal(moved.lastShipped, ids[2]);
assert.deepEqual(moved.tokens, state.tokens);
assert.equal(
projectRicheseNoField(moved, false).deployed!.effectiveForces,
1,
);
assert.throws(
() => moveRicheseNoField(state, ids[0], location),
/not deployed/,
);
for (const sector of [0, 19, 1.5, NaN, Infinity])
assert.throws(
() => moveRicheseNoField(state, ids[2], { ...location, sector }),
/sector/,
);
});
void test('every reveal cause returns capped physical materialization and the actual value without allocating casualties', () => {
const causes: NoFieldRevealCause[] = [
'voluntary',
'battle',
'storm',
'worm',
'gamont',
'beforeAlly',
'allyShipment',
'atomics',
'alliedSeparation',
];
for (const cause of causes)
for (const value of [0, 3, 5] as const)
for (const reserves of [0, 1, 2, 3, 4, 5, 20]) {
const state = deployed(value);
const result = reveal(state, reserves, cause);
assert.equal(result.forces, Math.min(value, reserves));
assert.equal(result.value, value);
assert.equal(result.cause, cause);
assert.equal(result.controller, 'richese');
assert.deepEqual(result.location, location);
assert.equal(result.state.deployed, null);
assert.equal(result.state.lastShipped, state.lastShipped);
assert.deepEqual(result.state.tokens, state.tokens);
assert.equal(
projectRicheseNoField(result.state, false).lastUsed,
value,
);
assert.ok(state.deployed);
assert.equal('tanks' in result, false);
}
});
void test('revealing returns component custody but cannot erase the consecutive-use restriction', () => {
let state = reveal(deployed(5), 2, 'storm').state;
assert.equal(projectRicheseNoField(state, false).lastUsed, 5);
assert.throws(
() =>
deployRicheseNoField(state, {
tokenId: ids[2],
controller: 'richese',
location,
}),
/twice/,
);
state = deployRicheseNoField(state, {
tokenId: ids[0],
controller: 'richese',
location,
});
assert.equal(projectRicheseNoField(state, false).lastUsed, null);
state = reveal(state).state;
assert.equal(projectRicheseNoField(state, false).lastUsed, 0);
state = deployRicheseNoField(state, {
tokenId: ids[2],
controller: 'richese',
location,
});
assert.equal(state.lastShipped, ids[2]);
assert.equal(state.tokens.length, 3);
});
void test('allied shipment reveals immediately from the ally reserves and shares the same last-shipped history', () => {
let state = reveal(deployed(0), 20, 'beforeAlly').state;
const result = shipAlliedRicheseNoField(
state,
{ tokenId: ids[2], controller: 'ally', location },
2,
);
assert.equal(result.forces, 2);
assert.equal(result.controller, 'ally');
assert.equal(result.cause, 'allyShipment');
assert.equal(result.state.deployed, null);
assert.equal(result.state.lastShipped, ids[2]);
assert.equal(projectRicheseNoField(result.state, false).lastUsed, 5);
state = result.state;
assert.throws(
() =>
shipAlliedRicheseNoField(
state,
{ tokenId: ids[2], controller: 'different-ally', location },
20,
),
/twice/,
);
assert.throws(
() =>
deployRicheseNoField(state, {
tokenId: ids[2],
controller: 'richese',
location,
}),
/twice/,
);
const zero = shipAlliedRicheseNoField(
state,
{ tokenId: ids[0], controller: 'ally', location },
20,
);
assert.equal(zero.forces, 0);
assert.equal(zero.state.lastShipped, ids[0]);
});
void test('paired shipment retains concealed custody while the other token materializes only available forces', () => {
for (const [concealedId, revealedId, reserves, value, forces] of [
[ids[0], ids[2], 2, 5, 2],
[ids[2], ids[1], 0, 3, 0],
[ids[1], ids[0], 20, 0, 0],
[ids[0], ids[1], 3, 3, 3],
] as const) {
const previous = fresh();
const snapshot = structuredClone(previous);
const result = deployRicheseNoFieldPair(
previous,
{ tokenId: concealedId, controller: 'richese', location },
revealedId,
reserves,
);
assert.deepEqual(previous, snapshot);
assert.deepEqual(result.state.deployed, {
tokenId: concealedId,
controller: 'richese',
location,
});
assert.equal(result.state.lastShipped, concealedId);
assert.deepEqual(
{
tokenId: result.revealed.tokenId,
controller: result.revealed.controller,
location: result.revealed.location,
value: result.revealed.value,
forces: result.revealed.forces,
cause: result.revealed.cause,
},
{
tokenId: revealedId,
controller: 'richese',
location,
value,
forces,
cause: 'voluntary',
},
);
assert.deepEqual(projectRicheseNoField(result.state, false), {
deployed: { controller: 'richese', location, effectiveForces: 1 },
lastUsed: null,
});
}
});
void test('paired shipment consumes neither identity nor history on invalid choices', () => {
const previous = reveal(deployed(0)).state;
const snapshot = JSON.stringify(previous);
const concealed = { tokenId: ids[1], controller: 'richese', location };
for (const [deployment, revealedId, message] of [
[concealed, ids[1], /different physical/],
[concealed, ids[0], /twice/],
[{ ...concealed, tokenId: ids[0] }, ids[2], /twice/],
[concealed, 'unknown', /physical/],
[{ ...concealed, tokenId: 'unknown' }, ids[2], /physical/],
] as const)
assert.throws(
() => deployRicheseNoFieldPair(previous, deployment, revealedId, 5),
message,
);
assert.equal(JSON.stringify(previous), snapshot);
const restored = JSON.parse(snapshot) as RicheseNoField;
const { state, revealed } = deployRicheseNoFieldPair(
restored,
concealed,
ids[2],
1,
);
assert.equal(revealed.forces, 1);
assert.equal(state.lastShipped, ids[1]);
assert.equal(state.deployed?.tokenId, ids[1]);
assert.equal(JSON.stringify(restored), snapshot);
const restoredPair = JSON.parse(JSON.stringify(state)) as RicheseNoField;
assert.deepEqual(
projectRicheseNoField(restoredPair, true),
projectRicheseNoField(state, true),
);
const afterReveal = reveal(restoredPair).state;
assert.equal(afterReveal.lastShipped, ids[1]);
assert.equal(afterReveal.deployed, null);
assert.throws(
() =>
deployRicheseNoField(afterReveal, {
tokenId: ids[1],
controller: 'richese',
location,
}),
/twice/,
);
});
void test('paired shipment rejects occupied custody, malformed reserves and placement without mutation', () => {
const prior = deployed(0);
const priorSnapshot = JSON.stringify(prior);
assert.throws(
() =>
deployRicheseNoFieldPair(
prior,
{ tokenId: ids[1], controller: 'richese', location },
ids[2],
5,
),
/existing/,
);
assert.equal(JSON.stringify(prior), priorSnapshot);
const previous = fresh();
const snapshot = JSON.stringify(previous);
const deployment = { tokenId: ids[1], controller: 'richese', location };
for (const reserves of [-1, NaN, 1.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
assert.throws(
() => deployRicheseNoFieldPair(previous, deployment, ids[2], reserves),
/reserves/,
);
for (const invalid of [
{ ...deployment, controller: ' ' },
{ ...deployment, location: { territory: 'imperial_basin', sector: 0 } },
])
assert.throws(
() => deployRicheseNoFieldPair(previous, invalid, ids[2], 5),
/controller|territory/,
);
assert.equal(JSON.stringify(previous), snapshot);
});
void test('failed allied materialization does not partially consume the selected token or rewrite history', () => {
const state = reveal(deployed(0)).state;
const exact = structuredClone(state);
for (const reserves of [-1, NaN, 2.5, Infinity, Number.MAX_SAFE_INTEGER + 1])
assert.throws(
() =>
shipAlliedRicheseNoField(
state,
{ tokenId: ids[2], controller: 'ally', location },
reserves,
),
/reserves/,
);
assert.deepEqual(state, exact);
});
void test('repeated or wrong-token reveal fails while JSON restoration resumes the same physical marker', () => {
const state = deployed(3);
const saved = JSON.stringify(state);
const loaded = JSON.parse(saved) as RicheseNoField;
assert.throws(
() =>
revealRicheseNoField(loaded, {
tokenId: ids[2],
reserves: 20,
cause: 'battle',
}),
/not deployed/,
);
const result = reveal(loaded, 2, 'battle');
assert.equal(result.tokenId, ids[1]);
assert.equal(result.forces, 2);
assert.throws(
() =>
revealRicheseNoField(result.state, {
tokenId: ids[1],
reserves: 20,
cause: 'battle',
}),
/not deployed/,
);
assert.equal(JSON.stringify(loaded), saved);
assert.equal(JSON.stringify(state), saved);
assert.deepEqual(
projectRicheseNoField(JSON.parse(JSON.stringify(result.state)), true),
projectRicheseNoField(result.state, true),
);
});
void test('outputs and projections are independent clones and caller-provided placement data stays unchanged', () => {
const initial = fresh();
const placement = {
tokenId: ids[2],
controller: 'richese',
location: { ...location },
};
const state = deployRicheseNoField(initial, placement);
placement.location.sector = 2;
assert.equal(state.deployed!.location.sector, 10);
const own = projectRicheseNoField(state, true);
own.private.tokens[0].id = 'changed';
own.deployed!.location.sector = 3;
assert.equal(state.tokens[0].id, ids[0]);
assert.equal(state.deployed!.location.sector, 10);
const result = reveal(state);
result.location.sector = 4;
result.state.tokens.reverse();
assert.equal(state.deployed!.location.sector, 10);
assert.equal(state.tokens[0].value, 0);
assert.equal(initial.lastShipped, null);
});
void test('sparse inventory arrays cannot bypass physical token validation', () => {
const sparseIds: string[] = [];
sparseIds.length = 3;
sparseIds[1] = 'opaque-b';
sparseIds[2] = 'opaque-c';
assert.throws(() => createRicheseNoField(sparseIds), /identities/);
const sparseTokens = fresh();
Reflect.deleteProperty(sparseTokens.tokens, '0');
assert.equal(sparseTokens.tokens.length, 3);
assert.throws(() => validateRicheseNoField(sparseTokens), /identities/);
assert.throws(() => projectRicheseNoField(sparseTokens, false), /identities/);
assert.throws(
() =>
deployRicheseNoField(sparseTokens, {
tokenId: ids[2],
controller: 'richese',
location,
}),
/identities/,
);
const json = JSON.parse(JSON.stringify(sparseTokens));
assert.throws(() => validateRicheseNoField(json), /identities/);
});
void test('Polar Sink uses the board sector-zero address without admitting off-planet containers', () => {
const state = deployRicheseNoField(createRicheseNoField(ids), {
tokenId: ids[1],
controller: 'r',
location: { territory: 'polar_sink', sector: 0 },
});
assert.deepEqual(projectRicheseNoField(state, false).deployed?.location, {
territory: 'polar_sink',
sector: 0,
});
const revealed = revealRicheseNoField(state, {
tokenId: ids[1],
reserves: 20,
cause: 'voluntary',
});
assert.equal(revealed.forces, 3);
assert.deepEqual(revealed.location, { territory: 'polar_sink', sector: 0 });
for (const territory of ['reserves', 'homeworld'])
assert.throws(
() => moveRicheseNoField(state, ids[1], { territory, sector: 0 }),
/sector/,
);
});
void test('a concealed No-Field retains custody while moving into and out of the mobile stronghold sector', () => {
for (const value of [0, 3, 5] as const) {
const initial = deployed(value),
before = structuredClone(initial);
const token = initial.deployed!.tokenId;
const moved = moveRicheseNoField(initial, token, {
territory: 'hidden_mobile_stronghold',
sector: 0,
});
assert.deepEqual(initial, before);
validateRicheseNoField(JSON.parse(JSON.stringify(moved)));
assert.equal(moved.lastShipped, initial.lastShipped);
assert.deepEqual(moved.tokens, initial.tokens);
assert.equal(
projectRicheseNoField(moved, false).deployed?.effectiveForces,
1,
);
assert.deepEqual(moveRicheseNoField(moved, token, location), initial);
const revealed = revealRicheseNoField(moved, {
tokenId: token,
reserves: 20,
cause: 'voluntary',
});
assert.equal(revealed.forces, value);
assert.deepEqual(revealed.location, {
territory: 'hidden_mobile_stronghold',
sector: 0,
});
}
});