import type { Game } from './engine'; import type { FreshDiscardBatch } from './semuta-drug'; import { choamGholaEvent, choamMarketGholaError } from './choam-market-ghola'; import { quoteChoamSaleCancellation } from './choam-sale-cancellation'; type OrdinaryDiscard = Extract['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. */ export function semutaMarketGholaSupported( game: Game, continuation: OrdinaryDiscard, entries: FreshDiscardBatch['entries'], ): boolean { const pending = game.pendingChoamMarketGhola, discard = game.pendingTreacheryDiscard; 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 || pending.discardSequence !== game.treacheryDiscardSequence || pending.event !== choamGholaEvent(pending) || JSON.stringify(pending.market) !== JSON.stringify(game.choamMarket) || pending.response?.kind !== 'choamSale' || pending.response.owner !== game.choamMarket.owner || !Array.isArray(pending.response.passed) || new Set(pending.response.passed).size !== pending.response.passed.length || pending.response.passed.some(id => !game.players.some(player => player.id === id)) || continuation.resume.decision || continuation.resume.pendingKarama || continuation.resume.phaseOpening || game.response || game.decision || game.pendingKarama || game.phaseOpening || discard && (!discard.reaction || discard.sequence !== pending.discardSequence)) return false; const entry = entries[0], cards = game.discard.filter(card => card.id === pending.card); if (entries.length !== 1 || !entry || !entry.publicFace || entry.discardedBy !== pending.player || entry.card.id !== pending.card || entry.card.kind !== 'special' || entry.card.effect !== 'ghola' || cards.length !== 1 || cards[0].name !== entry.card.name || cards[0].kind !== entry.card.kind || cards[0].effect !== entry.card.effect) return false; const income = continuation.resume.response; if (income && (income.kind !== 'revivalIncome' || income.intent !== pending.event || income.recipient !== pending.player || income.amount !== 1 || !Array.isArray(income.passed) || income.passed.length !== 0 || !game.players.some(player => player.id === income.owner && player.faction === 'tleilaxu'))) return false; try { if (!quoteChoamSaleCancellation(game, pending.response)) return false; // Before the discard is installed, the sequence is already incremented; // the ordinary frame validator can only run after installation. if (discard && choamMarketGholaError(game)) return false; return true; } catch { return false; } }