Route Advanced Emperor allied auction funding
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 02:48:11 +11:00
1 parent 3ba9087405
commit 599af2475d
19 files changed
+883 -35

No files matched your search

+79 -18
View File
@@ -1,5 +1,6 @@
import { createTwoPlayerControllers, validateTwoPlayerControllersLobby, validateTwoPlayerControllersComplete,
addTwoPlayerControlledSeat, TwoPlayerControllersError, type TwoPlayerControllers } from './two-player-controllers';
import { routeAuctionPayment, type AuctionPaymentLeg } from './auction-payment-routing';
import { createFutureBattleLeader, bindFutureBattleLeader, settleFutureBattleLeader, releaseFutureBattleLeader,
expireFutureBattleLeader, futureBattleLeaderTiming, futureBattleLeaderWindow, type FutureBattleLeaderReceipt } from './future-battle-leader';
import { createFactionDraft, validateFactionDraft, currentFactionDrafter, chooseFaction,
@@ -2098,6 +2099,8 @@ export type Game = {
recipient?: 'bank';
/** Actual Richese donor share of another buyer's lot; paid to Emperor or bank. */
richeseContribution?: number;
/** New Advanced invoices preserve each actual destination through responses. */
paymentRouting?: AuctionPaymentLeg[];
} | null;
pendingAmbassador?: {
/** Fresh entries retain their actual arrival source; older frames omit it. */
@@ -11366,7 +11369,8 @@ function spiceBankerIncomeIntegrity(g: Game): void {
const payment = carrier?.spiceBankerIncomePayment;
if (!payment) {
requireRule(!carrier || ('winner' in carrier ? carrier.free ||
(carrier.recipient !== 'bank' && auctionEmperorIncomeAmount(carrier) === 0) :
(carrier.recipient !== 'bank' && auctionEmperorIncomeAmount(carrier) === 0 &&
!carrier.paymentRouting?.some(leg => leg.recipient === 'bank')) :
carrier.kind !== 'guildIncome' &&
!(carrier.kind === 'revivalIncome' && carrier.recipient !== carrier.owner && (carrier.amount ?? 0) > 1)),
'The original paid transaction lost its native bank invoice.');
@@ -11379,6 +11383,13 @@ function spiceBankerIncomeIntegrity(g: Game): void {
new Set(payment.legs.map(leg => leg.payer)).size === payment.legs.length,
'The original paid bank transaction changed its event or contribution legs.');
if (carrier && 'winner' in carrier) {
if (carrier.paymentRouting) {
const expected = carrier.paymentRouting.filter(leg => leg.recipient !== 'seller')
.map(leg => ({payer:leg.payer, amount:leg.amount, recipient:leg.recipient === 'emperor' ? 'player' : 'bank'}));
requireRule(payment.kind === 'auction' && !carrier.free && JSON.stringify(payment.legs) === JSON.stringify(expected),
'The deferred bank invoice no longer matches its original auction payer legs.');
continue;
}
const amount = carrier.recipient === 'bank' ? carrier.amount : auctionEmperorIncomeAmount(carrier);
const contributionOnly = carrier.origin !== 'normal' && carrier.seller !== carrier.winner && carrier.recipient !== 'bank';
requireRule(payment.kind === 'auction' && !carrier.free &&
@@ -11590,7 +11601,18 @@ function bureaucratPaymentIntegrity(g: Game) {
requireRule(g.leaderSkills && bureaucratPaymentModeSupported(g) && carrier.bureaucratPaymentEvent === source.event,
'The saved Bureaucrat payment lost its module or independent event.');
validateBureaucratSource(g,source);
if ('winner' in carrier) requireRule(source.kind === 'auction' && !carrier.free &&
if ('winner' in carrier && carrier.paymentRouting) {
const sellerIncome = carrier.origin !== 'normal' && carrier.seller !== carrier.winner;
const shares = carrier.paymentRouting.filter(leg => leg.recipient === (sellerIncome ? 'seller' : 'emperor'))
.map(leg => ({payer:leg.payer,amount:leg.amount}));
requireRule(source.kind === 'auction' && !carrier.free &&
source.payee === (sellerIncome ? carrier.seller : byFaction(g,'emperor')?.id) &&
source.amount === shares.reduce((sum,share) => sum + share.amount,0) &&
shares.some(share => share.payer === source.payer) &&
(!source.contributions || JSON.stringify(source.contributions) === JSON.stringify(shares)) &&
(sellerIncome ? source.richeseSaleAmount === carrier.amount : source.richeseSaleAmount === undefined),
'The Bureaucrat source no longer matches its paid auction sale.');
} else if ('winner' in carrier) requireRule(source.kind === 'auction' && !carrier.free &&
(source.richeseSaleAmount !== undefined
? g.advanced && carrier.origin !== 'normal' && carrier.seller !== carrier.winner &&
(carrier.richeseContribution ?? 0) > 0 && source.richeseSaleAmount === carrier.amount &&
@@ -15414,15 +15436,25 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
if (!confirmed && beginNexusEmperorSellerPurchase(g, quote, bank)) return;
const lot = g.richeseAuction!, owner = getPlayer(g,lot.owner);
const winner = getPlayer(g, quote.winner), card = quote.card;
const emperor = byFaction(g, 'emperor');
const paymentRouting = !free && g.advanced && emperor && winner.ally === emperor.id && quote.allyPayment > 0
? routeAuctionPayment({advanced:true, free:false, price:quote.amount, buyer:winner.id,
emperor:emperor.id, seller:owner.id, bankOnly:bank, contributions:[
...(quote.ownPayment > 0 ? [{payer:winner.id,amount:quote.ownPayment}] : []),
{payer:emperor.id,amount:quote.allyPayment},
]}) : undefined;
const richeseContribution = !free && g.advanced && winner.id !== owner.id && winner.ally === owner.id
? quote.allyPayment : 0;
const bankAmount = free ? 0 : bank || winner.id === owner.id ? quote.amount : richeseContribution;
const emperor = byFaction(g, 'emperor');
const incomePayment = g.spiceBankerIncomePreview && bankAmount > 0 ? bankerIncomePayment(g, 'auction',
const routedBankLegs = paymentRouting?.filter(leg => leg.recipient !== 'seller').map(leg => ({
payer:leg.payer, amount:leg.amount, recipient:leg.recipient === 'emperor' ? 'player' as const : 'bank' as const,
}));
const incomePayment = g.spiceBankerIncomePreview && (routedBankLegs?.length || bankAmount > 0) ? bankerIncomePayment(g, 'auction',
routedBankLegs ?? (
bank || winner.id === owner.id
? [{payer:winner.id,amount:quote.ownPayment,recipient:bank || !emperor ? 'bank' : 'player'},
...(quote.allyPayment ? [{payer:winner.ally!,amount:quote.allyPayment,recipient:bank || !emperor ? 'bank' as const : 'player' as const}] : [])]
: [{payer:owner.id,amount:richeseContribution,recipient:emperor ? 'player' : 'bank'}]) : undefined;
: [{payer:owner.id,amount:richeseContribution,recipient:emperor ? 'player' : 'bank'}])) : undefined;
if (!free) payWithAlly(g, winner, quote.amount, quote.allyPayment);
g.richeseFunding = {};
const retained = lot.source === 'blackMarket' && winner.id === owner.id;
@@ -15434,10 +15466,14 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
}
g.currentAuctionSale = {
...(g.homeworldOccupationPreview ? { occupiedEvent: lot.event,
occupiedContributions: free ? [] : [{ payer: winner.id, amount: quote.ownPayment },
...(quote.allyPayment ? [{ payer: winner.ally!, amount: quote.allyPayment }] : [])] } : {}),
occupiedContributions: free ? [] : paymentRouting
? paymentRouting.filter(leg => leg.recipient === (winner.id === owner.id ? 'emperor' : 'seller'))
.map(({payer,amount}) => ({payer,amount}))
: [{ payer: winner.id, amount: quote.ownPayment },
...(quote.allyPayment ? [{ payer: winner.ally!, amount: quote.allyPayment }] : [])] } : {}),
...(richeseContribution ? {richeseContribution} : {}),
...(incomePayment ? {spiceBankerIncomePayment:incomePayment} : {}),
...(paymentRouting ? {paymentRouting} : {}),
winner: winner.id,
amount: quote.amount,
free,
@@ -15446,7 +15482,8 @@ function settleRicheseSoldLot(g: Game, quote: Extract<RicheseSettlementQuote,{ki
...(bank ? {recipient:'bank' as const} : {}),
...(!free ? stampAuctionBureaucrat(g,winner,quote.amount,quote.allyPayment,owner.id) : {}),
};
if (bank || !emperor) awardBankerIncome(g, incomePayment);
if (bank || !emperor || paymentRouting && auctionEmperorIncomeAmount(g.currentAuctionSale) === 0)
awardBankerIncome(g, incomePayment);
log(
g,
`${winner.name} ${retained ? 'retained their own Black Market card under provisional paid self-purchase' : `bought ${lot.source === 'cache' ? card.name : 'the concealed Black Market card'}`} for ${quote.amount} spice.${free ? ' Emperor Nexus retained the proven price; no seller or bank payment occurred.' : ' The funded bid was paid automatically.'}`,
@@ -16318,9 +16355,16 @@ function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): bo
if (!group || group.members.length < 3) return false;
const groupHasUnsettled = (records: readonly {player: string; fulfilled?: boolean; released?: boolean}[] | undefined) =>
!!records?.some(record => group.members.includes(record.player) && !record.fulfilled && !record.released);
// A current No bribe answer only constrains later ordinary bribes. Moving
// spice into alliance escrow or paying a bid/shipment does not satisfy it.
// Positive and stale answers still need their funding preserved separately.
const groupHasFundedBribe = (records: readonly {player: string; turn: number; answer: boolean;
fulfilled?: boolean; released?: boolean}[] | undefined) => !!records?.some(record =>
group.members.includes(record.player) && !record.fulfilled && !record.released &&
(record.turn !== g.turn || record.answer));
return committedBid || ![g.bidPromises, g.phaseBidPromises, g.turnActionPromises,
g.revivalPromises, g.leaderRevivalPromises, g.revivalExpressionPromises,
g.bribePromises, g.bribeExpressionPromises].some(groupHasUnsettled);
g.revivalPromises, g.leaderRevivalPromises, g.revivalExpressionPromises].some(groupHasUnsettled) &&
!groupHasFundedBribe(g.bribePromises) && !groupHasFundedBribe(g.bribeExpressionPromises);
}
function coalitionEmperorGroup(g: Game, p: Player) {
if (!g.coalitionState || p.faction !== 'emperor' || g.status !== 'playing' ||
@@ -17037,9 +17081,11 @@ function payWithAlly(g: Game, p: Player, cost: number, allyPayment: number) {
if (credit) credit.amount -= allyPayment;
}
function auctionBureaucratParties(g:Game,p:Player,amount:number,allyPayment:number,seller?:string) {
const split = g.advanced && allyPayment > 0 && seller !== undefined && seller !== p.id && p.ally === seller;
const emperorBank = g.advanced && allyPayment > 0 && p.ally === byFaction(g,'emperor')?.id;
const split = g.advanced && allyPayment > 0 && seller !== undefined && seller !== p.id &&
(p.ally === seller || emperorBank);
return {payee:seller && seller !== p.id ? seller : byFaction(g,'emperor')?.id,
shares:(split ? [{payer:p.id,amount:amount-allyPayment}]
shares:(split || emperorBank ? [{payer:p.id,amount:amount-allyPayment}]
: [{payer:p.id,amount:amount-allyPayment},...(allyPayment ? [{payer:p.ally!,amount:allyPayment}] : [])]).filter(share => share.amount > 0),
richeseSaleAmount:split ? amount : undefined};
}
@@ -18085,6 +18131,17 @@ function auctionNext(g: Game) {
function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false) {
const a = g.auction!;
const winner = getPlayer(g, a.bidder!);
const emperor = byFaction(g, 'emperor');
const donors = a.coalitionContributions ?? (a.allyPayment && winner.ally
? [{donor:winner.ally, amount:a.allyPayment}] : []);
const donorTotal = donors.reduce((sum, leg) => sum + leg.amount, 0);
const paymentRouting = !free && g.advanced && emperor &&
donors.some(leg => leg.donor === emperor.id && leg.amount > 0)
? routeAuctionPayment({advanced:true, free:false, price:a.bid, buyer:winner.id,
emperor:emperor.id, seller:null, contributions:[
...(a.bid > donorTotal ? [{payer:winner.id, amount:a.bid-donorTotal}] : []),
...donors.map(leg => ({payer:leg.donor, amount:leg.amount})),
]}) : undefined;
if (free) delete a.coalitionContributions;
else if (a.coalitionContributions !== undefined) {
coalitionBidPaymentQuote(g, winner, a.bid, a.coalitionContributions, true);
@@ -18096,18 +18153,21 @@ function settleAuction(g: Game, free = false, automatic = false, emperorNexus =
g.coalitionEscrow = {...g.coalitionEscrow!, pledges:result.pledges};
} else payWithAlly(g, winner, a.bid, a.allyPayment ?? 0);
winner.hand.push(a.cards[a.index]);
const emperor = byFaction(g, 'emperor');
const paysEmperor = !!emperor && emperor.id !== winner.id;
const incomePayment = !free && g.spiceBankerIncomePreview ? bankerIncomePayment(g, 'auction', [
const incomePayment = !free && g.spiceBankerIncomePreview ? bankerIncomePayment(g, 'auction', paymentRouting
? paymentRouting.map(leg => ({payer:leg.payer,amount:leg.amount,
recipient:leg.recipient === 'emperor' ? 'player' as const : 'bank' as const})) : [
{payer: winner.id, amount: a.bid - (a.allyPayment ?? 0), recipient: paysEmperor ? 'player' : 'bank'},
...(a.allyPayment ? [{payer: winner.ally!, amount: a.allyPayment,
recipient: paysEmperor ? 'player' as const : 'bank' as const}] : []),
]) : undefined;
g.currentAuctionSale = {
...(g.homeworldOccupationPreview ? { occupiedEvent: crypto.randomUUID(),
occupiedContributions: [{ payer: winner.id, amount: free ? 0 : a.bid - (a.allyPayment ?? 0) },
...(!free && a.allyPayment ? [{ payer: winner.ally!, amount: a.allyPayment }] : [])] } : {}),
occupiedContributions: paymentRouting ? paymentRouting.filter(leg => leg.recipient === 'emperor')
.map(({payer,amount}) => ({payer,amount})) : [{ payer: winner.id, amount: free ? 0 : a.bid - (a.allyPayment ?? 0) },
...(!free && a.allyPayment ? [{ payer: winner.ally!, amount: a.allyPayment }] : [])] } : {}),
...(incomePayment ? {spiceBankerIncomePayment: incomePayment} : {}),
...(paymentRouting ? {paymentRouting} : {}),
winner: winner.id,
amount: a.bid,
free,
@@ -18115,7 +18175,7 @@ function settleAuction(g: Game, free = false, automatic = false, emperorNexus =
seller: null,
...(!free ? stampAuctionBureaucrat(g,winner,a.bid,a.allyPayment ?? 0) : {}),
};
if (!paysEmperor) awardBankerIncome(g, incomePayment);
if (!paysEmperor || auctionEmperorIncomeAmount(g.currentAuctionSale) === 0) awardBankerIncome(g, incomePayment);
log(
g,
`${faction(winner.faction).name} won a treachery card for ${emperorNexus ? `the Emperor Nexus card after proving ${a.bid} spice` : free ? 'a Karama' : `${a.bid} spice`}.${automatic ? ' The declared spice payment was the only available payment method.' : ''}`,
@@ -18205,7 +18265,8 @@ function commitAuctionContinuation(g: Game, quote: AuctionContinuationQuote,bure
const sale = g.currentAuctionSale!, received = step.amount - (bureaucratDiversion ?? 0);
const source: OccupiedPercentageSource = { kind: 'richese-income', event: crypto.randomUUID(),
native: seller.id, amount: received, payment: { id: sale.occupiedEvent!,
binding: occupiedProducerProof(g, 'richese-income'), paid: sale.amount,
binding: occupiedProducerProof(g, 'richese-income'),
paid: sale.paymentRouting ? sale.occupiedContributions!.reduce((sum,leg) => sum + leg.amount,0) : sale.amount,
contributions: sale.occupiedContributions!, recipient: seller.id, received } };
if (admitOccupiedPercentage(g, source, received, { kind: 'auction', quote: { ...quote, steps: [] } })) return;
}