Allow bounded Kull cancellation continuations
Verify and publish checkpoint container / container (push) Successful in 10m22s
Verify and publish checkpoint container / container (push) Successful in 10m22s
This commit is contained in:
1 parent
07f1f1f2ae
commit
6e1b95d2fc
9 files changed
+569
-7
No files matched your search
+78
-4
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user