Files
Dune/tests/homeworld-charity-quote.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

171 lines
5.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { charityAmount, charityPayer, charityQuote } from '../game/charity';
import { createGame, newPlayer } from '../game/engine';
import { createHomeworldCustody } from '../game/homeworld-custody';
import { homeworldContext } from '../game/homeworld-game';
import type { FactionId } from '../game/catalog';
function fixture(
faction: FactionId = 'atreides',
advanced = false,
reserves = 5,
) {
const g = createGame(
'HOMECHARITY',
newPlayer('p', 'Recipient', faction),
advanced,
);
g.players.push(newPlayer('c', 'CHOAM', 'choam'));
const p = g.players[0];
p.reserves = reserves;
p.spice = 0;
if (faction === 'emperor') {
p.elites = {
reserves: Math.min(reserves, 5),
tanks: 0,
forces: {},
revived: 0,
};
}
g.homeworlds = { custody: createHomeworldCustody(homeworldContext(g)) };
g.choamCharity = { turn: g.turn, canceled: false };
return { g, p };
}
void test('ordinary poor-faction eligibility determines the separate bank addition, not a new wealth threshold', () => {
const { g, p } = fixture();
for (const [spice, ordinary, homeworld] of [
[0, 2, 1],
[1, 1, 1],
[2, 0, 0],
[12, 0, 0],
]) {
p.spice = spice;
const before = structuredClone(g);
assert.deepEqual(charityQuote(g, p), {
ordinary,
homeworld,
total: ordinary + homeworld,
});
assert.equal(charityAmount(g, p), ordinary + homeworld);
assert.equal(charityPayer(g)?.id, 'c');
assert.deepEqual(g, before);
}
g.choamCharity!.canceled = true;
p.spice = 1;
assert.equal(charityPayer(g), undefined);
assert.deepEqual(charityQuote(g, p), { ordinary: 1, homeworld: 1, total: 2 });
});
void test('Advanced Bene Gesserit retains wealthy charity eligibility while Basic Bene Gesserit does not', () => {
for (const advanced of [false, true]) {
const { g, p } = fixture('beneGesserit', advanced, 10);
for (const spice of [0, 1, 2, 99]) {
p.spice = spice;
const ordinary = advanced ? 2 : Math.max(0, 2 - spice);
assert.deepEqual(charityQuote(g, p), {
ordinary,
homeworld: ordinary > 0 ? 1 : 0,
total: ordinary + (ordinary > 0 ? 1 : 0),
});
}
}
});
void test('only the native primary low side grants a bonus: Salusa low alone and high primary grant none', () => {
const { g, p } = fixture('emperor', true, 10);
g.homeworlds!.custody!.salusa = { normal: 0, elite: 1 };
assert.deepEqual(charityQuote(g, p), { ordinary: 2, homeworld: 0, total: 2 });
// Both sides low still grant exactly one primary benefit, never two.
p.reserves = 5;
p.elites!.reserves = 1;
assert.deepEqual(charityQuote(g, p), { ordinary: 2, homeworld: 1, total: 3 });
// At the primary printed high threshold, the bank addition disappears.
p.reserves = 6;
assert.deepEqual(charityQuote(g, p), { ordinary: 2, homeworld: 0, total: 2 });
const basic = fixture('emperor', false, 4);
assert.deepEqual(charityQuote(basic.g, basic.p), {
ordinary: 2,
homeworld: 1,
total: 3,
});
});
void test('Inflation starts next turn, doubles both funded portions, and cancels all eligible charity', () => {
const { g, p } = fixture();
p.spice = 1;
g.inflation = {
side: 'double',
placedTurn: 1,
updatedTurn: 1,
flipped: false,
};
assert.deepEqual(charityQuote(g, p), { ordinary: 1, homeworld: 1, total: 2 });
g.turn = 2;
assert.deepEqual(charityQuote(g, p), { ordinary: 2, homeworld: 2, total: 4 });
p.spice = 2;
assert.deepEqual(charityQuote(g, p), { ordinary: 0, homeworld: 0, total: 0 });
p.spice = 0;
g.inflation.side = 'cancel';
assert.deepEqual(charityQuote(g, p), { ordinary: 0, homeworld: 0, total: 0 });
const bg = fixture('beneGesserit', true, 10);
bg.p.spice = 50;
bg.g.turn = 2;
bg.g.inflation = { ...g.inflation, side: 'double' };
assert.deepEqual(charityQuote(bg.g, bg.p), {
ordinary: 4,
homeworld: 2,
total: 6,
});
bg.g.inflation.side = 'cancel';
assert.equal(charityAmount(bg.g, bg.p), 0);
});
void test('disabled or pre-force Homeworld state retains the original ordinary quote', () => {
const { g, p } = fixture();
for (const homeworlds of [undefined, null, { custody: null }]) {
g.homeworlds = homeworlds;
assert.deepEqual(charityQuote(g, p), {
ordinary: 2,
homeworld: 0,
total: 2,
});
}
});
void test('the quote reads only claimant wealth and public custody, never other wealth, cards, predictions or RNG', () => {
const { g, p } = fixture();
const claimant = { id: p.id, faction: p.faction, spice: 1 };
for (const seat of g.players) {
for (const key of ['hand', 'traitors', 'prediction', 'spice']) {
Object.defineProperty(seat, key, {
get() {
throw new Error(`Private ${seat.id}.${key}`);
},
});
}
}
for (const key of ['deck', 'spiceDeck', 'discard']) {
Object.defineProperty(g, key, {
get() {
throw new Error(`Private ${key}`);
},
});
}
const oldRandom = Math.random;
Math.random = () => {
throw new Error('Charity cannot draw randomness.');
};
try {
assert.deepEqual(charityQuote(g, claimant), {
ordinary: 1,
homeworld: 1,
total: 2,
});
assert.equal(charityAmount(g, claimant), 2);
} finally {
Math.random = oldRandom;
}
});