Support Semuta after Box searches during bidding

This commit is contained in:
Quintine committed 2026-10-28 20:40:07 +11:00
1 parent 7dfdf8d593
commit 6bf165a6aa
8 files changed
+312 -2

No files matched your search

+25 -2
View File
@@ -1647,6 +1647,8 @@ export type Game = {
finalDiscardSignature: string;
resume: NonNullable<Game['pendingNullentropy']>['resume'];
parentSignature: string;
/** New Basic bidding reactions bind the existing lot and funding. */
auctionBinding?: { kind: 'ordinary' | 'richese'; signature: string };
}
| {
kind: 'ordinaryCardDiscard';
@@ -5115,6 +5117,7 @@ function finishNullentropy(
`${owner.name} recovered one private card with Nullentropy Box, shuffled the remaining discard pile, and placed the used Box on top. The bank was paid when the search began.`,
{ faction: owner.faction, name: 'Nullentropy Box' },
);
const auctionBinding = nullentropyAuctionBinding(g);
stageTreacheryDiscard(
g,
'nullentropyBox',
@@ -5129,6 +5132,7 @@ function finishNullentropy(
finalDiscardSignature: JSON.stringify(g.discard),
resume: pending.resume,
parentSignature: nullentropyParentSignature(g, pending.resume, event),
...(auctionBinding ? {auctionBinding} : {}),
},
);
}
@@ -6157,6 +6161,18 @@ function nullentropyParentSignature(
);
return JSON.stringify({ searchEvent, resume, parents });
}
/** Clock state is deliberately excluded: it pauses and resumes while decisions
* are pending. The lot and committed resources themselves remain unchanged. */
function nullentropyAuctionBinding(g: Game): {kind:'ordinary'|'richese';signature:string} | undefined {
if (!g.semutaPreview || !bareBasicSemutaProfile(g) || g.phase !== 3 || g.currentAuctionSale || g.ixAuction)
return undefined;
const kind = g.auction && !g.richeseAuction ? 'ordinary' : supportedRicheseTruthLot(g) ? 'richese' : undefined;
if (!kind) return undefined;
return {kind, signature: JSON.stringify({turn:g.turn,phase:g.phase,active:g.active,order:g.order,
auction:g.auction,richeseAuction:g.richeseAuction,richeseBidding:g.richeseBidding,
richeseFunding:g.richeseFunding,aid:g.aid,
players:g.players.map(p => ({id:p.id,spice:p.spice,ally:p.ally}))})};
}
type OrdinaryDiscardContinuation = Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
{ kind: 'ordinaryCardDiscard' }
@@ -7929,6 +7945,10 @@ function treacheryDiscardIntegrity(g: Game) {
nullentropyParentSignature(g, c.resume, c.searchEvent),
'The completed Box search does not match its suspended controls and obligations.',
);
if (c.auctionBinding) requireRule(
JSON.stringify(c.auctionBinding) === JSON.stringify(nullentropyAuctionBinding(g)),
'The completed Box search no longer matches its original auction and funding.',
);
suspendedControlsIntegrity(g, c.resume);
} else if (continuation?.kind === 'ordinaryCardDiscard') {
const c = continuation,
@@ -9222,10 +9242,13 @@ function semutaOfferSupported(
// native Distrans still cannot produce a transfer after that bid starts.
const ordinaryAuctionDiscard = g.phase === 3 && !!g.auction && !g.richeseAuction && !g.currentAuctionSale &&
(continuation.kind === 'truthtranceDiscard' || continuation.kind === 'distransDiscard' ||
continuation.kind === 'saphoTruthOrderDiscard');
continuation.kind === 'saphoTruthOrderDiscard' ||
continuation.kind === 'nullentropyDiscard' && continuation.auctionBinding?.kind === 'ordinary');
const richeseTruthDiscard = supportedRicheseTruthLot(g) &&
(continuation.kind === 'truthtranceDiscard' || continuation.kind === 'saphoTruthOrderDiscard') &&
!!continuation.richeseLotEvent && continuation.richeseLotEvent === g.richeseAuction?.event;
const richeseBoxDiscard = supportedRicheseTruthLot(g) && continuation.kind === 'nullentropyDiscard' &&
continuation.auctionBinding?.kind === 'richese';
if (continuation.kind === 'truthtranceDiscard' &&
['bribe', 'bribeExpression', 'mixedBribe'].includes(continuation.record.question.kind) &&
(!continuation.bribeBinding || !bareBasicSemutaProfile(g) || !!g.kullPreview)) return false;
@@ -9562,7 +9585,7 @@ 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 || !g.richeseAuction) &&
continuation.use.response.kind === 'emperorIncome') || richeseTruthDiscard || richeseBoxDiscard || !g.richeseAuction) &&
(richeseGiftDiscard || combatDiscard || alliedTraitorDiscard || occupyDiscard || continuation.kind === 'saphoAggressorDiscard' ||
bgDiscard ||
continuation.kind === 'specialKaramaDiscard' ||