57 lines
3.6 KiB
TypeScript
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;
|
|
}
|
|
}
|