Resume Richese auctions after Distrans Semuta reaction
Verify and publish checkpoint container / container (push) Successful in 11m24s

This commit is contained in:
Quintine committed 2026-10-29 10:38:59 +11:00
1 parent ccc4c7bb40
commit f8078ed5e2
9 files changed
+245 -56

No files matched your search

+28 -4
View File
@@ -1734,6 +1734,8 @@ export type Game = {
}
| {
kind: 'distransDiscard';
/** Present on new special-auction reactions; older saves omit it. */
auctionBinding?: string;
owner: string;
recipient: string;
transferred: string;
@@ -5485,9 +5487,17 @@ function distransBlock(g: Game, owner: Player, card: Card): string | null {
special.acted.length > 0 || Object.keys(special.sealed).length > 0));
if (started && (g.response || g.decision || g.pendingKarama || g.phaseOpening || g.pendingTreacheryDiscard))
return 'Finish the pending auction interaction before playing Distrans.';
// A special-lot Semuta continuation still needs an auction-bound resume.
if (started && special && (g.semutaPreview || g.semutaAdvancedOrdinaryPreview || g.semutaRecruitsPreview))
return 'Distrans between special-auction bids is unavailable with the Semuta preview.';
if (started && special && (g.semutaPreview || g.semutaAdvancedOrdinaryPreview || g.semutaRecruitsPreview)) {
const auctionBinding = distransSpecialAuctionBinding(g);
const continuation: DistransDiscardContinuation = {
kind: 'distransDiscard', owner: owner.id, recipient: owner.id, transferred: '', card: card.id,
auctionBinding, resume: { response: g.response, decision: g.decision,
pendingKarama: g.pendingKarama, phaseOpening: g.phaseOpening }, stateSignature: '',
};
if (!auctionBinding || !semutaOfferSupported(g, continuation,
[{ card, discardedBy: owner.id, publicFace: true }]))
return 'Finish the special auction before playing Distrans in this Semuta interaction.';
}
return distransAuctionBlock(g);
}
function validateDistrans(
@@ -6443,6 +6453,14 @@ type DistransDiscardContinuation = Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
{ kind: 'distransDiscard' }
>;
/** Preserve the physical special lot and its committed funds through Semuta. */
function distransSpecialAuctionBinding(g: Game): string | undefined {
if (g.phase !== 3 || !g.richeseAuction || g.auction || g.ixAuction || g.currentAuctionSale ||
distransAuctionBlock(g)) return undefined;
return JSON.stringify({ turn: g.turn, phase: g.phase, active: g.active, order: g.order,
auction: g.richeseAuction, bidding: g.richeseBidding, funding: g.richeseFunding,
aid: g.aid, players: g.players.map(p => ({ id: p.id, spice: p.spice, ally: p.ally })) });
}
function distransDiscardSignature(g: Game, c: DistransDiscardContinuation) {
const owner = getPlayer(g, c.owner), recipient = getPlayer(g, c.recipient);
return JSON.stringify({
@@ -6451,6 +6469,7 @@ function distransDiscardSignature(g: Game, c: DistransDiscardContinuation) {
turn: g.turn, phase: g.phase, active: g.active,
ownerHand: owner.hand.map(card => card.id),
recipientHand: recipient.hand.map(card => card.id),
...(c.auctionBinding === undefined ? {} : { auctionBinding: c.auctionBinding }),
});
}
type SaphoTruthOrderDiscardContinuation = Extract<
@@ -8338,6 +8357,7 @@ function treacheryDiscardIntegrity(g: Game) {
!g.discard.some(card => card.id === c.transferred) &&
!c.resume.response && !c.resume.decision &&
!c.resume.pendingKarama && !c.resume.phaseOpening &&
(c.auctionBinding === undefined || c.auctionBinding === distransSpecialAuctionBinding(g)) &&
c.stateSignature === distransDiscardSignature(g, c),
'The completed private Distrans transfer no longer matches its fresh discard.',
);
@@ -9703,6 +9723,9 @@ function semutaOfferSupported(
!!continuation.richeseLotEvent && continuation.richeseLotEvent === g.richeseAuction?.event;
const richeseBoxDiscard = supportedRicheseTruthLot(g) && continuation.kind === 'nullentropyDiscard' &&
continuation.auctionBinding?.kind === 'richese';
const richeseDistransDiscard = continuation.kind === 'distransDiscard' &&
continuation.auctionBinding !== undefined &&
continuation.auctionBinding === distransSpecialAuctionBinding(g);
if (continuation.kind === 'truthtranceDiscard' &&
['bribe', 'bribeExpression', 'mixedBribe'].includes(continuation.record.question.kind) &&
(!continuation.bribeBinding || !bareBasicSemutaProfile(g) || !bribePromiseModeSupported(g))) return false;
@@ -10129,7 +10152,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 || richeseBoxDiscard || !g.richeseAuction) &&
continuation.use.response.kind === 'emperorIncome') || 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 &&
@@ -44258,6 +44281,7 @@ function applyActionInner(
g.semutaRecruitsPreview && basicIndependentRecruitsSemutaProfile(g)) && !g.pendingTreacheryDiscard) {
const continuation: DistransDiscardContinuation = {
kind: 'distransDiscard', owner: p.id, recipient: recipient.id,
...(distransSpecialAuctionBinding(g) === undefined ? {} : { auctionBinding: distransSpecialAuctionBinding(g) }),
transferred: result.transferred.id, card: result.discarded.id,
resume: { response: g.response, decision: g.decision,
pendingKarama: g.pendingKarama, phaseOpening: g.phaseOpening },
+1 -1
View File
@@ -763,7 +763,7 @@ export const RULE_TOPICS: RuleTopic[] = [
steps: [
'Any faction holding Distrans may use it. Choose another player whose hand has room and a different Treachery Card from your own hand. The recipient need not be your ally. Transfer that card intact, then discard Distrans into the ordinary discard pile.',
'Open the Distrans transfer panel in your hand, select the recipient and card, inspect either card, then press Give and discard Distrans. There is no spice payment, purchase bonus, recipient acknowledgement or faction-power Karama response. Other players see the played Distrans, not the transferred card identity.',
'The printed exception is during a bid. Provisionally, supported ordinary, Once Around, Silent and normal Black Market lots allow Distrans between completed bid, pass or sealed-submission actions. The lot, eligibility, commitments and bidding order stay the same. Pending auction interactions restrict play; started special lots also remain blocked while the Semuta preview is enabled. A completed purchase income response may allow use. The exact scope awaits official clarification; giving Distrans itself remains guarded.',
'The printed exception is during a bid. Provisionally, supported ordinary, Once Around, Silent and normal Black Market lots allow Distrans between completed bid, pass or sealed-submission actions. The lot, eligibility, commitments and bidding order stay the same. Pending auction interactions restrict play; supported Semuta claim or pass preserves the special lot and its commitments. A completed purchase income response may allow use. The exact scope awaits official clarification; giving Distrans itself remains guarded.',
'Current hand space still controls bidding after a transfer. A full donor can free space; if the active bidder receives its final card, it must pass instead of raising. The existing auction skips full hands without adding a lot or resetting earlier passes.',
'Distrans resolves in one server action and can keep supported existing responses, decisions or phase openings in place; pending auction interactions restrict the started-lot window. Active Truthtrance retains priority. Both outgoing cards must be uncommitted; a sealed plan, fixed prescience answer, pending Richese gift or accepted replacement cannot lose its reserved card.',
'A transfer must preserve binding battle answers, compulsory discards and both sides of a pending Harkonnen hand exchange. The recipient must retain room for its mandatory incoming return. Optional proposals that become unavailable can still finish through their existing decline or no-effect path.',