Files
Dune/game/richese-funding.ts
T
2026-09-08 07:37:52 +10:00

88 lines
2.9 KiB
TypeScript

import type { RicheseAuction } from './richese-auction';
export type RicheseFundingRecord = {
amount: number;
allyPayment: number;
donor: string | null;
};
export type RicheseFunding = Record<string, RicheseFundingRecord>;
function nonnegativeInteger(value: number, label: string) {
if (!Number.isSafeInteger(value) || value < 0)
throw new RangeError(`${label} must be a nonnegative safe integer.`);
}
/** Pure exact-split validation; never silently substitutes a different payer. */
export function validateRicheseFunding(
amount: number,
allyPayment: number,
ownAvailable: number,
allyAvailable: number,
): { own: number; ally: number } {
nonnegativeInteger(amount, 'Bid');
nonnegativeInteger(allyPayment, 'Ally payment');
nonnegativeInteger(ownAvailable, 'Own available spice');
nonnegativeInteger(allyAvailable, 'Ally available spice');
if (allyPayment > amount)
throw new RangeError('Ally payment cannot exceed the bid.');
const own = amount - allyPayment;
if (own > ownAvailable || allyPayment > allyAvailable)
throw new RangeError('The declared payment split is not funded.');
return { own, ally: allyPayment };
}
function committedPlayers(
auction: RicheseAuction | null | undefined,
): string[] {
if (!auction) return [];
if (auction.outcome)
return auction.outcome.kind === 'sold' ? [auction.outcome.winner] : [];
if (auction.method === 'silent')
return auction.eligible.filter((id) => Object.hasOwn(auction.sealed, id));
return auction.bidder ? [auction.bidder] : [];
}
function ownShare(record: RicheseFundingRecord): number {
nonnegativeInteger(record.amount, 'Reserved bid');
nonnegativeInteger(record.allyPayment, 'Reserved ally payment');
if (record.allyPayment > record.amount)
throw new RangeError('Reserved ally payment cannot exceed the bid.');
return record.amount - record.allyPayment;
}
/**
* Terminal winners stay reserved until the engine clears their funding record.
* No losing offer, unrelated ledger entry, or unsubmitted Silent draft reserves.
* The engine must atomically bind each accepted bid to its exact funding record.
*/
export function richeseOwnCommitment(
auction: RicheseAuction | null | undefined,
funding: RicheseFunding,
playerId: string,
): number {
if (
!committedPlayers(auction).includes(playerId) ||
!Object.hasOwn(funding, playerId)
)
return 0;
return ownShare(funding[playerId]);
}
/** Returns only this donor's reservation; never a list of private bidder splits. */
export function richeseAllyCommitment(
auction: RicheseAuction | null | undefined,
funding: RicheseFunding,
donorId: string,
): number {
let total = 0;
for (const player of committedPlayers(auction)) {
if (!Object.hasOwn(funding, player)) continue;
const record = funding[player];
if (record.donor !== donorId) continue;
ownShare(record);
total += record.allyPayment;
nonnegativeInteger(total, 'Total reserved ally payment');
}
return total;
}