Continue Kull across paid sale responses
Verify and publish checkpoint container / container (push) Successful in 10m57s

This commit is contained in:
Quintine committed 2026-10-29 02:00:59 +11:00
1 parent 3d2470b7f8
commit e7520e8679
9 files changed
+487 -18

No files matched your search

+35 -15
View File
@@ -1620,7 +1620,7 @@ export type Game = {
allianceReceipt?: true;
/** New Basic Kull attempts retain whole-answer order and progress. */
turnActionReceipt?: true;
/** A Basic bonus cancellation retains its already-paid auction parent. */
/** A Basic sale cancellation retains its already-paid auction parent. */
auctionReceipt?: true;
stage: 'offer' | 'counter';
selection?: { source: 'printed' | 'nexus'; card: string; nexusEvent?: string };
@@ -6940,7 +6940,7 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
!frame.resume.decision && !frame.resume.pendingKarama && !frame.resume.phaseOpening;
let revivalSupported = false;
let guildSupported = false;
let bonusSupported = false;
let saleSupported = false;
if (nativeResponse && frame && use?.kind === 'cancel') {
// Success has just installed this very Kull restriction. Validate the
// suspended source as it stood before that restriction, without changing
@@ -6952,12 +6952,19 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
revivalSupported = !!g.pendingRevival && semutaRevivalKaramaResponse(source, use.response, frame.owner);
guildSupported = (!!g.pendingShipment || !!g.pendingGuildTransport) &&
semutaGuildShipmentKaramaResponse(source, use.response);
bonusSupported = frame.auctionReceipt === true && use.response.kind === 'harkonnenBonus' &&
!!g.currentAuctionSale && (g.currentAuctionSale.origin === 'normal'
const paidSale = frame.auctionReceipt === true && !!g.currentAuctionSale && (g.currentAuctionSale.origin === 'normal'
? !!g.auction && !g.richeseAuction
: g.currentAuctionSale.origin === 'cache' && !!g.richeseAuction &&
g.richeseAuction.source === 'cache' && !g.auction) &&
semutaBonusNoFieldTraitorResponse(source, use.response, frame.owner);
g.richeseAuction.source === 'cache' && !g.auction);
if (paidSale && use.response.kind === 'harkonnenBonus')
saleSupported = semutaBonusNoFieldTraitorResponse(source, use.response, frame.owner);
else if (paidSale && use.response.kind === 'ixAllyCard')
saleSupported = semutaMarketGrantKaramaResponse(source, use.response, frame.owner);
else if (paidSale && use.response.kind === 'emperorIncome' && !use.response.source &&
semutaIncomeKaramaResponse(source, use.response)) {
validateKaramaUse(source, getPlayer(source, frame.owner), use);
saleSupported = true;
}
} catch (error) {
if (!(error instanceof RuleError)) throw error;
}
@@ -6971,13 +6978,14 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
JSON.stringify(use.response) === JSON.stringify(frame.resume.response) &&
!frame.resume.decision && !frame.resume.pendingKarama && !frame.resume.phaseOpening;
return battleSupported && !g.truthtrance && !g.pendingRicheseGift && !g.pendingNullentropy &&
!g.pendingAmbassador && !g.pendingIxAlly && (!g.pendingRevival || revivalSupported) &&
!g.pendingAmbassador && (!g.pendingIxAlly || saleSupported && use?.kind === 'cancel' &&
use.response.kind === 'ixAllyCard') && (!g.pendingRevival || revivalSupported) &&
(!g.pendingShipment || guildSupported) && (!g.pendingGuildTransport || guildSupported) &&
!g.pendingHomeworldShipment && !g.pendingTerrorEntry &&
!g.pendingEcazPlacement && !g.pendingMoritaniPlacement &&
!g.pendingArrivalOverlap && !g.pendingArrivalReaction?.length && !g.pendingExchange &&
!g.pendingRichesePurchaseIncome && !g.pendingChoamMarketGhola && (!g.currentAuctionSale || bonusSupported) &&
!g.choamMarket && (!g.richeseAuction || bonusSupported) && !g.pendingFremenMove && !g.pendingIxMove &&
!g.pendingRichesePurchaseIncome && !g.pendingChoamMarketGhola && (!g.currentAuctionSale || saleSupported) &&
!g.choamMarket && (!g.richeseAuction || saleSupported) && !g.pendingFremenMove && !g.pendingIxMove &&
!g.pendingMobileMove && !g.pendingChoamMove && !g.ornithopter && !g.ecologicalStorm &&
!g.summonedWorm && !g.wormRides.length;
}
@@ -7951,7 +7959,10 @@ function treacheryDiscardIntegrity(g: Game) {
(!g.pendingNullentropy || continuation?.kind === 'karamaRicheseGiftDiscard') &&
(!g.pendingAmbassador || box || truth || ambassadorTerror || ambassadorCost || continuation?.kind === 'karamaRicheseGiftDiscard') &&
(!g.pendingIxAlly || box || truth || continuation?.kind === 'karamaRicheseGiftDiscard' ||
continuation?.kind === 'karamaMarketDiscard' && continuation.use.response.kind === 'ixAllyCard'),
continuation?.kind === 'karamaMarketDiscard' && continuation.use.response.kind === 'ixAllyCard' ||
(continuation?.kind === 'kullSuccessDiscard' || continuation?.kind === 'kullCounterDiscard') &&
continuation.frame.auctionReceipt === true && continuation.frame.intent.kind === 'ordinary' &&
continuation.frame.intent.use.kind === 'cancel' && continuation.frame.intent.use.response.kind === 'ixAllyCard'),
'The saved discard does not match its current continuation.',
);
const ids = new Set<string>();
@@ -19231,8 +19242,17 @@ function canonicalKullCard(card: Card): boolean {
return !!expected && card.name === expected.name && card.kind === expected.kind &&
card.effect === expected.effect;
}
/** A settled normal sale is not an outstanding bid. Validate its native
* response and physical auction receipt before exempting it from funding. */
function kullPaidAuctionSale(g: Game, player: Player): boolean {
const sale = g.currentAuctionSale, auction = g.auction, response = g.response;
if (!sale || sale.origin !== 'normal' || !auction || auction.bidder !== player.id ||
sale.winner !== player.id || sale.amount !== auction.bid || !response) return false;
try { return !!validateAuctionContinuationCancellation(g, response); }
catch (error) { if (error instanceof RuleError) return false; throw error; }
}
function kullCostRetainsAuctionPayment(g: Game, p: Player, card: Card, physical: readonly Card[]): boolean {
return !g.auction || g.auction.bidder !== p.id || auctionSpicePaymentFunded(g, p) ||
return !g.auction || g.auction.bidder !== p.id || kullPaidAuctionSale(g, p) || auctionSpicePaymentFunded(g, p) ||
p.hand.some(held => held.id !== card.id && canonicalKullCard(held) &&
physical.reduce((count, candidate) => count + Number(candidate.id === held.id), 0) === 1 &&
!karamaSpendingBlock(g, p, held, 'auction-free-purchase'));
@@ -19429,8 +19449,8 @@ function kullIntegrity(g: Game): void {
requireRule(frame.auctionReceipt === undefined || frame.auctionReceipt === true &&
!g.advanced && g.semutaPreview === true && !!g.currentAuctionSale &&
frame.intent.kind === 'ordinary' && frame.intent.use.kind === 'cancel' &&
frame.intent.use.response.kind === 'harkonnenBonus',
'The Kull attempt has lost its original paid auction bonus.');
['harkonnenBonus','emperorIncome','ixAllyCard'].includes(frame.intent.use.response.kind),
'The Kull attempt has lost its original paid auction response.');
validateKullAttemptStamp(g, frame, physicalTreacheryCards(g));
requireRule(Number.isSafeInteger(g.kullSequence) && g.kullSequence! > 0 &&
frame.event === JSON.stringify(['kull', g.turn, g.phase, g.kullSequence, frame.player, frame.owner]),
@@ -19498,7 +19518,7 @@ function beginKullAttempt(g: Game, p: Player, card: Card, intent: KullFrame['int
'Kull preview cannot interrupt this unproven Truthtrance cancellation continuation.');
// This composition is user-deferred. The guard is private and independent
// of CHOAM's hidden hand; never accept a transaction needing a made-up remedy.
requireRule(!g.auction || g.auction.bidder !== p.id ||
requireRule(!g.auction || g.auction.bidder !== p.id || kullPaidAuctionSale(g, p) ||
g.auction.bid <= p.spice + (g.auction.allyPayment ?? 0),
'Kull preview defers Karama activation while your winning overbid cannot be paid in spice.');
const resume: KullFrame['resume'] = {
@@ -19524,7 +19544,7 @@ function beginKullAttempt(g: Game, p: Player, card: Card, intent: KullFrame['int
...(turnActionPromiseModeSupported(g) && g.turnActionPromises?.length
? { turnActionReceipt: true as const } : {}),
...(g.semutaPreview && !g.advanced && !!g.currentAuctionSale && intent.kind === 'ordinary' &&
intent.use.kind === 'cancel' && intent.use.response.kind === 'harkonnenBonus'
intent.use.kind === 'cancel' && ['harkonnenBonus','emperorIncome','ixAllyCard'].includes(intent.use.response.kind)
? { auctionReceipt: true as const } : {}),
worthless: structuredClone(g.pendingChoamWorthless ?? null), signature: '',
};