Files
Dune/game/ghola-leader-promises.ts
T
Quintine c1ffa1c28a
Verify and publish checkpoint container / container (push) Successful in 10m45s
Prototype named Ghola leader Truthtrance promises
2026-10-29 17:34:55 +11:00

83 lines
4.3 KiB
TypeScript

/** A separate question from ordinary leader revival or merely playing Ghola. */
export type GholaLeaderClaim = { turn: number; leader: string };
export type GholaLeaderPromise = GholaLeaderClaim & {
event: string;
player: string;
asker: string;
answer: boolean;
fulfilled?: boolean;
released?: boolean;
};
export class GholaLeaderPromiseError extends Error {
constructor(message: string) {
super(message);
this.name = 'GholaLeaderPromiseError';
}
}
const identifier = (value: unknown): value is string =>
typeof value === 'string' && value.length > 0 && value === value.trim();
const optionalBoolean = (value: unknown) => value === undefined || typeof value === 'boolean';
/** Question envelopes may contain their own kind and target; extract only the
* canonical claim, never coercing an identity or turn. */
export function parseGholaLeaderClaim(value: unknown): GholaLeaderClaim {
if (!value || typeof value !== 'object' || Array.isArray(value))
throw new GholaLeaderPromiseError('Choose a current-turn named Ghola leader question.');
const input = value as Record<string, unknown>;
if (typeof input.turn !== 'number' || !Number.isSafeInteger(input.turn) || input.turn < 1 || !identifier(input.leader))
throw new GholaLeaderPromiseError('Choose the current turn and exact leader identity.');
return { turn: input.turn, leader: input.leader };
}
export function gholaLeaderClaimText(claim: GholaLeaderClaim,
leaderName: (id: string) => string = id => id === 'kwisatz' ? 'Kwisatz Haderach' : id): string {
return `revive ${leaderName(claim.leader)} with Ghola from now until the end of this turn`;
}
export function liveGholaLeaderPromises(promises: readonly GholaLeaderPromise[], player: string,
turn: number): GholaLeaderPromise[] {
return promises.filter(promise => promise.player === player && promise.turn === turn &&
!promise.fulfilled && !promise.released);
}
/** This describes obligations, not permission or a guarantee that enough cards
* exist. The engine's shared completion planner must establish feasibility. */
export function gholaLeaderRequirements(promises: readonly GholaLeaderPromise[], player: string,
turn: number): { required: string[]; forbidden: string[] } | null {
const required = new Set<string>(), forbidden = new Set<string>();
for (const promise of liveGholaLeaderPromises(promises, player, turn))
(promise.answer ? required : forbidden).add(promise.leader);
if ([...required].some(leader => forbidden.has(leader))) return null;
return { required: [...required], forbidden: [...forbidden] };
}
/** Call only for an actual successful Ghola leader return, never for a play
* returning forces, a proposal, or any other source of revival. */
export function recordPromisedGholaLeader(promises: readonly GholaLeaderPromise[], player: string,
turn: number, leader: string): GholaLeaderPromise[] {
if (!identifier(leader)) throw new GholaLeaderPromiseError('Retain the exact Ghola-returned leader identity.');
const matching = liveGholaLeaderPromises(promises, player, turn).filter(promise => promise.leader === leader);
if (matching.some(promise => !promise.answer))
throw new GholaLeaderPromiseError('This Ghola leader return conflicts with your Truthtrance No answer.');
return promises.map(promise => matching.includes(promise) ? { ...promise, fulfilled: true } : promise);
}
/** Validate durable event and participant identities; native engine rules own
* the physical leader, ownership, costs, and legal Ghola source. */
export function validateGholaLeaderPromises(promises: readonly GholaLeaderPromise[], players: readonly string[]): void {
if (!Array.isArray(promises)) throw new GholaLeaderPromiseError('The Ghola leader commitment record is invalid.');
const events = new Set<string>();
for (const promise of promises) {
parseGholaLeaderClaim(promise);
if (!identifier(promise.event) || events.has(promise.event) ||
!identifier(promise.player) || !identifier(promise.asker) ||
!players.includes(promise.player) || !players.includes(promise.asker) || promise.player === promise.asker ||
typeof promise.answer !== 'boolean' || !optionalBoolean(promise.fulfilled) || !optionalBoolean(promise.released) ||
promise.fulfilled && promise.released)
throw new GholaLeaderPromiseError('The Ghola leader commitment lost its event or participants.');
events.add(promise.event);
}
}