Add cumulative Truthtrance bribe-payment commitments
This commit is contained in:
1 parent
7fc21113e3
commit
335b4ce638
18 files changed
+1453
-21
No files matched your search
@@ -0,0 +1,134 @@
|
||||
import { nativePromiseScopeSupported, type NativePromiseScope } from './native-promise-scope';
|
||||
|
||||
export type BribeClaim = { turn: number; recipient: string; minimum: number };
|
||||
export type BribePromise = BribeClaim & {
|
||||
event: string;
|
||||
player: string;
|
||||
asker: string;
|
||||
answer: boolean;
|
||||
/** Matching ordinary payments after this answer, before their later receipt. */
|
||||
progress: number;
|
||||
fulfilled?: boolean;
|
||||
released?: boolean;
|
||||
};
|
||||
type BribePayment = { recipient: string; amount: number };
|
||||
|
||||
export class BribePromiseError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'BribePromiseError';
|
||||
}
|
||||
}
|
||||
|
||||
const identifier = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.length > 0 && value === value.trim();
|
||||
const positiveInteger = (value: unknown): value is number =>
|
||||
typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
||||
const optionalBoolean = (value: unknown) => value === undefined || typeof value === 'boolean';
|
||||
|
||||
export function parseBribeClaim(value: unknown): BribeClaim {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
throw new BribePromiseError('Choose a current-turn ordinary bribe question.');
|
||||
const input = value as Record<string, unknown>;
|
||||
if (!positiveInteger(input.turn) || !identifier(input.recipient) || !positiveInteger(input.minimum))
|
||||
throw new BribePromiseError('Choose the current turn, exact recipient and a positive safe whole bribe minimum.');
|
||||
return { turn: input.turn, recipient: input.recipient, minimum: input.minimum };
|
||||
}
|
||||
|
||||
/** Permission for the existing bare native context, not a game-start gate.
|
||||
* The engine retains nonally targets, timing and ordinary payment authority. */
|
||||
export function bribePromiseModeSupported(game: NativePromiseScope & {
|
||||
semutaPreview?: unknown; kullPreview?: unknown; nexusKullPreview?: unknown;
|
||||
richeseBetrayalPreview?: unknown;
|
||||
}): boolean {
|
||||
return nativePromiseScopeSupported(game) && !game.semutaPreview && !game.kullPreview &&
|
||||
!game.nexusKullPreview && !game.richeseBetrayalPreview;
|
||||
}
|
||||
|
||||
export function bribeClaimText(claim: BribeClaim, recipientName: (id: string) => string = id => id): string {
|
||||
return `pay at least ${claim.minimum} spice to ${recipientName(claim.recipient)} in ordinary bribes from now until this turn ends`;
|
||||
}
|
||||
|
||||
export function liveBribePromises(promises: readonly BribePromise[], player: string, turn: number): BribePromise[] {
|
||||
return promises.filter(promise => promise.player === player && promise.turn === turn &&
|
||||
!promise.fulfilled && !promise.released);
|
||||
}
|
||||
|
||||
function validateProgress(promise: BribePromise): void {
|
||||
parseBribeClaim(promise);
|
||||
if (typeof promise.answer !== 'boolean' || !Number.isSafeInteger(promise.progress) || promise.progress < 0 ||
|
||||
!optionalBoolean(promise.fulfilled) || !optionalBoolean(promise.released) ||
|
||||
(promise.fulfilled && promise.released) ||
|
||||
(!promise.answer && promise.progress >= promise.minimum) ||
|
||||
(promise.answer && !promise.released && !!promise.fulfilled !== (promise.progress >= promise.minimum)))
|
||||
throw new BribePromiseError('The bribe commitment has invalid cumulative answer progress.');
|
||||
}
|
||||
|
||||
/** Provisional cumulative composition: only the actual payer's completed
|
||||
* ordinary payments count. Prior payments, card effects, gifts and the later
|
||||
* Mentat receipt do not call this function or add progress again. */
|
||||
export function recordPromisedBribe(promises: readonly BribePromise[], player: string, turn: number,
|
||||
payment: BribePayment): BribePromise[] {
|
||||
if (!payment || typeof payment !== 'object' || Array.isArray(payment) ||
|
||||
!identifier(payment.recipient) || !positiveInteger(payment.amount))
|
||||
throw new BribePromiseError('Retain the exact recipient and positive safe whole ordinary bribe payment.');
|
||||
const progress = new Map<BribePromise, number>();
|
||||
for (const promise of liveBribePromises(promises, player, turn)) {
|
||||
if (promise.recipient !== payment.recipient) continue;
|
||||
validateProgress(promise);
|
||||
const next = promise.progress + payment.amount;
|
||||
if (!Number.isSafeInteger(next))
|
||||
throw new BribePromiseError('The cumulative bribe amount exceeds the safe integer range.');
|
||||
if (!promise.answer && next >= promise.minimum)
|
||||
throw new BribePromiseError('This payment conflicts with your cumulative Truthtrance No answer.');
|
||||
progress.set(promise, next);
|
||||
}
|
||||
return promises.map(promise => {
|
||||
const next = progress.get(promise);
|
||||
return next === undefined ? promise : {
|
||||
...promise, progress: next,
|
||||
...(promise.answer && next >= promise.minimum ? { fulfilled: true } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Minimal final additional ordinary payments, grouped by exact recipient.
|
||||
* Overlapping Yes answers share the same future payment; different recipients
|
||||
* each require their own spice. No caps remain binding after smaller payments. */
|
||||
export function bribePromiseRequirements(promises: readonly BribePromise[], player: string, turn: number): BribePayment[] | null {
|
||||
const live = liveBribePromises(promises, player, turn);
|
||||
const required = new Map<string, number>();
|
||||
for (const promise of live) {
|
||||
validateProgress(promise);
|
||||
if (promise.answer)
|
||||
required.set(promise.recipient, Math.max(required.get(promise.recipient) ?? 0, promise.minimum - promise.progress));
|
||||
}
|
||||
for (const promise of live) {
|
||||
const amount = required.get(promise.recipient) ?? 0;
|
||||
if (!promise.answer && amount >= promise.minimum - promise.progress) return null;
|
||||
if (!Number.isSafeInteger(promise.progress + amount)) return null;
|
||||
}
|
||||
let total = 0;
|
||||
const payments: BribePayment[] = [];
|
||||
for (const [recipient, amount] of required) {
|
||||
total += amount;
|
||||
if (!Number.isSafeInteger(total)) return null;
|
||||
if (amount > 0) payments.push({ recipient, amount });
|
||||
}
|
||||
return payments;
|
||||
}
|
||||
|
||||
export function validateBribePromises(promises: readonly BribePromise[], players: readonly string[]): void {
|
||||
if (!Array.isArray(promises)) throw new BribePromiseError('The bribe commitment record is invalid.');
|
||||
const events = new Set<string>();
|
||||
for (const promise of promises) {
|
||||
parseBribeClaim(promise);
|
||||
if (!identifier(promise.event) || events.has(promise.event) ||
|
||||
!identifier(promise.player) || !identifier(promise.asker) ||
|
||||
!players.includes(promise.player) || !players.includes(promise.asker) || !players.includes(promise.recipient) ||
|
||||
promise.player === promise.asker || promise.player === promise.recipient)
|
||||
throw new BribePromiseError('The bribe commitment lost its original event or participants.');
|
||||
validateProgress(promise);
|
||||
events.add(promise.event);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user