Support multiple named Ghola commitments
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
6c1530382d
commit
bdd5aaf18c
8 files changed
+286
-26
No files matched your search
+80
-9
@@ -22201,6 +22201,53 @@ function* turnActionTransferOrderSeeds(source: Game, actor: string, requirements
|
||||
* A separate Movement forecast retains the later-play alternative. */
|
||||
function* turnActionOwnedCardSeeds(source: Game, actor: string, requirements: readonly TurnActionCardRequirement[]):
|
||||
Generator<{ game: Game; actions: Action[] }> {
|
||||
const outstandingTargets = (game: Game): string[] => [...new Set(requirements.flatMap(required => {
|
||||
if (!required.gholaEvent) return [];
|
||||
const promise = game.gholaLeaderPromises?.find(row => row.event === required.gholaEvent);
|
||||
return promise && !promise.fulfilled && !promise.released && promise.answer ? [promise.leader] : [];
|
||||
}))];
|
||||
const targets = outstandingTargets(source);
|
||||
if (targets.length > 1) {
|
||||
// A named return can share a physical Ghola play with same-leader answers,
|
||||
// but different leaders need distinct plays. Search actual held Ghola and
|
||||
// known Box recoveries only; neither random draws nor extra cards are
|
||||
// invented. Each Ghola step satisfies a new identity and each Box step
|
||||
// consumes a physical Box, so the recursion is bounded.
|
||||
const maxSteps = targets.length + getPlayer(source, actor).hand.filter(card => card.effect === 'nullentropyBox').length;
|
||||
const search = function* (game: Game, actions: Action[]): Generator<{ game: Game; actions: Action[] }> {
|
||||
if (actions.length >= maxSteps) return;
|
||||
const remaining = outstandingTargets(game);
|
||||
if (!remaining.length) return;
|
||||
for (const action of nativePrintedCardActions(game, actor, 'ghola')) {
|
||||
if (typeof action.leader !== 'string' || !remaining.includes(action.leader)) continue;
|
||||
try {
|
||||
const played = applyActionInner(game, actor, action, 'movementPreparation');
|
||||
const owner = getPlayer(played, actor);
|
||||
owner.spice += pendingBattleRevivalIncome(played, owner);
|
||||
const projected = groundMovementProbe(played, actor), next = [...actions, action];
|
||||
const outstanding = outstandingTargets(projected);
|
||||
if (outstanding.length >= remaining.length) continue;
|
||||
if (!outstanding.length) yield { game: projected, actions: next };
|
||||
else yield* search(projected, next);
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
const owner = getPlayer(game, actor);
|
||||
const known = knownMovementDiscards(game.movementDiscardKnowledge, actor, game.discard)
|
||||
.filter(card => card.effect === 'ghola');
|
||||
for (const box of owner.hand.filter(card => card.effect === 'nullentropyBox')) for (const card of known) {
|
||||
const projected = projectMovementBoxRecovery(game, actor, box.id, card.id, false);
|
||||
if (!projected) continue;
|
||||
const next = [...actions, { type: 'card', card: box.id }];
|
||||
yield* search(projected, next);
|
||||
}
|
||||
};
|
||||
yield* search(source, []);
|
||||
// For several names, do not let the generic one-card forecast search an
|
||||
// expensive movement tree after each partial play: it tracks printed-use
|
||||
// counts, not named-event progress. This bounded current-state route keeps
|
||||
// already accepted one-name forecasts unchanged.
|
||||
return;
|
||||
}
|
||||
const gholaDue = requirements.some(required => required.effect === 'ghola' && required.answer &&
|
||||
nativePrintedCardCount(source, actor, 'ghola') <= required.baseline);
|
||||
const boxDue = requirements.some(required => required.effect === 'nullentropyBox' && required.answer &&
|
||||
@@ -37494,7 +37541,7 @@ function gholaLeaderTruthAnswers(g: Game, p: Player, claim: GholaLeaderClaim): T
|
||||
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;
|
||||
if (!requirements) return false;
|
||||
return !!turnActionCompletion({ ...g, gholaLeaderPromises: promises }, p.id);
|
||||
});
|
||||
}
|
||||
@@ -37518,17 +37565,29 @@ function reconcileGholaLeaderTruth(g: Game, voluntary?: { actor: string; action:
|
||||
}
|
||||
}
|
||||
function projectedGholaLeaderGuide(g: Game, player: string): { action: Action } | null {
|
||||
const promises = liveGholaLeaderPromises(g.gholaLeaderPromises ?? [], player, g.turn).filter(p => p.answer);
|
||||
if (!promises.length) return null;
|
||||
// A paid Box selection reveals its candidates only to its owner. Prefer a
|
||||
// known physical Ghola that preserves all remaining commitments.
|
||||
if (g.pendingNullentropy?.player === player && g.decision?.kind === 'nullentropy' &&
|
||||
g.decision.player === player && !g.truthtrance && !g.response && !g.pendingKarama && !g.pendingTreacheryDiscard) {
|
||||
const paid = g.pendingNullentropy;
|
||||
if (!nullentropyIntegrity(g)) for (const card of eligibleNullentropyCards(g.discard).filter(card => card.effect === 'ghola')) {
|
||||
const projected = projectMovementBoxRecovery(g, player, paid.box, card.id, true);
|
||||
if (projected && turnActionCompletion(projected, player))
|
||||
return { action: { type: 'decision', event: paid.event, card: card.id } };
|
||||
}
|
||||
return 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.
|
||||
// A completion may defer a play into a later Movement forecast. Quote an
|
||||
// immediately legal native return only if the same physical play preserves
|
||||
// every remaining commitment.
|
||||
for (const action of nativePrintedCardActions(g, player, 'ghola')) {
|
||||
if (action.leader !== promise.leader) continue;
|
||||
if (!promises.some(promise => promise.leader === action.leader)) continue;
|
||||
try {
|
||||
const trial = applyActionInner(g, player, action, 'movementPreparation');
|
||||
if (!trial.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled)) continue;
|
||||
if (!promises.some(promise => trial.gholaLeaderPromises?.find(row => row.event === promise.event && row.fulfilled))) continue;
|
||||
const owner = getPlayer(trial, player);
|
||||
owner.spice += pendingBattleRevivalIncome(trial, owner);
|
||||
trial.truthtrance = null; trial.response = null; trial.pendingKarama = null;
|
||||
@@ -37536,6 +37595,15 @@ function projectedGholaLeaderGuide(g: Game, player: string): { action: Action }
|
||||
if (turnActionCompletion(trial, player)) return { action };
|
||||
} catch (error) { if (!(error instanceof RuleError)) throw error; }
|
||||
}
|
||||
// If the shared plan starts with an owned Box, expose that exact native
|
||||
// start; the paid private search supplies its own decision on the next step.
|
||||
const owner = getPlayer(g, player);
|
||||
const known = knownMovementDiscards(g.movementDiscardKnowledge, player, g.discard)
|
||||
.filter(card => card.effect === 'ghola');
|
||||
for (const box of owner.hand.filter(card => card.effect === 'nullentropyBox')) for (const card of known) {
|
||||
const projected = projectMovementBoxRecovery(g, player, box.id, card.id, false);
|
||||
if (projected && turnActionCompletion(projected, player)) return { action: { type: 'card', card: box.id } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function leaderRevivalTruthAnswers(g: Game, p: Player, claim: LeaderRevivalClaim): ('yes' | 'no' | 'unknown')[] {
|
||||
@@ -43260,7 +43328,10 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
|
||||
!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.truthtrance = null; trial.response = null;
|
||||
// The owner's paid Box choice is part of the witness, not an unrelated
|
||||
// foreign decision to clear before quoting its eventual selected card.
|
||||
if (!(trial.pendingNullentropy?.player === id && trial.decision?.kind === 'nullentropy')) 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.');
|
||||
|
||||
Reference in new issue
Block a user