Files
Dune/game/bribe-options.ts

49 lines
2.3 KiB
TypeScript

import type { Action, Game } from './engine';
export interface BribeOptions {
available: number;
incoming: number;
blocked: string | null;
targets: { id: string; maximum: number; limitation: string | null }[];
}
/** Ordinary bribes remain out of turn, including after Mentat's opening
* collection. Late receipts wait for the next opening, not another collection. */
export function bribeTimingBlock(g: Game): string | null {
if (g.status !== 'playing') return 'Bribes require a game in progress.';
if (g.inflation?.side === 'double') return 'Bribes are prohibited while Inflation shows Double.';
if (g.pendingKull || g.pendingTreacheryDiscard || g.pendingNullentropy || g.bureaucratPayments?.pending ||
['name', 'reveal'].includes(g.battle?.mentatQuestion?.stage ?? '') ||
g.nexusCards?.phase?.stage === 'drawing' || g.truthtrance ||
g.nexusTraitorExchanges?.some(exchange => exchange.stage === 'return') ||
g.phaseOpening || g.response || g.decision)
return 'Finish the current decision or response before paying a bribe.';
return null;
}
/** The existing automatic token lifecycle will turn an unflipped Cancel into
* Double at this opening. An owed payment must precede that known closure. */
export function bribesCloseBeforeNextMentat(g: Pick<Game, 'turn' | 'inflation'>): boolean {
const token = g.inflation;
return !!token && token.updatedTurn < g.turn && !token.flipped && token.side === 'cancel';
}
/** Payment only removes spendable spice, so a preserved completion is monotonic. */
export function maximumBribe(available: number, preservesCommitments: (amount: number) => boolean): number {
let low = 0, high = Math.max(0, Math.floor(available));
while (low < high) {
const amount = low + Math.ceil((high - low) / 2);
if (preservesCommitments(amount)) low = amount;
else high = amount - 1;
}
return low;
}
/** Client-side guard against a stale selection; the server revalidates every payment. */
export function bribeAction(options: BribeOptions, target: string, amount: number): Action | null {
const recipient = options.targets.find(candidate => candidate.id === target);
if (options.blocked || !recipient || !Number.isSafeInteger(amount) || amount < 1 || amount > recipient.maximum)
return null;
return { type: 'bribe', target, amount };
}