Allow bounded Kull cancellation continuations
Verify and publish checkpoint container / container (push) Successful in 10m22s

This commit is contained in:
Quintine committed 2026-10-29 01:46:28 +11:00
1 parent 07f1f1f2ae
commit 6e1b95d2fc
9 files changed
+569 -7

No files matched your search

+78 -4
View File
@@ -19253,18 +19253,50 @@ function kullCostPromisesAllow(g: Game, p: Player, card: Card, fuelSource: 'prin
projected.decision = null;
projected.pendingKarama = null;
let wholeSource = projected;
if (wholePromises.length && g.pendingKull?.intent.kind === 'ordinary' &&
g.pendingKull.intent.use.kind === 'cancel') {
// Kull success leaves the original response open; prevention resumes the
// original cancellation from that same response. Keep it for either quote.
wholeSource = structuredClone(projected);
wholeSource.response = structuredClone(source.response);
wholeSource.decision = structuredClone(source.decision);
wholeSource.phaseOpening = structuredClone(source.phaseOpening);
}
if (!success && wholePromises.length && g.pendingKull) {
if (g.pendingKull.intent.kind !== 'ordinary') return false;
const original = getPlayer(projected, g.pendingKull.owner);
const original = getPlayer(wholeSource, g.pendingKull.owner);
const attempted = original.hand.find(row => row.id === g.pendingKull!.card);
if (!attempted) return false;
const resumed = projectKullOrdinaryOutcome(projected, original, attempted, g.pendingKull.intent.use);
const resumed = projectKullOrdinaryOutcome(wholeSource, original, attempted, g.pendingKull.intent.use);
if (!resumed) return false;
wholeSource = resumed;
}
let wholeCompletion = !wholePromises.length || !!turnActionCompletion(wholeSource, p.id);
if (!wholeCompletion && success && wholeSource.response &&
(wholeSource.response.kind === 'voice' || wholeSource.response.kind === 'prescience') &&
!wholeSource.decision && !wholeSource.phaseOpening && !battlePromises.length && !shipmentPromises.length &&
!(['travelPromises','movementPromises','movementExpressionPromises','bidPromises','phaseBidPromises',
'revivalPromises','leaderRevivalPromises','revivalExpressionPromises','bribePromises',
'bribeExpressionPromises'] as const).some(key => wholeSource[key]?.some(row =>
row.player === p.id && !row.fulfilled && !row.released)) &&
wholePromises.every(promise => {
let found = false;
for (const branch of turnActionRule(() => turnActionBranches(promise))) {
found = true;
if (branch.some(leaf => leaf.kind !== 'payment')) return false;
}
return found;
})) {
// The reopened Voice/Prescience response cannot change the payer's
// wallet or recipient. Quote a payment-only answer after that response,
// without choosing either player's response or a later battle plan.
const afterResponse = structuredClone(wholeSource);
afterResponse.response = null;
wholeCompletion = !!turnActionCompletion(afterResponse, p.id);
}
if ((battlePromises.length && !findReachableBattlePlan(projected, owner, { promises: battlePromises })) ||
(shipmentPromises.length && !findShipmentCompletion(projected, owner, shipmentPromises)) ||
(wholePromises.length && !turnActionCompletion(wholeSource, p.id)))
!wholeCompletion)
return false;
}
return true;
@@ -19497,10 +19529,52 @@ function resumeKullAttempt(g: Game, prevented: boolean): void {
spendKarama(g, p, p.hand.find(card => card.id === frame.card)!, frame.intent.use, true);
}
}
/** Quote only known shipment-price and ordinary-auction effects without
/** Quote known shipment-price, ordinary-auction and bounded cancellation effects without
* executing a foreign response, drawing a card or exposing a hidden lot.
* Other parent effects need their own deterministic whole-answer quotation. */
function projectKullOrdinaryOutcome(g: Game, player: Player, card: Card, use: KaramaUse): Game | null {
if (use.kind === 'cancel') {
const response = use.response;
// A separately owned arrival/phase decision remains at the table after
// this response; do not erase it to manufacture a completion witness.
if (!g.response || g.decision || g.phaseOpening ||
JSON.stringify(g.response) !== JSON.stringify(response)) return null;
const battleWindow = karamaBattleCancelOpportunity(g, player.id);
const trial = structuredClone(g), owner = getPlayer(trial, player.id);
if (!owner.hand.some(held => held.id === card.id)) return null;
owner.hand = owner.hand.filter(held => held.id !== card.id);
if (battleWindow && card.kind === 'special' && card.effect === 'karama')
recordNativeTurnActionWindowCard(trial, player.id, 'karamaBattleCancel', battleWindow.source);
if (response.kind === 'voice' || response.kind === 'prescience' || response.kind === 'eliteStrength') {
const quote = currentCombatResponseQuote(trial, {kind:'response',
response:{kind:response.kind, owner:response.owner}, canceled:true});
// A later power/choice belongs to its original owner and cannot be
// assumed as part of this deterministic cancellation quote.
if (quote.response || quote.decision) return null;
commitCombatResponseQuote(trial, quote);
} else {
// A plain queued shipment entry has no remaining effect without Ecaz
// or Moritani. Any other queued entry may own a separate decision.
const arrivals = g.pendingArrivalReaction ?? [];
const inertArrivals = !arrivals.length || (!byFaction(g, 'ecaz') && !byFaction(g, 'moritani') &&
arrivals.every(entry => entry.cause === 'shipment' && entry.resume === 'none' &&
entry.turn === g.turn && entry.phase === g.phase &&
Object.keys(entry).every(key => ['entrant','territory','sector','amount','elite','cause','resume','turn','phase'].includes(key))));
if (!semutaEmperorShareKaramaResponse(g, response) &&
!(response.kind === 'guildIncome' && response.source === undefined &&
semutaIncomeKaramaResponse(g, response) && !g.pendingArrivalOverlap &&
inertArrivals && !g.pendingSpiritualAdvisor && !g.pendingAutomaticAdvisorFlips?.length)) return null;
}
if (battleWindow) {
const original = liveTurnActionPromises(trial.turnActionPromises ?? [], player.id, trial.turn);
trial.turnActionPromises = turnActionRule(() => closeTurnActionWindowCardPlay(
trial.turnActionPromises ?? [], player.id, trial.turn, 'karamaBattleCancel', battleWindow.source));
if (original.some(row => trial.turnActionPromises?.find(next => next.event === row.event)?.released)) return null;
}
trial.truthtrance = null; trial.response = null; trial.decision = null;
trial.pendingKarama = null; trial.phaseOpening = null; trial.pendingKull = null;
return trial;
}
if (use.kind !== 'shipment' && use.kind !== 'purchase' && use.kind !== 'auctionPayment') return null;
const window = windowCardOpportunity(g, use.kind === 'shipment' ? 'karamaShipment' : 'karamaAuction');
const lot = use.kind === 'shipment' ? null : bidOpportunity(g);
+1 -1
View File
@@ -3316,7 +3316,7 @@ export const RULE_TOPICS: RuleTopic[] = [
steps: [
'In a quiet Basic Storm, Spice Blow, Charity, Bidding, Revival or Shipment and Movement window, ask whether a player will use a named native leader in their first later battle this turn, if one occurs. Storm must have a pending movement after the dials; Spice Blow must be before the draw. The answer waits for an actual battle; if none occurs, it expires. This separate provisional question currently supports classic factions without optional modules and uses only currently available leaders for its answer.',
'Choose Combine printed card plays, native battle powers and this turn’s other actions. Full AND/OR groups share sixteen logical leaves and four grouping levels, with optional current facts. This is a provisional composition of original-window card plays, native power declarations, printed Hajr/Ghola use, current battle decisions, final Nexus membership, current-lot bids, ordinary Revival, funded payments and owned-stage travel; wider action families and optional combinations remain unfinished.',
'The supported bare Basic printed-Kull profile also permits these questions, including with Richese and Semuta. The answer remains bound while Kull or its counter is resolving; some Karama uses remain guarded until their effects can be quoted without assuming hidden or foreign outcomes.',
'The supported bare Basic printed-Kull profile also permits these questions, including with Richese and Semuta. The answer remains bound while Kull or its counter is resolving. Voice, Prescience, Ixian cyborg strength and clean Guild income cancellations can use a proven continuation; other Karama uses remain guarded until their effects can be quoted without assuming hidden or foreign outcomes.',
'A final-alliance clause names another current participant during this original quiet native Nexus. Existing invitations and reciprocal formation remain authoritative. Offers, temporary membership and your own Ready do not settle the clause; final actual membership is fixed only when the Nexus closes.',
'Whole No can permit a temporary paid pair and later require a break. Own Ready must preserve a completion after final membership is fixed; remaining payment or physical-action alternatives keep their later deadlines. Definite answers, including constants, uniformly reopen own Nexus readiness; later ordinary payments require confirmation again.',
'Missing foreign consent can offer I don’t know and save or change the question; it never invents a definite Yes. Unknown requires an otherwise funded native completion, so a known unaffordable branch stays No. Foreign withdrawal can release impossibility without vetoing the other player’s rights.',