Files
Dune/tests/homeworld-shipment.test.ts
T
Quintine 89569e92a1
Verify and publish checkpoint container / container (push) Successful in 11m10s
Support Advanced native Homeworld return
2026-10-29 04:48:47 +11:00

424 lines
13 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
quoteHomeworldShipment as quote,
type HomeworldShipmentContext,
type HomeworldShipmentIntent,
} from '../game/homeworld-shipment';
import {
HomeworldCustodyError,
homeworldForceGroups,
type HomeworldCustody,
} from '../game/homeworld-custody';
function fixture(): {
context: HomeworldShipmentContext;
custody: HomeworldCustody;
} {
return {
context: {
advanced: true,
players: [
{
id: 'e',
faction: 'emperor',
reserves: 15,
eliteReserves: 4,
ally: null,
},
{
id: 'a',
faction: 'atreides',
reserves: 17,
eliteReserves: 0,
ally: null,
},
{
id: 'g',
faction: 'guild',
reserves: 20,
eliteReserves: 0,
ally: null,
},
{
id: 'f',
faction: 'fremen',
reserves: 20,
eliteReserves: 3,
ally: null,
},
],
},
custody: {
salusa: { normal: 2, elite: 3 },
visitors: {
'homeworld:atreides': { e: { normal: 4, elite: 1 } },
'homeworld:emperor': { a: { normal: 3, elite: 0 } },
},
},
};
}
const invasion: HomeworldShipmentIntent = {
player: 'e',
destination: 'homeworld:guild',
sources: { 'homeworld:emperor': { normal: 3, elite: 1 } },
};
function rejected(
context: HomeworldShipmentContext,
custody: HomeworldCustody,
intent: unknown = invasion,
) {
const before = structuredClone({ context, custody, intent });
assert.throws(
() => quote(context, custody, intent as HomeworldShipmentIntent),
HomeworldCustodyError,
);
assert.deepEqual({ context, custody, intent }, before);
}
function totals(context: HomeworldShipmentContext, custody: HomeworldCustody) {
const groups = homeworldForceGroups(context, custody);
return context.players.map((p) => ({
player: p.id,
normal: groups.reduce((n, w) => n + (w.forces[p.id]?.normal ?? 0), 0),
elite: groups.reduce((n, w) => n + (w.forces[p.id]?.elite ?? 0), 0),
}));
}
void test('native invasion preserves typed global custody and yields detached source and destination receipts', () => {
const { context, custody } = fixture();
const before = structuredClone({ context, custody, invasion });
const result = quote(context, custody, invasion);
assert.equal(result.amount, 4);
assert.equal(result.elite, 1);
assert.equal(result.cost, 4);
assert.deepEqual(result.sources, [
{
homeworld: 'homeworld:emperor',
player: 'e',
before: { normal: 9, elite: 1 },
after: { normal: 6, elite: 0 },
},
]);
assert.equal(result.receipts.length, 2);
assert.deepEqual(result.state.visitors['homeworld:guild'].e, {
normal: 3,
elite: 1,
});
assert.deepEqual(result.state.salusa, custody.salusa);
assert.equal(result.players[0].reserves, 11);
assert.equal(result.players[0].eliteReserves, 3);
const next = {
...context,
players: result.players.map((p) => ({ ...p, ally: null })),
};
assert.deepEqual(totals(next, result.state), totals(context, custody));
assert.deepEqual({ context, custody, invasion }, before);
result.sources[0].before.normal = 0;
result.state.salusa!.normal = 0;
result.state.visitors['homeworld:atreides'].e.normal = 0;
result.players[0].reserves = 0;
assert.deepEqual({ context, custody, invasion }, before);
});
void test('Guild may return an exact foreign garrison to its native world at the rounded half rate', () => {
const { context, custody } = fixture();
const outward = quote(context, custody, {
player: 'g',
destination: 'homeworld:atreides',
sources: { 'homeworld:guild': { normal: 5, elite: 0 } },
});
const next = {
...context,
players: outward.players.map((p) => ({ ...p, ally: null })),
};
const before = structuredClone({ next, custody: outward.state });
const returned = quote(next, outward.state, {
player: 'g',
destination: 'homeworld:guild',
sources: { 'homeworld:atreides': { normal: 5, elite: 0 } },
});
assert.equal(returned.amount, 5);
assert.equal(returned.cost, 3);
assert.equal(returned.players[2].reserves, 20);
assert.deepEqual(returned.state, custody);
assert.deepEqual(returned.sources[0], {
homeworld: 'homeworld:atreides',
player: 'g',
before: { normal: 5, elite: 0 },
after: { normal: 0, elite: 0 },
});
assert.deepEqual({ next, custody: outward.state }, before);
rejected(context, custody, {
player: 'g',
destination: 'homeworld:guild',
sources: { 'homeworld:guild': { normal: 1, elite: 0 } },
});
});
void test('Guild rounds the half rate once for odd and even counts including one and twenty', () => {
for (const amount of [1, 2, 3, 4, 5, 19, 20]) {
const { context, custody } = fixture();
const result = quote(context, custody, {
player: 'g',
destination: 'homeworld:atreides',
sources: { 'homeworld:guild': { normal: amount, elite: 0 } },
});
assert.equal(result.cost, Math.ceil(amount / 2));
assert.equal(result.amount, amount);
}
});
void test('Fremen invasion pays for normal and special counters; Guild alliance does not grant invasion discounts', () => {
const { context, custody } = fixture();
context.players[2].ally = 'f';
context.players[3].ally = 'g';
const result = quote(context, custody, {
player: 'f',
destination: 'homeworld:atreides',
sources: { 'homeworld:fremen': { normal: 2, elite: 3 } },
});
assert.equal(result.cost, 5);
assert.equal(result.amount, 5);
assert.equal(result.elite, 3);
assert.equal(result.players[3].reserves, 15);
assert.equal(result.players[3].eliteReserves, 0);
});
void test('foreign source departure moves only that explicit garrison and survives JSON restoration', () => {
const { context, custody } = fixture();
const result = quote(context, custody, {
player: 'e',
destination: 'homeworld:guild',
sources: { 'homeworld:atreides': { normal: 4, elite: 1 } },
});
assert.equal(result.cost, 5);
assert.equal(result.state.visitors['homeworld:atreides'], undefined);
assert.deepEqual(
result.players,
context.players.map(({ ally: _ally, ...p }) => p),
);
const restored = JSON.parse(JSON.stringify(result)) as typeof result;
const second = quote(
{
...context,
players: restored.players.map((p) => ({ ...p, ally: null })),
},
restored.state,
{
player: 'e',
destination: 'homeworld:atreides',
sources: { 'homeworld:guild': { normal: 4, elite: 1 } },
},
);
assert.deepEqual(second.state, custody);
assert.deepEqual(second.players, result.players);
rejected(context, custody, {
player: 'e',
destination: 'homeworld:guild',
sources: { 'homeworld:atreides': { normal: 5, elite: 0 } },
});
});
void test('Advanced Emperor may combine its two native origins and retains precise Salusa allocation', () => {
const { context, custody } = fixture();
const result = quote(context, custody, {
...invasion,
sources: {
'homeworld:emperor': { normal: 3, elite: 1 },
'homeworld:emperor:salusa': { normal: 2, elite: 2 },
},
});
assert.equal(result.amount, 8);
assert.equal(result.elite, 3);
assert.equal(result.cost, 8);
assert.equal(result.sources.length, 2);
assert.deepEqual(result.state.salusa, { normal: 0, elite: 1 });
assert.deepEqual(result.state.visitors['homeworld:guild'].e, {
normal: 5,
elite: 3,
});
assert.equal(result.players[0].reserves, 7);
assert.equal(result.players[0].eliteReserves, 1);
assert.deepEqual(
result.sources.map((r) => r.before),
[
{ normal: 9, elite: 1 },
{ normal: 2, elite: 3 },
],
);
});
void test('Advanced may return an actual foreign garrison to either native Emperor World at the normal rate', () => {
const { context, custody } = fixture();
for (const destination of ['homeworld:emperor', 'homeworld:emperor:salusa']) {
const before = structuredClone({ context, custody });
const result = quote(context, custody, {
player: 'e', destination,
sources: { 'homeworld:atreides': { normal: 4, elite: 1 } },
});
assert.equal(result.amount, 5);
assert.equal(result.cost, 5);
assert.equal(result.players[0].reserves, 20);
assert.equal(result.players[0].eliteReserves, 5);
assert.equal(result.state.visitors['homeworld:atreides'], undefined);
const next = { ...context, players: result.players.map(p => ({ ...p, ally: null })) };
assert.deepEqual(totals(next, result.state), totals(context, custody));
assert.deepEqual({ context, custody }, before);
}
});
void test('Basic preserves the native return restriction and all modes reject a mutual ally destination', () => {
const { context, custody } = fixture();
const basic = { ...context, advanced: false };
const basicCustody = { ...custody, salusa: null };
rejected(basic, basicCustody, {
...invasion,
destination: 'homeworld:emperor',
sources: { 'homeworld:atreides': { normal: 1, elite: 0 } },
});
context.players[0].ally = 'a';
context.players[1].ally = 'e';
rejected(context, custody, {
...invasion,
destination: 'homeworld:atreides',
});
for (const destination of ['homeworld:emperor', 'homeworld:emperor:salusa'])
rejected(context, custody, {
player: 'a',
destination,
sources: { 'homeworld:atreides': { normal: 1, elite: 0 } },
});
});
void test('unrelated multi-source shipments, repeated source destinations and inactive worlds cannot bypass custody', () => {
const { context, custody } = fixture();
for (const sources of [
{},
{
'homeworld:emperor': { normal: 1, elite: 0 },
'homeworld:atreides': { normal: 1, elite: 0 },
},
{
'homeworld:emperor': { normal: 1, elite: 0 },
'homeworld:emperor:salusa': { normal: 1, elite: 0 },
'homeworld:atreides': { normal: 1, elite: 0 },
},
{ 'homeworld:guild': { normal: 1, elite: 0 } },
{ 'homeworld:harkonnen': { normal: 1, elite: 0 } },
{ arrakeen: { normal: 1, elite: 0 } },
])
rejected(context, custody, { ...invasion, sources });
rejected(context, custody, {
...invasion,
player: 'a',
sources: {
'homeworld:emperor': { normal: 1, elite: 0 },
'homeworld:emperor:salusa': { normal: 1, elite: 0 },
},
});
const basic = { ...context, advanced: false };
rejected(
basic,
{ ...custody, salusa: null },
{
...invasion,
sources: {
'homeworld:emperor': { normal: 1, elite: 0 },
'homeworld:emperor:salusa': { normal: 1, elite: 0 },
},
},
);
assert.equal(quote(basic, { ...custody, salusa: null }, invasion).amount, 4);
});
void test('malformed and unavailable typed orders reject immutably without pooling or substituting counters', () => {
const { context, custody } = fixture();
for (const intent of [
null,
[],
{},
{ ...invasion, payment: 0 },
{ ...invasion, player: 'absent' },
{ ...invasion, destination: 'arrakeen' },
{ ...invasion, sources: [] },
...[
null,
{},
{ normal: 0, elite: 0 },
{ normal: 1 },
{ normal: 1, elite: 0, advisors: true },
{ normal: '1', elite: 0 },
{ normal: -1, elite: 1 },
{ normal: 0.5, elite: 0 },
{ normal: NaN, elite: 0 },
{ normal: Infinity, elite: 0 },
{ normal: Number.MAX_SAFE_INTEGER, elite: 1 },
{ normal: 21, elite: 0 },
{ normal: 10, elite: 0 },
{ normal: 0, elite: 2 },
].map((forces) => ({
...invasion,
sources: { 'homeworld:emperor': forces },
})),
{
...invasion,
sources: {
'homeworld:emperor': { normal: 19, elite: 0 },
'homeworld:emperor:salusa': { normal: 1, elite: 1 },
},
},
])
rejected(context, custody, intent);
});
void test('entire custody is validated before transport, including duplicate reserves and foreign physical totals', () => {
for (const mutate of [
(c: HomeworldShipmentContext) => {
c.players.push({ ...c.players[0] });
},
(c: HomeworldShipmentContext) => {
c.players[0].reserves += 1;
},
(c: HomeworldShipmentContext) => {
c.players[0].ally = 'absent';
},
(_c: HomeworldShipmentContext, s: HomeworldCustody) => {
s.salusa!.elite = 5;
},
(_c: HomeworldShipmentContext, s: HomeworldCustody) => {
s.visitors['homeworld:atreides'].e.elite = 2;
},
(_c: HomeworldShipmentContext, s: HomeworldCustody) => {
s.visitors['homeworld:atreides'].a = { normal: 1, elite: 0 };
},
(_c: HomeworldShipmentContext, s: HomeworldCustody) => {
s.visitors['homeworld:harkonnen'] = { g: { normal: 1, elite: 0 } };
},
]) {
const { context, custody } = fixture();
mutate(context, custody);
rejected(context, custody);
}
});
void test('quote reads no secret cards, spice, RNG or transport usage and commits no payment', () => {
const { context, custody } = fixture();
const expected = quote(context, custody, invasion);
for (const object of [context, ...context.players])
for (const key of [
'hand',
'spice',
'rng',
'shipped',
'payment',
'response',
])
Object.defineProperty(object, key, {
enumerable: true,
get() {
throw new Error('private or caller-owned context read');
},
});
assert.deepEqual(quote(context, custody, invasion), expected);
});