Files
Dune/game/homeworld-payment-income.ts
T
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

88 lines
3.1 KiB
TypeScript

import type { Game } from './engine';
import { HomeworldCustodyError } from './homeworld-custody';
import { homeworldContext } from './homeworld-game';
import { homeworldPopulations } from './homeworld-population';
export type HomeworldPaymentIncomeContext = Pick<
Game,
'advanced' | 'players' | 'homeworlds'
>;
export type HomeworldPaymentIncomeQuote = {
gross: number;
income: number;
bank: number;
low: boolean;
};
/** Split one already eligible payment. Contributor exclusions, payment-unit
* classification and timing belong to the caller. This does not resolve how
* multiple contributors define a payment for rounding purposes.
* This quotes physical low population only, not occupation entitlements. */
export function quoteHomeworldPaymentIncome(
context: HomeworldPaymentIncomeContext,
ownerId: string,
kind: 'shipment' | 'treachery',
gross: number,
): HomeworldPaymentIncomeQuote {
if (!Number.isSafeInteger(gross) || gross < 0)
throw new HomeworldCustodyError(
'Homeworld payment income requires a nonnegative safe integer payment.',
);
if (kind !== 'shipment' && kind !== 'treachery')
throw new HomeworldCustodyError('Unknown Homeworld payment income kind.');
const faction = kind === 'shipment' ? 'guild' : 'emperor';
const owner = context.players.find((player) => player.id === ownerId);
if (!owner || owner.faction !== faction)
throw new HomeworldCustodyError(
`Only the ${faction === 'guild' ? 'Guild' : 'Emperor'} receives this payment income.`,
);
const card = kind === 'shipment' ? 'junction' : 'kaitain';
const low = context.homeworlds?.custody
? homeworldPopulations(
homeworldContext(context),
context.homeworlds.custody,
).some(
(home) =>
home.native === ownerId && home.card === card && home.side === 'low',
)
: false;
const income = low ? Math.ceil(gross / 2) : gross;
return { gross, income, bank: gross - income, low };
}
/** Compare the unresolved transaction and contributor rounding interpretations.
* The caller may accept agreement; divergence requires an explicit ruling. */
export function quoteGuildPaymentRounding(
gross: number,
contributions: number[],
): { transaction: number; contribution: number; unambiguous: boolean } {
if (
!Number.isSafeInteger(gross) ||
gross < 0 ||
!Array.isArray(contributions) ||
contributions.length > 2 ||
!Array.from(contributions).every(
(amount) => Number.isSafeInteger(amount) && amount >= 0,
)
)
throw new HomeworldCustodyError(
'Guild rounding requires a nonnegative safe payment and at most two nonnegative safe contributions.',
);
const total = contributions.reduce((sum, amount) => sum + amount, 0);
if (!Number.isSafeInteger(total) || total !== gross)
throw new HomeworldCustodyError(
'Guild contributions must sum to the original eligible payment.',
);
const transaction = Math.ceil(gross / 2);
const contribution = contributions.reduce(
(sum, amount) => sum + Math.ceil(amount / 2),
0,
);
return {
transaction,
contribution,
unambiguous: transaction === contribution,
};
}