Files
Dune/tests/guild-homeworld-shipment.test.ts
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

381 lines
11 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 { MOBILE_LOCATION } from '../game/board';
import {
quoteGuildHomeworldShipment as quote,
type GuildHomeworldShipmentContext,
type GuildHomeworldShipmentIntent,
} from '../game/guild-homeworld-shipment';
import {
HomeworldCustodyError,
homeworldForceGroups,
type HomeworldCustody,
} from '../game/homeworld-custody';
function fixture(): {
context: GuildHomeworldShipmentContext;
custody: HomeworldCustody;
} {
return {
context: {
advanced: true,
storm: 12,
players: [
{
id: 'g',
faction: 'guild',
reserves: 10,
eliteReserves: 0,
ally: null,
},
{
id: 'e',
faction: 'emperor',
reserves: 20,
eliteReserves: 5,
ally: null,
},
{
id: 'a',
faction: 'atreides',
reserves: 20,
eliteReserves: 0,
ally: null,
},
],
board: {
g: {
forces: {
'false_wall_south:4': 3,
'false_wall_south:5': 4,
'arrakeen:10': 1,
},
eliteForces: {},
},
e: { forces: {}, eliteForces: {} },
a: { forces: {}, eliteForces: {} },
},
},
custody: {
visitors: { 'homeworld:emperor': { g: { normal: 2, elite: 0 } } },
salusa: { normal: 0, elite: 5 },
},
};
}
const intent: GuildHomeworldShipmentIntent = {
player: 'g',
destination: 'homeworld:atreides',
sources: {
'false_wall_south:4': { normal: 3, elite: 0 },
'false_wall_south:5': { normal: 2, elite: 0 },
},
};
function reject(
context: GuildHomeworldShipmentContext,
custody: HomeworldCustody,
action: unknown = intent,
) {
const before = structuredClone({ context, custody, action });
assert.throws(
() => quote(context, custody, action as GuildHomeworldShipmentIntent),
HomeworldCustodyError,
);
assert.deepEqual({ context, custody, action }, before);
}
function total(
context: GuildHomeworldShipmentContext,
custody: HomeworldCustody,
) {
return (
homeworldForceGroups(context, custody).reduce(
(sum, w) => sum + (w.forces.g?.normal ?? 0),
0,
) + Object.values(context.board.g.forces).reduce((sum, n) => sum + n, 0)
);
}
void test('Guild combines canonical sectors from one territory and conserves detached physical custody', () => {
const { context, custody } = fixture();
const before = structuredClone({ context, custody, intent });
const result = quote(context, custody, intent);
assert.equal(result.origin, 'false_wall_south');
assert.equal(result.amount, 5);
assert.equal(result.elite, 0);
assert.equal(result.cost, 3);
assert.deepEqual(result.boardSources, [
{
key: 'false_wall_south:4',
before: { normal: 3, elite: 0 },
after: { normal: 0, elite: 0 },
},
{
key: 'false_wall_south:5',
before: { normal: 4, elite: 0 },
after: { normal: 2, elite: 0 },
},
]);
assert.deepEqual(result.boardForces, {
'false_wall_south:5': 2,
'arrakeen:10': 1,
});
assert.deepEqual(result.state.visitors['homeworld:atreides'].g, {
normal: 5,
elite: 0,
});
assert.deepEqual(result.receipts, [
{
homeworld: 'homeworld:atreides',
player: 'g',
before: { normal: 0, elite: 0 },
after: { normal: 5, elite: 0 },
},
]);
assert.equal(
total(
{
...context,
players: result.players.map((p) => ({ ...p, ally: null })),
board: {
...context.board,
g: {
forces: result.boardForces,
eliteForces: result.boardEliteForces,
},
},
},
result.state,
),
total(context, custody),
);
assert.deepEqual({ context, custody, intent }, before);
result.boardSources[0].before.normal = 17;
result.boardForces['arrakeen:10'] = 0;
result.state.visitors['homeworld:emperor'].g.normal = 0;
result.state.salusa!.elite = 0;
result.players[0].reserves = 0;
assert.deepEqual({ context, custody, intent }, before);
});
void test('Junction return increases native reserves while either Emperor world accepts foreign arrivals', () => {
for (const destination of [
'homeworld:guild',
'homeworld:emperor',
'homeworld:emperor:salusa',
]) {
const { context, custody } = fixture();
const result = quote(context, custody, { ...intent, destination });
assert.equal(
result.players[0].reserves,
destination === 'homeworld:guild' ? 15 : 10,
);
assert.deepEqual(result.state.salusa, custody.salusa);
assert.equal(result.state.visitors['homeworld:guild'], undefined);
if (destination !== 'homeworld:guild')
assert.equal(
result.state.visitors[destination].g.normal,
destination === 'homeworld:emperor' ? 7 : 5,
);
}
});
void test('Guild tariff rounds the combined group once from one through twenty physical counters', () => {
for (const amount of [1, 2, 3, 4, 5, 19, 20]) {
const { context, custody } = fixture();
context.players[0].reserves = 20 - amount;
custody.visitors = {};
context.board.g.forces = { 'arrakeen:10': amount };
const result = quote(context, custody, {
...intent,
sources: { 'arrakeen:10': { normal: amount, elite: 0 } },
});
assert.equal(result.amount, amount);
assert.equal(result.cost, Math.ceil(amount / 2));
}
});
void test('Arrakis transport is Guild-only and mutual allies cannot become Homeworld destinations', () => {
const { context, custody } = fixture();
context.players[0].ally = 'e';
context.players[1].ally = 'g';
for (const destination of ['homeworld:emperor', 'homeworld:emperor:salusa'])
reject(context, custody, { ...intent, destination });
reject(context, custody, { ...intent, player: 'e' });
assert.equal(quote(context, custody, intent).amount, 5);
context.players[1].ally = null;
assert.equal(
quote(context, custody, { ...intent, destination: 'homeworld:emperor' })
.amount,
5,
);
});
void test('storm sectors cannot join a group while other sectors in the territory remain legal', () => {
const { context, custody } = fixture();
context.storm = 4;
reject(context, custody);
assert.equal(
quote(context, custody, {
...intent,
sources: { 'false_wall_south:5': { normal: 4, elite: 0 } },
}).amount,
4,
);
reject(context, custody, {
...intent,
sources: {
'false_wall_south:5': { normal: 1, elite: 0 },
'arrakeen:10': { normal: 1, elite: 0 },
},
});
});
void test('mobile stronghold interior requires its active pointer and stays storm protected', () => {
const { context, custody } = fixture();
context.board.g.forces = { [MOBILE_LOCATION]: 8 };
const action = {
...intent,
sources: { [MOBILE_LOCATION]: { normal: 5, elite: 0 } },
};
reject(context, custody, action);
context.mobileStronghold = 'false_wall_south:4';
context.storm = 4;
assert.equal(quote(context, custody, action).amount, 5);
for (const pointer of [
MOBILE_LOCATION,
'false_wall_south:04',
'homeworld:guild',
'false_wall_south:99',
'',
]) {
context.mobileStronghold = pointer;
reject(context, custody, action);
}
});
void test('malformed sources, alias sectors and unavailable force identities reject without mutation', () => {
const { context, custody } = fixture();
for (const action of [
null,
[],
{},
{ ...intent, amount: 5 },
{ ...intent, sources: {} },
{ ...intent, sources: [] },
{ ...intent, player: 'absent' },
{ ...intent, destination: 'homeworld:harkonnen' },
...[
'false_wall_south:04',
'false_wall_south:4:extra',
'false_wall_south:4.0',
'false_wall_south:99',
'homeworld:guild',
'false_wall_south',
].map((key) => ({
...intent,
sources: { [key]: { normal: 1, elite: 0 } },
})),
...[
null,
{},
{ normal: 0, elite: 0 },
{ normal: 1 },
{ normal: 1, elite: 0, extra: 0 },
{ normal: '1', elite: 0 },
{ normal: NaN, elite: 0 },
{ normal: Infinity, elite: 0 },
{ normal: 0.5, elite: 0 },
{ normal: -1, elite: 0 },
{ normal: 21, elite: 0 },
{ normal: 4, elite: 0 },
{ normal: 1, elite: 1 },
].map((selected) => ({
...intent,
sources: { 'false_wall_south:4': selected },
})),
])
reject(context, custody, action);
});
void test('corrupt board, reserve and visitor counters cannot be laundered into a valid destination', () => {
for (const mutate of [
(c: GuildHomeworldShipmentContext) => {
c.board.g.forces['arrakeen:10']++;
},
(c: GuildHomeworldShipmentContext) => {
c.board.g.forces['arrakeen:010'] = 0;
},
(c: GuildHomeworldShipmentContext) => {
c.board.g.forces['arrakeen:10'] = -1;
},
(c: GuildHomeworldShipmentContext) => {
c.board.g.eliteForces['false_wall_south:4'] = 1;
},
(c: GuildHomeworldShipmentContext) => {
c.players[0].ally = 'absent';
},
(c: GuildHomeworldShipmentContext) => {
c.storm = 19;
},
(_c: GuildHomeworldShipmentContext, s: HomeworldCustody) => {
s.visitors['homeworld:emperor'].e = { normal: 1, elite: 0 };
},
(_c: GuildHomeworldShipmentContext, s: HomeworldCustody) => {
s.salusa!.elite = 6;
},
]) {
const { context, custody } = fixture();
mutate(context, custody);
reject(context, custody);
}
});
void test('JSON restoration preserves a second partial shipment and exact custody', () => {
const { context, custody } = fixture();
const result = JSON.parse(
JSON.stringify(quote(context, custody, intent)),
) as ReturnType<typeof quote>;
const next = {
...context,
players: result.players.map((p) => ({ ...p, ally: null })),
board: {
...context.board,
g: { forces: result.boardForces, eliteForces: result.boardEliteForces },
},
};
const second = quote(next, result.state, {
...intent,
destination: 'homeworld:guild',
sources: { 'false_wall_south:5': { normal: 2, elite: 0 } },
});
assert.equal(second.players[0].reserves, 12);
assert.deepEqual(second.boardForces, { 'arrakeen:10': 1 });
assert.equal(second.state.visitors['homeworld:atreides'].g.normal, 5);
});
void test('quote does not inspect secret cards, resources, phase usage or other players’ hidden board values', () => {
const { context, custody } = fixture();
const expected = quote(context, custody, intent);
for (const object of [context, ...context.players, context.board.e])
for (const key of [
'hand',
'spice',
'rng',
'shipped',
'noField',
'payment',
'response',
])
Object.defineProperty(object, key, {
enumerable: true,
get() {
throw new Error('Caller-owned private field read.');
},
});
Object.defineProperty(context.board.e, 'forces', {
get() {
throw new Error('Unrelated board read.');
},
});
assert.deepEqual(quote(context, custody, intent), expected);
});