Support printed Kull during market Ghola
Verify and publish checkpoint container / container (push) Successful in 11m25s

This commit is contained in:
Quintine committed 2026-10-29 08:37:50 +11:00
1 parent 79fc12c1c1
commit bcca08cac5
10 files changed
+232 -25

No files matched your search

+3 -1
View File
@@ -135,7 +135,9 @@ export function choamMarketGholaError(g: Game): string | null {
const heldRevival = g.homeworldRevivalReturn;
const contexts = [g, g.pendingExchange, g.pendingNullentropy?.resume,
g.pendingRicheseGift?.resume, g.pendingRichesePurchaseIncome?.resume,
g.summonedWorm?.resume,
g.summonedWorm?.resume, g.pendingKull?.resume,
continuation && (continuation.kind === 'kullSuccessDiscard' ||
continuation.kind === 'kullCounterDiscard') ? continuation.frame.resume : null,
continuation && 'resume' in continuation ? continuation.resume : null,
heldRevival && ['choice', 'arrival'].includes(heldRevival.stage)
? { response: heldRevival.resumeResponse } : null];
+28 -6
View File
@@ -551,7 +551,7 @@ import {
import { transferDistrans } from './distrans';
import { distransAuctionBlock } from './distrans-timing';
import { SEMUTA_DRUG_ID, committedSemutaCandidates, resolveSemutaDrug, type FreshDiscardBatch, type SemutaContext } from './semuta-drug';
import { semutaMarketGholaSupported } from './semuta-market-ghola';
import { semutaMarketGholaProfile, semutaMarketGholaSupported } from './semuta-market-ghola';
import {
prepareRicheseGift,
transferRicheseGift,
@@ -6618,6 +6618,16 @@ function semutaKaramaRoleCost(g: Game, owner: string, card: Card): boolean {
return card.effect === 'karama' || (card.effect === 'truthtrance' &&
canUseAsKaramaRole(g, getPlayer(g, owner), card));
}
/** Match only the native income belonging to this already committed Ghola. */
function semutaMarketGholaIncome(g: Game, response: ResponseWindow): boolean {
const pending = g.pendingChoamMarketGhola;
if (!semutaMarketGholaProfile(g) || !bareBasicSemutaProfile(g) ||
!pending || pending.stage !== 'income' || response.kind !== 'revivalIncome' ||
response.source !== undefined || response.intent !== pending.event ||
response.recipient !== pending.player || response.amount !== 1 ||
!g.players.some(p => p.id === response.owner && p.faction === 'tleilaxu')) return false;
return choamMarketGholaError({...g, response}) === null;
}
/** Original paid income sources; cancellation never reverses the completed transaction. */
function semutaIncomeKaramaResponse(g: Game, response: ResponseWindow): boolean {
if (g.battle || g.pendingShipment || g.pendingGuildTransport ||
@@ -6629,7 +6639,8 @@ function semutaIncomeKaramaResponse(g: Game, response: ResponseWindow): boolean
if (response.kind === 'revivalIncome') {
// Ordinary returns and Ghola have already committed. Only their income
// remains; other producers retain their own unfinished parent contracts.
if (response.source !== undefined || g.pendingRevival || g.pendingChoamMarketGhola ||
if (response.source !== undefined || g.pendingRevival ||
(g.pendingChoamMarketGhola && !semutaMarketGholaIncome(g, response)) ||
(g.homeworldRevivalReturn && g.homeworldRevivalReturn.stage !== 'complete') ||
g.pendingHomeworldShipment || g.pendingSpiritualAdvisor ||
g.pendingArrivalOverlap || g.pendingArrivalReaction?.length ||
@@ -7029,6 +7040,7 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
let revivalSupported = false;
let guildSupported = false;
let saleSupported = false;
let marketSupported = 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
@@ -7040,6 +7052,12 @@ 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);
if (g.pendingChoamMarketGhola && semutaMarketGholaIncome(source, use.response)) {
validateKaramaUse(source, getPlayer(source, frame.owner), use);
marketSupported = true;
} else if (!g.pendingChoamMarketGhola && g.choamMarket && use.response.kind === 'choamSale') {
marketSupported = semutaMarketGrantKaramaResponse(source, use.response, frame.owner);
}
const paidSale = frame.auctionReceipt === true && !!g.currentAuctionSale && (g.currentAuctionSale.origin === 'normal'
? !!g.auction && !g.richeseAuction
: g.currentAuctionSale.origin === 'cache' && !!g.richeseAuction &&
@@ -7072,8 +7090,8 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
!g.pendingHomeworldShipment && !g.pendingTerrorEntry &&
!g.pendingEcazPlacement && !g.pendingMoritaniPlacement &&
!g.pendingArrivalOverlap && !g.pendingArrivalReaction?.length && !g.pendingExchange &&
!g.pendingRichesePurchaseIncome && !g.pendingChoamMarketGhola && (!g.currentAuctionSale || saleSupported) &&
!g.choamMarket && (!g.richeseAuction || saleSupported) && !g.pendingFremenMove && !g.pendingIxMove &&
!g.pendingRichesePurchaseIncome && (!g.pendingChoamMarketGhola || marketSupported) && (!g.currentAuctionSale || saleSupported) &&
(!g.choamMarket || marketSupported) && (!g.richeseAuction || saleSupported) && !g.pendingFremenMove && !g.pendingIxMove &&
!g.pendingMobileMove && !g.pendingChoamMove && !g.ornithopter && !g.ecologicalStorm &&
!g.summonedWorm && !g.wormRides.length;
}
@@ -9618,6 +9636,10 @@ function semutaOfferSupported(
const marketGholaDiscard = continuation.kind === 'ordinaryCardDiscard' && continuation.effect === 'ghola' &&
g.semutaPreview === true && bareBasicSemutaProfile(g) && ordinaryDiscardIncomeMatches(g, continuation) &&
semutaMarketGholaSupported(g, continuation, entries);
const marketGholaIncomeDiscard = continuation.kind === 'karamaIncomeDiscard' &&
g.semutaPreview === true && bareBasicSemutaProfile(g) && !g.nexusKullPreview &&
(!g.pendingTreacheryDiscard || !!g.pendingTreacheryDiscard.reaction) &&
semutaMarketGholaIncome(g, continuation.use.response);
const richeseDiscard = richeseGiftDiscard || continuation.kind === 'karamaRicheseAuctionDiscard';
const auctionAdvantageDiscard = continuation.kind === 'karamaAuctionAdvantageDiscard';
const terminalDiscard = continuation.kind === 'karamaTerminalDiscard';
@@ -10050,13 +10072,13 @@ function semutaOfferSupported(
(richeseGiftDiscard || !g.pendingRicheseGift) &&
(richeseGiftDiscard || (continuation.kind === 'karamaIncomeDiscard' &&
continuation.use.response.kind === 'richesePurchaseIncome') || !g.pendingRichesePurchaseIncome) &&
(richeseGiftDiscard || saleCostDiscard || marketGholaDiscard || (continuation.kind === 'choamSaleDiscard'
(richeseGiftDiscard || saleCostDiscard || marketGholaDiscard || marketGholaIncomeDiscard || (continuation.kind === 'choamSaleDiscard'
? !!g.choamMarket && !g.choamMarket.sale &&
!g.choamMarket.trade && !g.biddingEnd && !g.currentAuctionSale
: continuation.kind === 'kaitainDiscard'
? g.biddingEnd?.event === continuation.event && biddingEndError(g) === null
: !g.choamMarket)) &&
(richeseGiftDiscard || marketGholaDiscard || !g.pendingChoamMarketGhola);
(richeseGiftDiscard || marketGholaDiscard || marketGholaIncomeDiscard || !g.pendingChoamMarketGhola);
}
function semutaContext(g: Game, owner: Player): SemutaContext {
const pending = g.pendingTreacheryDiscard!;
+10 -6
View File
@@ -6,6 +6,15 @@ import { quoteChoamSaleCancellation } from './choam-sale-cancellation';
type OrdinaryDiscard = Extract<NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
{ kind: 'ordinaryCardDiscard' }>;
/** Shared bounds for the original discard and its later income response. */
export function semutaMarketGholaProfile(game: Game): boolean {
return game.status === 'playing' && !game.advanced && game.semutaPreview === true &&
game.expansions.includes('choam') && !game.homeworlds && !game.nexusCards &&
!game.leaderSkills && !game.techTokens && !game.strongholdCards && !game.ecazTreachery &&
!game.nexusKullPreview && !game.discoveryEnabled && !game.discoveries && !game.discoveryStash &&
!game.greatMaker && !game.sandtrout && !game.coalitionState;
}
/** Qualify the original Ghola discard while its CHOAM sale is suspended.
* The caller separately owns the Basic Semuta profile and the ordinary
* revival-income qualification. This never changes a saved frame. */
@@ -13,12 +22,7 @@ export function semutaMarketGholaSupported(
game: Game, continuation: OrdinaryDiscard, entries: FreshDiscardBatch['entries'],
): boolean {
const pending = game.pendingChoamMarketGhola, discard = game.pendingTreacheryDiscard;
if (game.status !== 'playing' || game.advanced || game.semutaPreview !== true ||
!game.expansions.includes('choam') || game.homeworlds || game.nexusCards ||
game.leaderSkills || game.techTokens || game.strongholdCards || game.ecazTreachery ||
game.kullPreview || game.nexusKullPreview ||
game.discoveryEnabled || game.discoveries || game.discoveryStash || game.greatMaker ||
game.sandtrout || game.coalitionState || !pending || !game.choamMarket ||
if (!semutaMarketGholaProfile(game) || !pending || !game.choamMarket ||
continuation.kind !== 'ordinaryCardDiscard' || continuation.effect !== 'ghola' ||
pending.stage !== 'discard' || pending.turn !== game.turn || pending.phase !== game.phase ||
pending.player !== continuation.player || pending.card !== continuation.card ||