Extend Basic Emperor auction gifts to Nexus and Richese
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-30 19:06:22 +11:00
1 parent b258c6867f
commit dac1b432db
11 files changed
+509 -14

No files matched your search

+51 -7
View File
@@ -2127,6 +2127,8 @@ export type Game = {
turn: number;
signature: string;
giftAmount?: number;
source?: 'richese';
quote?: Extract<RicheseSettlementQuote,{kind:'sold'}>;
} | null;
richeseGiftBlocked?: { turn: number; phase: number; cards: string[] };
richeseAllyOffer?: RicheseAllyOffer | null;
@@ -6720,8 +6722,10 @@ function emperorAuctionGiftResumeMatches(g: Game, response: ResponseWindow, deci
const pending = g.pendingEmperorAuctionGift;
return !!pending && response.kind === 'emperorGift' && response.emperorAuctionGiftEvent === pending.event &&
response.owner === pending.owner && response.recipient === pending.recipient &&
decision?.kind === 'auctionPayment' && decision.player === pending.recipient &&
g.phase === 3 && !!g.auction && g.auction.bidder === pending.recipient && !g.currentAuctionSale && !g.richeseAuction;
g.phase === 3 && !g.currentAuctionSale && (pending.source === 'richese'
? !decision && !!g.richeseAuction && !g.auction && pending.quote?.winner === pending.recipient
: decision?.kind === 'auctionPayment' && decision.player === pending.recipient &&
!!g.auction && g.auction.bidder === pending.recipient && !g.richeseAuction);
}
/** Preserve physical identity: a Shrine authorizes the role, not a manufactured card. */
function semutaKaramaRoleCost(g: Game, owner: string, card: Card): boolean {
@@ -7202,7 +7206,8 @@ function kullSemutaParentSupported(g: Game, frame?: NonNullable<Game['pendingKul
!g.pendingEcazPlacement && !g.pendingMoritaniPlacement &&
!g.pendingArrivalOverlap && !g.pendingArrivalReaction?.length && !g.pendingExchange &&
!g.pendingRichesePurchaseIncome && (!g.pendingChoamMarketGhola || marketSupported) && (!g.currentAuctionSale || saleSupported) &&
(!g.choamMarket || marketSupported) && (!g.richeseAuction || saleSupported) && !g.pendingFremenMove && !g.pendingIxMove &&
(!g.choamMarket || marketSupported) && (!g.richeseAuction || saleSupported ||
!!frame && use?.kind === 'cancel' && emperorAuctionGiftResumeMatches(g, use.response, frame.resume.decision)) && !g.pendingFremenMove && !g.pendingIxMove &&
!g.pendingMobileMove && !g.pendingChoamMove && !g.ornithopter && !g.ecologicalStorm &&
!g.summonedWorm && !g.wormRides.length;
}
@@ -10260,7 +10265,10 @@ function semutaOfferSupported(
(advisorDiscard || auctionCostDiscard || bonusDiscard || auctionAdvantageDiscard || richeseGiftDiscard || bgDiscard || continuation.kind === 'specialKaramaDiscard' || continuation.kind === 'saphoAuctionDiscard' ||
continuation.kind === 'ixAllyCard' ||
(continuation.kind === 'karamaIncomeDiscard' &&
continuation.use.response.kind === 'emperorIncome') || richeseTruthDiscard || richeseBoxDiscard || richeseDistransDiscard || !g.richeseAuction) &&
continuation.use.response.kind === 'emperorIncome') ||
continuation.kind === 'karamaEmperorShareDiscard' &&
emperorAuctionGiftResumeMatches(g, continuation.use.response, continuation.resume.decision) ||
richeseTruthDiscard || richeseBoxDiscard || richeseDistransDiscard || !g.richeseAuction) &&
(richeseGiftDiscard || combatDiscard || alliedTraitorDiscard || occupyDiscard || continuation.kind === 'saphoAggressorDiscard' ||
(continuation.kind === 'saphoTruthOrderDiscard' && !!continuation.battleEvent &&
g.phase === 6 && g.battle?.event === continuation.battleEvent && !g.battle.revealed && !g.battle.preparation &&
@@ -15969,6 +15977,7 @@ function settleRicheseLot(g: Game) {
return;
}
if (beginRicheseBetrayal(g,quote)) return;
if (beginEmperorRicheseAuctionGift(g,quote)) return;
settleRicheseSoldLot(g,quote,false);
}
const EMPEROR_SELLER_POLICY = 'Provisional seller policy: retain your own full original price after proving it. The seller stays unpaid; no Spice Bank reimbursement, payer invoice or payment-derived income is created. Original card acquisition and native bonus/lot continuation still resolve.';
@@ -18914,6 +18923,12 @@ function auctionNext(g: Game) {
return;
}
if (beginNormalRicheseBetrayal(g)) return;
if (!g.advanced && g.nexusCards) {
g.decision = { kind: 'auctionPayment', player: next.player };
g.active = next.player;
beginEmperorAuctionGift(g);
if (g.pendingEmperorAuctionGift) return;
}
if (beginNexusEmperorPayment(g)) return;
g.decision = { kind: 'auctionPayment', player: next.player };
g.active = next.player;
@@ -18931,15 +18946,32 @@ function emperorAuctionGiftParked(g: Game) {
return g.pendingKull?.resume ?? null;
}
function emperorAuctionGiftSignature(g: Game): string {
if (g.pendingEmperorAuctionGift?.source === 'richese')
return JSON.stringify(['richese',g.turn,g.phase,g.active,g.richeseAuction,g.richeseBidding,
g.richeseFunding,g.aid,g.pendingEmperorAuctionGift.quote]);
return JSON.stringify([g.turn, g.phase, g.active, g.decision ?? emperorAuctionGiftParked(g)?.decision, g.order, g.auction, g.aid]);
}
function beginEmperorRicheseAuctionGift(g: Game, quote: Extract<RicheseSettlementQuote,{kind:'sold'}>): boolean {
if (g.advanced || g.status !== 'playing' || g.phase !== 3 || g.richeseAuction?.source !== 'cache' ||
g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || g.nexusCards ||
g.discoveryEnabled || g.sandtrout || g.coalitionState || g.ecazTreachery ||
g.pendingEmperorAuctionGift || g.response || g.decision || g.phaseOpening || g.truthtrance ||
g.pendingKarama || g.pendingTreacheryDiscard || g.pendingRicheseGift || g.pendingNullentropy ||
g.currentAuctionSale || g.auction || g.ixAuction) return false;
const emperor = byFaction(g,'emperor'), buyer = g.players.find(player => player.id === quote.winner);
if (!emperor || !buyer || emperor.id === buyer.id || emperor.ally !== buyer.id || buyer.ally !== emperor.id) return false;
g.pendingEmperorAuctionGift = {event:crypto.randomUUID(), owner:emperor.id, recipient:buyer.id,
turn:g.turn, source:'richese', quote:structuredClone(quote), signature:''};
g.pendingEmperorAuctionGift.signature = emperorAuctionGiftSignature(g);
return true;
}
/** The public alliance and winning seat alone determine this first refusal.
* Only auctionNext creates it: a restored payment never reopens the offer. */
function beginEmperorAuctionGift(g: Game) {
if (g.status !== 'playing' || g.phase !== 3 ||
(g.advanced ? g.expansions.length !== 0 || g.players.some(seat => !['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction))
: g.expansions.length > 0 && !selectedPhaseModuleRoster(g)) ||
g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || g.nexusCards ||
g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || (g.advanced && g.nexusCards) ||
g.discoveryEnabled || g.sandtrout || g.coalitionState || g.pendingEmperorAuctionGift ||
g.response || g.phaseOpening || g.truthtrance || g.pendingKarama ||
g.pendingTreacheryDiscard || g.pendingRicheseGift || g.pendingNullentropy ||
@@ -18968,8 +19000,12 @@ function emperorAuctionGiftIntegrity(g: Game) {
requireRule(g.status === 'playing' && g.phase === 3 && g.turn === pending.turn &&
typeof pending.event === 'string' && !!pending.event && owner?.faction === 'emperor' && recipient &&
owner.ally === recipient.id && recipient.ally === owner.id &&
decision?.kind === 'auctionPayment' && decision.player === recipient.id &&
g.auction?.bidder === recipient.id && pending.signature === emperorAuctionGiftSignature(g),
(pending.source === 'richese'
? !decision && !g.auction && g.richeseAuction?.source === 'cache' && !!pending.quote &&
pending.quote.winner === recipient.id && JSON.stringify(pending.quote) === JSON.stringify(richeseSettlementQuote(g))
: pending.source === undefined && pending.quote === undefined &&
decision?.kind === 'auctionPayment' && decision.player === recipient.id && g.auction?.bidder === recipient.id) &&
pending.signature === emperorAuctionGiftSignature(g),
'The Emperor auction gift lost its original payment opportunity.');
const response = parked?.response ?? (g.pendingKarama?.use.kind === 'cancel'
? g.pendingKarama.use.response : g.response);
@@ -44636,6 +44672,13 @@ function applyActionInner(
requireRule(Object.keys(action).sort().join(',') === 'event,type' && action.event === pending.event,
'This Emperor auction opportunity is no longer current.');
g.pendingEmperorAuctionGift = null;
if (pending.source === 'richese') {
requireRule(pending.quote, 'The Richese auction gift lost its committed sale.');
settleRicheseSoldLot(g,pending.quote,false);
return g;
}
if (!g.advanced && g.nexusCards && !beginNexusEmperorPayment(g))
recoverAuctionPayment(g, getPlayer(g, pending.recipient));
return g;
}
requireRule(t === 'emperorGift' && Object.keys(action).sort().join(',') === 'amount,type',
@@ -50122,6 +50165,7 @@ export function viewGame(state: Game, id: string, now?: number) {
? { owner: g.pendingEmperorAuctionGift.owner } : null,
emperorAuctionGift: g.pendingEmperorAuctionGift?.owner === id && !g.response && !g.pendingKarama
? { event: g.pendingEmperorAuctionGift.event, recipient: g.pendingEmperorAuctionGift.recipient,
...(g.pendingEmperorAuctionGift.source ? {source:g.pendingEmperorAuctionGift.source} : {}),
maximum: emperorAuctionGiftMaximum(g, me) } : null,
coalitionEmperor: (() => {
const group = coalitionEmperorGroup(g, me);