Prototype named Ghola leader Truthtrance promises
Verify and publish checkpoint container / container (push) Successful in 10m45s
Verify and publish checkpoint container / container (push) Successful in 10m45s
This commit is contained in:
1 parent
f8078ed5e2
commit
c1ffa1c28a
16 files changed
+689
-9
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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.');
|
||||
|
||||
Reference in new issue
Block a user