Initial commit
This commit is contained in:
commit
8c709fb52f
771 files changed
+222983
No files matched your search
@@ -0,0 +1,131 @@
|
||||
import { TECH_TOKENS, type TechId, type TechState } from './tech-tokens';
|
||||
|
||||
export class PhaseResourceError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'PhaseResourceError';
|
||||
}
|
||||
}
|
||||
function requireResources(
|
||||
condition: unknown,
|
||||
message: string,
|
||||
): asserts condition {
|
||||
if (!condition) throw new PhaseResourceError(message);
|
||||
}
|
||||
const whole = (n: unknown): n is number =>
|
||||
Number.isSafeInteger(n) && (n as number) >= 0;
|
||||
export type ResourceSeat = { id: string; spice: number };
|
||||
export type AidRefund = { player: string; amount: number; balance: number };
|
||||
export type PhaseResourceContext = {
|
||||
phase: number;
|
||||
players: readonly ResourceSeat[];
|
||||
techTokens?: TechState | null;
|
||||
aid: Readonly<Record<string, { recipient: string; amount: number }>>;
|
||||
};
|
||||
/** Refunds return escrow to its donor. The recipient's current alliance, funds,
|
||||
* or continued eligibility is not needed to return that already committed spice. */
|
||||
export function quoteAidRefunds(
|
||||
players: readonly ResourceSeat[],
|
||||
aid: PhaseResourceContext['aid'],
|
||||
): AidRefund[] {
|
||||
requireResources(
|
||||
Array.isArray(players) &&
|
||||
new Set(players.map((p) => p.id)).size === players.length &&
|
||||
aid &&
|
||||
typeof aid === 'object' &&
|
||||
!Array.isArray(aid),
|
||||
'Phase completion needs distinct seated donors and an aid ledger.',
|
||||
);
|
||||
return Object.entries(aid).map(([id, credit]) => {
|
||||
const p = players.find((p) => p.id === id);
|
||||
requireResources(
|
||||
p &&
|
||||
whole(credit?.amount) &&
|
||||
whole(p.spice) &&
|
||||
whole(p.spice + credit.amount),
|
||||
'Phase completion needs valid seated aid refunds and balances.',
|
||||
);
|
||||
return {
|
||||
player: id,
|
||||
amount: credit.amount,
|
||||
balance: p.spice + credit.amount,
|
||||
};
|
||||
});
|
||||
}
|
||||
export type PhaseResourceQuote = {
|
||||
credits: (
|
||||
| {
|
||||
kind: 'tech';
|
||||
token: TechId;
|
||||
player: string;
|
||||
amount: number;
|
||||
balance: number;
|
||||
}
|
||||
| { kind: 'aid'; player: string; amount: number; balance: number }
|
||||
)[];
|
||||
tokenResets: TechId[];
|
||||
refunds: AidRefund[];
|
||||
clearAid: boolean;
|
||||
balances: Record<string, number>;
|
||||
};
|
||||
/** Current-phase token payouts precede escrow refunds. No phase is entered,
|
||||
* no token accrues income, and no already-settled phase prefix is replayed. */
|
||||
export function quotePhaseResources(
|
||||
g: PhaseResourceContext,
|
||||
): PhaseResourceQuote {
|
||||
requireResources(
|
||||
whole(g.phase) &&
|
||||
g.phase <= 8 &&
|
||||
Array.isArray(g.players) &&
|
||||
new Set(g.players.map((p) => p.id)).size === g.players.length &&
|
||||
g.players.every(
|
||||
(p) => typeof p.id === 'string' && !!p.id && whole(p.spice),
|
||||
),
|
||||
'Phase resource settlement needs a current phase and valid player balances.',
|
||||
);
|
||||
const quote: PhaseResourceQuote = {
|
||||
credits: [],
|
||||
tokenResets: [],
|
||||
refunds: [],
|
||||
clearAid: [3, 5, 6].includes(g.phase),
|
||||
balances: Object.fromEntries(g.players.map((p) => [p.id, p.spice])),
|
||||
};
|
||||
if (g.techTokens)
|
||||
for (const rule of TECH_TOKENS) {
|
||||
if (rule.phase !== g.phase) continue;
|
||||
const token = g.techTokens[rule.id];
|
||||
requireResources(
|
||||
token &&
|
||||
typeof token === 'object' &&
|
||||
whole(token.spice) &&
|
||||
(token.owner === null || g.players.some((p) => p.id === token.owner)),
|
||||
'Current-phase technology income needs valid token custody and spice.',
|
||||
);
|
||||
if (!token.owner || !token.spice) continue;
|
||||
const balance = quote.balances[token.owner] + token.spice;
|
||||
requireResources(
|
||||
whole(balance),
|
||||
'Technology settlement would overflow the owner’s spice balance.',
|
||||
);
|
||||
quote.balances[token.owner] = balance;
|
||||
quote.credits.push({
|
||||
kind: 'tech',
|
||||
token: rule.id,
|
||||
player: token.owner,
|
||||
amount: token.spice,
|
||||
balance,
|
||||
});
|
||||
quote.tokenResets.push(rule.id);
|
||||
}
|
||||
if (quote.clearAid) {
|
||||
quote.refunds = quoteAidRefunds(
|
||||
g.players.map((p) => ({ id: p.id, spice: quote.balances[p.id] })),
|
||||
g.aid,
|
||||
);
|
||||
for (const refund of quote.refunds) {
|
||||
quote.credits.push({ kind: 'aid', ...refund });
|
||||
quote.balances[refund.player] = refund.balance;
|
||||
}
|
||||
}
|
||||
return quote;
|
||||
}
|
||||
Reference in new issue
Block a user