Files
Dune/game/homeworld-payment-income.ts
T

143 lines
5.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 whole eligible invoice, including all eligible contributors.
* Contributor exclusions, payment-unit classification and timing belong to the
* caller. 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 };
}
/** Attribute the frozen final Bank remainder of one eligible player invoice.
* Contributions retain declared primary/ally order. The caller excludes direct
* Bank and self-payee legs and selects the first Bureaucrat-eligible payer;
* bankRetained excludes that payer's flat diversion. */
export function quoteHomeworldBankPayment(
contributions: readonly { payer: string; amount: number }[],
bankRetained: number,
diversion?: { payer: string; amount: 2 },
): { payer: string; amount: number }[] {
if (!Array.isArray(contributions))
throw new HomeworldCustodyError(
'Homeworld Bank payment requires original payer contributions.',
);
const payers = new Set<string>();
let total = 0;
for (const leg of contributions) {
if (
!leg ||
typeof leg.payer !== 'string' ||
leg.payer.trim().length === 0 ||
payers.has(leg.payer) ||
!Number.isSafeInteger(leg.amount) ||
leg.amount <= 0
)
throw new HomeworldCustodyError(
'Homeworld Bank payment requires distinct payers with positive safe original contributions.',
);
payers.add(leg.payer);
total += leg.amount;
if (!Number.isSafeInteger(total))
throw new HomeworldCustodyError(
'Homeworld Bank payment contributions exceed a safe invoice total.',
);
}
let diversionIndex = -1;
if (diversion !== undefined) {
if (
!diversion ||
diversion.amount !== 2 ||
typeof diversion.payer !== 'string'
)
throw new HomeworldCustodyError(
'Homeworld Bank diversion requires an original payer and exactly two spice.',
);
diversionIndex = contributions.findIndex(
(leg) => leg.payer === diversion.payer,
);
if (diversionIndex < 0 || contributions[diversionIndex].amount < 5)
throw new HomeworldCustodyError(
'Homeworld Bank diversion requires a selected original contribution of at least five spice.',
);
}
const remainingTotal = total - (diversionIndex < 0 ? 0 : 2);
if (
!Number.isSafeInteger(bankRetained) ||
(bankRetained !== 0 &&
bankRetained !== remainingTotal &&
bankRetained !== Math.floor(remainingTotal / 2) &&
bankRetained !== Math.ceil(remainingTotal / 2))
)
throw new HomeworldCustodyError(
'Homeworld Bank remainder must match zero, full or whole-invoice half income.',
);
const partial = bankRetained > 0 && bankRetained < remainingTotal;
let remainder = 0;
if (partial) {
let base = 0;
for (let i = 0; i < contributions.length; i++) {
const remaining = contributions[i].amount - (i === diversionIndex ? 2 : 0);
base += Math.floor(remaining / 2);
}
remainder = bankRetained - base;
}
const bankLegs: { payer: string; amount: number }[] = [];
for (let i = 0; i < contributions.length; i++) {
const leg = contributions[i];
const remaining = leg.amount - (i === diversionIndex ? 2 : 0);
let amount = partial
? Math.floor(remaining / 2)
: bankRetained === remainingTotal ? remaining : 0;
if (remainder > 0) {
const extra = Math.min(remainder, remaining - amount);
amount += extra;
remainder -= extra;
}
if (i === diversionIndex) amount += 2;
if (amount > 0) bankLegs.push({ payer: leg.payer, amount });
}
return bankLegs;
}