Prototype named Ghola leader Truthtrance promises
Verify and publish checkpoint container / container (push) Successful in 10m45s

This commit is contained in:
Quintine committed 2026-10-29 17:34:55 +11:00
1 parent f8078ed5e2
commit c1ffa1c28a
16 files changed
+689 -9

No files matched your search

+5
View File
@@ -1343,6 +1343,7 @@ function policyActions(g: GameView): Action[] {
if (g.alliancePromiseGuide) return [g.alliancePromiseGuide.action];
if (g.revivalExpressionPromiseGuide) return [g.revivalExpressionPromiseGuide.action];
if (g.leaderRevivalPromiseGuide) return [g.leaderRevivalPromiseGuide.action];
if (g.gholaLeaderPromiseGuide) return [g.gholaLeaderPromiseGuide.action];
if (g.revivalPromiseGuide) return [g.revivalPromiseGuide.action];
if (g.richeseBidPromiseGuide) return [g.richeseBidPromiseGuide.action];
if (g.phaseBidPromiseGuide) return [g.phaseBidPromiseGuide.action];
@@ -1376,6 +1377,10 @@ function policyActions(g: GameView): Action[] {
const answer = g.truthLeaderRevivalAnswers?.[0];
return answer ? [{ type: 'truthAnswer', answer }] : [];
}
if (w.question.kind === 'gholaLeader') {
const answer = g.truthGholaLeaderAnswers?.[0];
return answer ? [{ type: 'truthAnswer', answer }] : [];
}
if (w.question.kind === 'allianceExpression' || w.question.kind === 'mixedAlliance') {
const choices = w.question.kind === 'allianceExpression' ? g.truthAllianceExpressionAnswers : g.truthMixedAllianceAnswers;
const answer = choices?.find(answer => answer !== 'unknown') ?? choices?.[0];
+102 -7
View File
@@ -1,3 +1,4 @@
import { GholaLeaderPromiseError, liveGholaLeaderPromises, gholaLeaderRequirements, recordPromisedGholaLeader, validateGholaLeaderPromises, type GholaLeaderClaim, type GholaLeaderPromise } from './ghola-leader-promises';
import { basicPendingMovement } from './basic-pending-movement';
import { createTwoPlayerControllers, validateTwoPlayerControllersLobby, validateTwoPlayerControllersComplete,
addTwoPlayerControlledSeat, TwoPlayerControllersError, type TwoPlayerControllers } from './two-player-controllers';
@@ -2468,6 +2469,7 @@ export type Game = {
})[];
revivalPromises?: RevivalPromise[];
leaderRevivalPromises?: LeaderRevivalPromise[];
gholaLeaderPromises?: GholaLeaderPromise[];
revivalExpressionPromises?: RevivalExpressionPromise[];
/** Original mixed questions link to the existing ordinary-return ledger. */
mixedRevivalQuestions?: (MixedRevivalClaim & {
@@ -22049,7 +22051,7 @@ function turnActionRule<T>(operation: () => T): T {
try { return operation(); }
catch (error) { if (error instanceof TurnActionError) throw new RuleError(error.message); throw error; }
}
type TurnActionCardRequirement = { effect: PrintedCardEffect; answer: boolean; baseline: number };
type TurnActionCardRequirement = { effect: PrintedCardEffect; answer: boolean; baseline: number; gholaEvent?: string };
/** This field exists only on detached completion clones and is never returned
* as a real game or installed as a selected whole-answer branch. */
type TurnActionCardProbe = Game & {
@@ -22086,7 +22088,9 @@ function recordNativePrintedCardPlay(g: Game, player: string, effect: PrintedCar
recordTurnActionCardPlay(g.turnActionPromises!, player, g.turn, effect));
}
function turnActionCardsMatch(g: Game, actor: string, requirements: readonly TurnActionCardRequirement[]): boolean {
return requirements.every(required => (nativePrintedCardCount(g, actor, required.effect) > required.baseline) === required.answer);
return requirements.every(required => required.gholaEvent
? !!g.gholaLeaderPromises?.find(promise => promise.event === required.gholaEvent && promise.fulfilled)
: (nativePrintedCardCount(g, actor, required.effect) > required.baseline) === required.answer);
}
function* nativePrintedCardActions(g: Game, actor: string, effect: PrintedCardEffect): Generator<Action> {
const player = getPlayer(g, actor);
@@ -22163,7 +22167,8 @@ function* turnActionBoxSeeds(source: Game, actor: string, onlyBox?: string):
/** Reject the known printed fee before paid inspection can suspend legality.
* The native owner chooses the recovered card only after actual payment. */
function guardTurnActionBoxCost(source: Game, owner: Player, box: string) {
if (!liveTurnActionPromises(source.turnActionPromises ?? [], owner.id, source.turn).length) return;
if (!liveTurnActionPromises(source.turnActionPromises ?? [], owner.id, source.turn).length &&
!liveGholaLeaderPromises(source.gholaLeaderPromises ?? [], owner.id, source.turn).some(promise => promise.answer)) return;
for (const seed of turnActionBoxSeeds(source, owner.id, box)) if (turnActionCompletion(seed.game, owner.id)) return;
throw new RuleError('The two-spice Box payment must preserve your accepted whole Truthtrance answers.');
}
@@ -22502,7 +22507,8 @@ function reopenTurnActionWindowReady(g: Game, actor: string) {
/** Check the actual spent card and returned pieces before a later foreign
* income/discard response can hide an own voluntary loss of feasibility. */
function guardTurnActionPrintedCardCost(g: Game, player: Player, used: Card) {
if (!liveTurnActionPromises(g.turnActionPromises ?? [], player.id, g.turn).length) return;
if (!liveTurnActionPromises(g.turnActionPromises ?? [], player.id, g.turn).length &&
!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player.id, g.turn).some(promise => promise.answer)) return;
const trial = structuredClone(g), owner = getPlayer(trial, player.id);
owner.spice += pendingBattleRevivalIncome(trial, owner);
const suspended = g.pendingTreacheryDiscard?.continuation;
@@ -23071,6 +23077,9 @@ function turnActionCompletionAfterFreshDiscard(source: Game, actor: string, unkn
delete trial.travelPromiseOrder;
const cardRequirements: TurnActionCardRequirement[] = leaves.flatMap(leaf => leaf.kind === 'cardPlay'
? [{ effect: leaf.effect, answer: leaf.answer, baseline: nativePrintedCardCount(source, actor, leaf.effect) }] : []);
for (const promise of liveGholaLeaderPromises(source.gholaLeaderPromises ?? [], actor, source.turn))
if (promise.answer) cardRequirements.push({ effect: 'ghola', answer: true,
baseline: nativePrintedCardCount(source, actor, 'ghola'), gholaEvent: promise.event });
if (cardRequirements.length) (trial as TurnActionCardProbe)._turnActionCardPolicy = {
player: actor, forbidden: cardRequirements.filter(required => !required.answer).map(required => required.effect),
};
@@ -32959,6 +32968,10 @@ function applyGholaEffect(g: Game, p: Player, action: Action, cardId?: string) {
);
if (cardId) stageHomeworldRevivalReturn(g, p, 'ghola', group, grant, cardId);
}
if (typeof action.leader === 'string') {
try { g.gholaLeaderPromises = recordPromisedGholaLeader(g.gholaLeaderPromises ?? [], p.id, g.turn, action.leader); }
catch (error) { if (error instanceof GholaLeaderPromiseError) throw new RuleError(error.message); throw error; }
}
techIncome(g, 'axlotl', p);
collectRevivalIncome(g, p, 0, false, true);
recordNativePrintedCardPlay(g, p.id, 'ghola');
@@ -37474,6 +37487,57 @@ function leaderRevivalIdentity(g: Game, p: Player, leader: string): boolean {
return leader === 'kwisatz' ? g.advanced && p.faction === 'atreides' :
leader === DUKE_VIDAL_ID ? p.faction === 'ecaz' && !!g.dukeVidal : p.leaders.some(l => l.id === leader);
}
function gholaLeaderTruthAnswers(g: Game, p: Player, claim: GholaLeaderClaim): TruthAnswer[] {
if (claim.turn !== g.turn || !leaderRevivalIdentity(g, p, claim.leader)) return [];
const ask = g.truthtrance?.queue[0]?.player ?? g.host;
return (['yes', 'no'] as const).filter(answer => {
const promises = [...(g.gholaLeaderPromises ?? []), { ...claim, event: 'ghola-candidate',
player: p.id, asker: ask, answer: answer === 'yes' }];
const requirements = gholaLeaderRequirements(promises, p.id, g.turn);
if (!requirements || requirements.required.length > 1) return false;
return !!turnActionCompletion({ ...g, gholaLeaderPromises: promises }, p.id);
});
}
function bindGholaLeaderTruth(g: Game, p: Player, claim: GholaLeaderClaim, answer: boolean, asker: string) {
requireRule(gholaLeaderTruthAnswers(g, p, claim).includes(answer ? 'yes' : 'no'),
'Choose an answer with a legal named Ghola completion.');
(g.gholaLeaderPromises ??= []).push({ ...claim, event: crypto.randomUUID(), player: p.id, asker, answer });
}
function reconcileGholaLeaderTruth(g: Game, voluntary?: { actor: string; action: Action }) {
if (!g.gholaLeaderPromises?.length) return;
try { validateGholaLeaderPromises(g.gholaLeaderPromises, g.players.map(p => p.id)); }
catch (error) { if (error instanceof GholaLeaderPromiseError) throw new RuleError(error.message); throw error; }
if (g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama) return;
for (const promise of g.gholaLeaderPromises) {
if (promise.fulfilled || promise.released) continue;
if (!promise.answer && (promise.turn !== g.turn || g.status !== 'playing')) { promise.fulfilled = true; continue; }
if (promise.answer && (promise.turn !== g.turn || g.status !== 'playing' || !turnActionCompletion(g, promise.player))) {
requireRule(voluntary?.actor !== promise.player, 'You cannot voluntarily evade your named Ghola Truthtrance answer.');
promise.released = true;
}
}
}
function projectedGholaLeaderGuide(g: Game, player: string): { action: Action } | null {
if (g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama) return null;
const promise = liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player, g.turn).find(p => p.answer);
if (!promise) return null;
// A completion can defer the Ghola play into a later movement forecast.
// Offer an immediate action only if its actual physical play is legal now
// and leaves a joint completion for every other accepted answer.
for (const action of nativePrintedCardActions(g, player, 'ghola')) {
if (action.leader !== promise.leader) continue;
try {
const trial = applyActionInner(g, player, action, 'movementPreparation');
if (!trial.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled)) continue;
trial.truthtrance = null; trial.response = null; trial.pendingKarama = null;
const owner = getPlayer(trial, player);
owner.spice += pendingBattleRevivalIncome(trial, owner);
reconcileRevivalPromises(trial, { actor: player, action });
if (turnActionCompletion(trial, player)) return { action };
} catch (error) { if (!(error instanceof RuleError)) throw error; }
}
return null;
}
function leaderRevivalTruthAnswers(g: Game, p: Player, claim: LeaderRevivalClaim): ('yes' | 'no' | 'unknown')[] {
if (!revivalQuestionWindow(g) || !revivalOpportunity(g, p.id) || claim.turn !== g.turn ||
!leaderRevivalIdentity(g, p, claim.leader)) return [];
@@ -37586,7 +37650,8 @@ function guardPromisedLeaderRevival(g: Game, p: Player, action: Action,
!liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).length &&
!liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length &&
!hasMovementPromises(g, p.id) &&
!hasBribePromises(g, p.id, true)) return;
!hasBribePromises(g, p.id, true) &&
!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], p.id, g.turn).some(promise => promise.answer)) return;
if (!quote) {
requireRule(leaderRevivalPromisesSatisfied(g.leaderRevivalPromises ?? [], p.id, g.turn, []),
'Complete the named leader returns required by your Truthtrance Yes before finishing Revival.');
@@ -37596,8 +37661,10 @@ function guardPromisedLeaderRevival(g: Game, p: Player, action: Action,
trial.truthtrance = null;
if (!trial.revivalOrder) trial.ready = trial.ready.filter(player => player !== p.id);
stagePromisedLeaderReturn(trial, actor, quote);
requireRule(!!revivalPromiseCompletion(trial, actor) && bribeCompatibleLaterPromises(trial, actor, {}),
'This leader request would prevent you from honoring your Revival answers.');
requireRule(!!revivalPromiseCompletion(trial, actor) && bribeCompatibleLaterPromises(trial, actor, {}) &&
(!liveGholaLeaderPromises(trial.gholaLeaderPromises ?? [], p.id, g.turn).some(promise => promise.answer) ||
!!turnActionCompletion(trial, p.id)),
'This leader request would prevent you from honoring your Truthtrance answers.');
}
/** Reconcile the shared purse in actual answer order across both ledgers.
* Existing records without an order stamp precede newly accepted answers. */
@@ -43181,6 +43248,24 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
// turn that forced debit into the passer's voluntary payment or expenditure.
const voluntary = forcedThumperDraw || action.type === 'passResponse' ? undefined : { actor: id, action };
const pledgeVoluntary = initiator === id ? voluntary : undefined;
// Voluntary actions may create a response owned by somebody else before the
// regular reconciler can run. Quote the already-completed action on a
// detached state before that response can hide loss of the last witness.
if (voluntary && liveGholaLeaderPromises(state.gholaLeaderPromises ?? [], id, state.turn).some(promise => promise.answer)) {
const prior = liveGholaLeaderPromises(state.gholaLeaderPromises ?? [], id, state.turn).filter(promise => promise.answer);
const unresolved = prior.some(promise => !g.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled));
if (unresolved && (g.turn !== state.turn || g.status !== 'playing'))
throw new RuleError('Complete your named Ghola Truthtrance answer before ending this turn.');
if (unresolved && !state.truthtrance && !state.response && !state.decision && !state.phaseOpening &&
!state.pendingTreacheryDiscard && !state.pendingKarama &&
(g.truthtrance || g.response || g.decision || g.phaseOpening || g.pendingTreacheryDiscard || g.pendingKarama)) {
const trial = structuredClone(g);
trial.truthtrance = null; trial.response = null; trial.decision = null;
trial.phaseOpening = null; trial.pendingTreacheryDiscard = null; trial.pendingKarama = null;
requireRule(!!turnActionCompletion(trial, id),
'This action would remove the last legal completion of your named Ghola Truthtrance answer.');
}
}
reconcilePledgeCommitments(g, pledgeVoluntary);
reconcileTravelPromises(g, promiseActor, action, state.phase, state);
reconcileBidPromises(g, voluntary);
@@ -43189,6 +43274,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
reconcileRicheseBids(g, voluntary);
reconcileCharityTruth(g, voluntary);
reconcileRevivalPromises(g, voluntary);
reconcileGholaLeaderTruth(g, voluntary);
reconcileBribePromises(g, voluntary);
reconcilePublicDealTruth(g, voluntary, options?.insolventDealPayment ? undefined : state);
reconcileGroundMovementPromises(g, promiseActor, action, state.phase);
@@ -44055,6 +44141,8 @@ function applyActionInner(
bindRevivalExpression: bindRevivalExpressionTruth,
mixedRevivalAnswers: mixedRevivalTruthAnswers,
bindMixedRevival: bindMixedRevivalTruth,
gholaLeaderAnswers: gholaLeaderTruthAnswers,
bindGholaLeader: bindGholaLeaderTruth,
leaderRevivalAnswers: leaderRevivalTruthAnswers,
bindLeaderRevival: bindLeaderRevivalTruth,
revivalAnswers: revivalTruthAnswers,
@@ -48186,6 +48274,8 @@ function applyActionInner(
return g;
}
if (t === 'ready') {
if (g.phase === 8) requireRule(!liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], id, g.turn)
.some(promise => promise.answer), 'Complete your named Ghola Truthtrance answer before finishing Mentat Pause.');
requireRule(!(g.phase === 1 && g.nexus && g.coalitionState?.proposal),
'Resolve the group invitation before finishing the Nexus.');
if (g.phase === 2) {
@@ -48545,6 +48635,11 @@ export function viewGame(state: Game, id: string, now?: number) {
g.truthtrance.question.kind === 'revival' ? revivalTruthAnswers(g, me, g.truthtrance.question) : null,
revivalPromises: (g.revivalPromises ?? []).filter(promise => promise.player === id).map(promise => ({ ...promise })),
revivalPromiseGuide: projectedRevivalPromiseGuide(g, id),
truthGholaLeaderTargets: g.status === 'playing' && g.phase >= 0 && g.phase <= 7 ? g.players.map(player => player.id) : [],
truthGholaLeaderAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
g.truthtrance.question.kind === 'gholaLeader' ? gholaLeaderTruthAnswers(g, me, g.truthtrance.question) : null,
gholaLeaderPromises: (g.gholaLeaderPromises ?? []).filter(promise => promise.player === id).map(promise => ({ ...promise })),
gholaLeaderPromiseGuide: projectedGholaLeaderGuide(g, id),
truthLeaderRevivalTargets: revivalQuestionWindow(g) ? g.players.filter(player => revivalOpportunity(g, player.id)).map(player => player.id) : [],
truthLeaderRevivalAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.target === id &&
g.truthtrance.question.kind === 'leaderRevival' ? leaderRevivalTruthAnswers(g, me, g.truthtrance.question) : null,
+82
View File
@@ -0,0 +1,82 @@
/** 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);
}
}
+19
View File
@@ -3652,6 +3652,25 @@ export const RULE_TOPICS: RuleTopic[] = [
{ area: 'Verification', status: 'Partial', detail: 'Pure whole-tree logic, controlled editor/guide and inherited original H/A/T battle/death histories; broader source families, browser/deployed and final assurance remain due.', evidence: ['tests/fixture-truth-revival-expression.ts', 'tests/truth-revival-expression-runtime.test.ts', 'tests/truth-revival-expression-controls.test.tsx', 'tests/truth-revival-expression-funding.test.ts'] },
],
},
{
id: 'truthtrance-ghola-leader',
title: 'Truthtrance: named Ghola leader',
category: 'Cards', coverage: 'Partial', developmentStage: 'Prototyped',
summary: 'Ask whether a named leader will return using Ghola this turn.',
steps: [
'The provisional question binds a future personal Ghola return of the named leader. Returning forces, ordinary revival and earlier plays do not count.',
'Yes requires a compatible owned completion; No forbids that specific Ghola leader return. One distinct outstanding Yes target is supported. Some pending interactions remain conservatively guarded.',
'The private panel and minimal bot guide use the native Ghola action. Broader interactions, saved-game assurance and deployed acceptance remain open.',
],
related: ['card-truthtrance', 'truthtrance-leader-revival'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Named Ghola outcomes participate in the joint completion planner.', evidence: ['game/ghola-leader-promises.ts', 'game/engine.ts', 'game/truthtrance.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Private question, answers and guide.', evidence: ['components/ghola-leader-claim-fields.tsx', 'components/ghola-leader-promises.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Minimal legal answer and action guide; calibration remains deferred.', evidence: ['game/bots.ts'] },
{ area: 'Documentation', status: 'Partial', detail: 'Provisional interpretation and current limits.', evidence: ['docs/TRUTHTRANCE_LEADER_REVIVAL_PROMISES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused native and control tests; deployed acceptance is open.', evidence: ['tests/ghola-leader-truth-runtime.test.ts', 'tests/ghola-leader-promise-controls.test.tsx'] },
],
},
{
id: 'truthtrance-leader-revival',
title: 'Truthtrance: named leader revival this turn',
+23
View File
@@ -21,6 +21,7 @@ import { parseBribeExpressionClaim, bribeExpressionRecipients, bribeExpressionTe
type BribeExpressionClaim } from './bribe-expression-promises';
import { parseRevivalExpressionClaim, revivalExpressionText, revivalExpressionPromiseModeSupported, type RevivalExpressionClaim } from './revival-expression-promises';
import { parseLeaderRevivalClaim, leaderRevivalClaimText, leaderRevivalPromiseModeSupported, type LeaderRevivalClaim } from './leader-revival-promises';
import { parseGholaLeaderClaim, gholaLeaderClaimText, type GholaLeaderClaim } from './ghola-leader-promises';
import { parseAllianceClaim, allianceClaimText, alliancePromiseModeSupported, type AllianceClaim } from './alliance-promises';
import { parseBribeClaim, bribeClaimText, bribePromiseModeSupported, type BribeClaim } from './bribe-promises';
import { parsePledgeClaim, type PledgeClaim } from './pledge-promises';
@@ -113,6 +114,7 @@ export type TruthQuestion =
| ({ kind: 'bribeExpression'; target: string } & BribeExpressionClaim)
| ({ kind: 'revivalExpression'; target: string } & RevivalExpressionClaim)
| ({ kind: 'leaderRevival'; target: string } & LeaderRevivalClaim)
| ({ kind: 'gholaLeader'; target: string } & GholaLeaderClaim)
| ({ kind: 'alliance'; target: string } & AllianceClaim)
| ({ kind: 'bribe'; target: string } & BribeClaim)
| ({ kind: 'pledgeAid'; target: string } & PledgeClaim)
@@ -321,6 +323,16 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
'Choose a supported current-turn revival expression in a quiet window.');
return { kind: 'revivalExpression', target: v.target, ...claim };
}
if (v.kind === 'gholaLeader') {
check(Object.keys(v).sort().join(',') === 'kind,leader,target,turn',
'Supply the current turn, exact named leader and respondent.');
const claim = parseGholaLeaderClaim(v);
check(leaderRevivalPromiseModeSupported(g) && g.status === 'playing' && g.phase >= 0 && g.phase <= 7 &&
!g.response && !g.decision && !g.phaseOpening && !g.pendingRevival && !g.pendingKarama &&
!g.pendingTreacheryDiscard && claim.turn === g.turn,
'Choose a supported current-turn Ghola leader question in a quiet window.');
return { kind: 'gholaLeader', target: v.target, ...claim };
}
if (v.kind === 'leaderRevival') {
check(Object.keys(v).sort().join(',') === 'kind,leader,target,turn',
'Supply the current turn, exact named leader and respondent.');
@@ -849,6 +861,7 @@ export function truthQuestionText(
if (q.kind === 'charity') return 'Will you claim Charity after this answer during this turn’s Charity phase?';
if (q.kind === 'revivalExpression') return `Will you ${revivalExpressionText(q.claim, id => id === 'kwisatz' ? 'Kwisatz Haderach' : leaderName(id))} from now until your Revival opportunity ends this turn?`;
if (q.kind === 'leaderRevival') return `Will you ${leaderRevivalClaimText(q, id => id === 'kwisatz' ? 'Kwisatz Haderach' : leaderName(id))}?`;
if (q.kind === 'gholaLeader') return `Will you ${gholaLeaderClaimText(q, id => id === 'kwisatz' ? 'Kwisatz Haderach' : leaderName(id))}?`;
if (q.kind === 'allianceExpression') return `Is it true that you ${allianceExpressionText(q.claim, playerName)}?`;
if (q.kind === 'alliance') return `Will you ${allianceClaimText(q, playerName)}?`;
if (q.kind === 'bribeExpression') return `Will you ${bribeExpressionText(q.claim, playerName)} from now until this turn ends?`;
@@ -990,6 +1003,8 @@ export function resolveTruthAction(
bindMixedRevival?: (g: Game, p: Player, claim: MixedRevivalClaim, answer: boolean, asker: string) => void;
revivalExpressionAnswers?: (g: Game, p: Player, claim: RevivalExpressionClaim) => TruthAnswer[];
bindRevivalExpression?: (g: Game, p: Player, claim: RevivalExpressionClaim, answer: boolean, asker: string) => void;
gholaLeaderAnswers?: (g: Game, p: Player, claim: GholaLeaderClaim) => TruthAnswer[];
bindGholaLeader?: (g: Game, p: Player, claim: GholaLeaderClaim, answer: boolean, asker: string) => void;
leaderRevivalAnswers?: (g: Game, p: Player, claim: LeaderRevivalClaim) => TruthAnswer[];
bindLeaderRevival?: (g: Game, p: Player, claim: LeaderRevivalClaim, answer: boolean, asker: string) => void;
revivalAnswers?: (g: Game, p: Player, claim: RevivalClaim) => TruthAnswer[];
@@ -1172,6 +1187,9 @@ export function resolveTruthAction(
if (w.question.kind === 'revivalExpression')
check(effects.revivalExpressionAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
'Choose a whole revival expression with a compatible native completion.');
if (w.question.kind === 'gholaLeader')
check(effects.gholaLeaderAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
'Choose a named Ghola leader question with a compatible owned completion.');
if (w.question.kind === 'leaderRevival')
check(effects.leaderRevivalAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
'Choose a named leader question with a compatible native completion.');
@@ -1303,6 +1321,11 @@ export function resolveTruthAction(
'Choose a whole revival answer compatible with native returns and earlier promises.');
if (a.answer !== 'unknown') effects.bindRevivalExpression(g, p, q, a.answer === 'yes', current.player);
}
if (q.kind === 'gholaLeader') {
check(effects.gholaLeaderAnswers && effects.bindGholaLeader && effects.gholaLeaderAnswers(g, p, q).includes(a.answer),
'Choose a named Ghola leader answer compatible with native eligibility and earlier promises.');
if (a.answer !== 'unknown') effects.bindGholaLeader(g, p, q, a.answer === 'yes', current.player);
}
if (q.kind === 'leaderRevival') {
check(effects.leaderRevivalAnswers && effects.bindLeaderRevival && effects.leaderRevivalAnswers(g, p, q).includes(a.answer),
'Choose a named leader-revival answer compatible with native eligibility and earlier promises.');