Files
Dune/game/semuta-market-ghola.ts
T
Quintine bcca08cac5
Verify and publish checkpoint container / container (push) Successful in 11m25s
Support printed Kull during market Ghola
2026-10-29 08:37:50 +11:00

57 lines
3.6 KiB
TypeScript

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<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. */
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;
}
}