Support staged group bid funding
Verify and publish checkpoint container / container (push) Successful in 10m27s
Verify and publish checkpoint container / container (push) Successful in 10m27s
This commit is contained in:
1 parent
34aa64cdef
commit
1eeeb32566
12 files changed
+426
-37
No files matched your search
+70
-15
@@ -1008,6 +1008,8 @@ export type Auction = {
|
||||
passed: string[];
|
||||
opener: number;
|
||||
allyPayment?: number;
|
||||
/** Private donor-specific shares of the current high bid. */
|
||||
coalitionContributions?: CoalitionContribution[];
|
||||
};
|
||||
export type Decision =
|
||||
| { kind: 'ecazBattleLead'; player: string; event: string; choices: string[]; lowEcaz?: true }
|
||||
@@ -16047,21 +16049,29 @@ function applyCoalitionTransition(g: Game, result: CoalitionTransition) {
|
||||
}
|
||||
/** Initial staged group-funding scope: ordinary Basic planet shipments, with
|
||||
* Guild outside the group. Guild-allied rate and other purchases remain separate. */
|
||||
function coalitionShipmentFundingAvailable(g: Game, p: Player): boolean {
|
||||
if (!g.coalitionEscrow || !g.coalitionState || g.status !== 'playing' || g.advanced || g.phase !== 5 ||
|
||||
function coalitionFundingAvailable(g: Game, p: Player, committedBid = false): boolean {
|
||||
if (!g.coalitionEscrow || !g.coalitionState || g.status !== 'playing' || g.advanced || ![3, 5].includes(g.phase) ||
|
||||
g.expansions.length || g.homeworlds || g.nexusCards || g.leaderSkills || g.techTokens ||
|
||||
g.strongholdCards || g.ecazTreachery || g.discoveryEnabled || g.karamaShipping ||
|
||||
g.spiceBankerIncomePreview) return false;
|
||||
g.spiceBankerIncomePreview || g.kullPreview || g.richeseAuction || g.ixAuction ||
|
||||
(!committedBid && (g.bidPromises?.length || g.phaseBidPromises?.length || g.turnActionPromises?.length ||
|
||||
g.revivalPromises?.length || g.bribePromises?.length))) return false;
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, p.id));
|
||||
const guild = byFaction(g, 'guild');
|
||||
return group?.members.length === 3 && (!guild || !group.members.includes(guild.id));
|
||||
return group?.members.length === 3 && (g.phase === 3 || !guild || !group.members.includes(guild.id));
|
||||
}
|
||||
function coalitionShipmentQuote(g: Game, p: Player, cost: number, contributions: CoalitionContribution[]) {
|
||||
requireRule(coalitionShipmentFundingAvailable(g, p), 'Group funding is unavailable for this shipment.');
|
||||
requireRule(g.phase === 5 && coalitionFundingAvailable(g, p), 'Group funding is unavailable for this shipment.');
|
||||
return coalitionRule(() => quoteCoalitionFunding({seats:g.players, coalitionState:g.coalitionState,
|
||||
authorizations:g.coalitionEscrow!.authorizations, pledges:g.coalitionEscrow!.pledges,
|
||||
purpose:'shipment', payer:p.id, cost, ownAvailable:uncommittedSpice(g, p), contributions}));
|
||||
}
|
||||
function coalitionBidPaymentQuote(g: Game, p: Player, cost: number, contributions: CoalitionContribution[], committed = false) {
|
||||
requireRule(g.phase === 3 && coalitionFundingAvailable(g, p, committed), 'Group funding is unavailable for this bid.');
|
||||
return coalitionRule(() => quoteCoalitionFunding({seats:g.players, coalitionState:g.coalitionState,
|
||||
authorizations:g.coalitionEscrow!.authorizations, pledges:g.coalitionEscrow!.pledges,
|
||||
purpose:'bid', payer:p.id, cost, ownAvailable:p.spice, contributions}));
|
||||
}
|
||||
/** The original native reciprocal quote owns identity and pairing legality. */
|
||||
function normalNexusAllianceQuote(g: Game, p: Player, target: string | null) {
|
||||
if (target !== null) {
|
||||
@@ -16687,7 +16697,9 @@ function uncommittedSpice(g: Game, p: Player) {
|
||||
richeseOwnCommitment(g.richeseAuction, g.richeseFunding ?? {}, p.id) -
|
||||
// A current sale already consumed the bid; only an unpaid bid stays reserved.
|
||||
(g.auction?.bidder === p.id && !g.currentAuctionSale
|
||||
? g.auction.bid - (g.auction.allyPayment ?? 0)
|
||||
? g.auction.bid - (g.auction.coalitionContributions
|
||||
? g.auction.coalitionContributions.reduce((sum, share) => sum + share.amount, 0)
|
||||
: (g.auction.allyPayment ?? 0))
|
||||
: 0) -
|
||||
(battleSupportCost(g, p, g.battle?.plans[p.id]?.support ?? 0) -
|
||||
(g.battle?.plans[p.id]?.allyPayment ?? 0)) -
|
||||
@@ -16850,6 +16862,15 @@ function normalBidQuote(g: Game, p: Player, amount: unknown, requestedAid?: unkn
|
||||
checkBureaucratContributors(g, parties.payee, parties.shares);
|
||||
return { amount: quote.amount, allyPayment };
|
||||
}
|
||||
function coalitionBidQuote(g: Game, p: Player, amount: unknown, contributions: CoalitionContribution[]) {
|
||||
requireRule(g.auction && coalitionFundingAvailable(g, p), 'Group funding is unavailable for this bid.');
|
||||
const capacity = normalAuctionHandBlock(p.hand.length, handLimit(p));
|
||||
requireRule(!capacity, capacity ?? 'Your Treachery hand is full.');
|
||||
const quote = quoteNormalAuctionBid({currentBid:g.auction.bid, amount, maximum:Number.MAX_SAFE_INTEGER});
|
||||
if (!quote.ok) throw new RuleError(quote.reason);
|
||||
coalitionBidPaymentQuote(g, p, quote.amount, contributions);
|
||||
return {amount:quote.amount, allyPayment:0};
|
||||
}
|
||||
function bidOpportunity(g: Game): BidOpportunity | null {
|
||||
const a = g.auction;
|
||||
if (g.status !== 'playing' || g.phase !== 3 || !a || g.currentAuctionSale ||
|
||||
@@ -17716,7 +17737,16 @@ function auctionNext(g: Game) {
|
||||
function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false) {
|
||||
const a = g.auction!;
|
||||
const winner = getPlayer(g, a.bidder!);
|
||||
if (!free) payWithAlly(g, winner, a.bid, a.allyPayment ?? 0);
|
||||
if (free) delete a.coalitionContributions;
|
||||
else if (a.coalitionContributions !== undefined) {
|
||||
coalitionBidPaymentQuote(g, winner, a.bid, a.coalitionContributions, true);
|
||||
const result = coalitionRule(() => settleCoalitionFunding({seats:g.players, coalitionState:g.coalitionState,
|
||||
authorizations:g.coalitionEscrow!.authorizations, pledges:g.coalitionEscrow!.pledges,
|
||||
purpose:'bid', payer:winner.id, cost:a.bid, ownAvailable:winner.spice,
|
||||
contributions:a.coalitionContributions!}));
|
||||
winner.spice -= result.quote.own;
|
||||
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;
|
||||
@@ -19388,6 +19418,10 @@ function completeKarama(g: Game, p: Player, use: KaramaUse, card?: Card) {
|
||||
function auctionSpicePaymentFunded(g: Game, p: Player) {
|
||||
const a = g.auction;
|
||||
if (!a || a.bidder !== p.id) return false;
|
||||
if (a.coalitionContributions !== undefined) {
|
||||
try { coalitionBidPaymentQuote(g, p, a.bid, a.coalitionContributions, true); return true; }
|
||||
catch (error) { if (error instanceof RuleError) return false; throw error; }
|
||||
}
|
||||
const allyPayment = a.allyPayment ?? 0;
|
||||
return (
|
||||
Number.isInteger(a.bid) &&
|
||||
@@ -19753,7 +19787,8 @@ function finishAutomaticDecision(g: Game): boolean {
|
||||
}
|
||||
function recoverAuctionPayment(g: Game, p: Player) {
|
||||
const a = g.auction!;
|
||||
if (a.bid <= p.spice + (aidFor(g, p)?.amount ?? 0) || karamaCard(g, p, undefined, 'auction-free-purchase')) {
|
||||
if ((a.coalitionContributions !== undefined ? auctionSpicePaymentFunded(g, p)
|
||||
: a.bid <= p.spice + (aidFor(g, p)?.amount ?? 0)) || karamaCard(g, p, undefined, 'auction-free-purchase')) {
|
||||
g.decision = { kind: 'auctionPayment', player: p.id };
|
||||
return;
|
||||
}
|
||||
@@ -19763,6 +19798,7 @@ function recoverAuctionPayment(g: Game, p: Player) {
|
||||
a.bid = 0;
|
||||
a.bidder = null;
|
||||
a.allyPayment = 0;
|
||||
delete a.coalitionContributions;
|
||||
a.passed = [];
|
||||
if (a.saphoOrder) {
|
||||
const first = a.saphoOrder.find(id => getPlayer(g,id).hand.length < handLimit(getPlayer(g,id)));
|
||||
@@ -44463,16 +44499,18 @@ function applyActionInner(
|
||||
return g;
|
||||
}
|
||||
if (t === 'coalitionPledge') {
|
||||
requireRule(coalitionShipmentFundingAvailable(g, p), 'Group shipment funding is unavailable now.');
|
||||
requireRule(coalitionFundingAvailable(g, p), 'Group funding is unavailable now.');
|
||||
const recipient = stringField(action.recipient);
|
||||
const amount = integer(action.amount, 0, Number.MAX_SAFE_INTEGER, 'Group pledge');
|
||||
const locked = g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === recipient
|
||||
? g.auction.coalitionContributions?.find(share => share.donor === id)?.amount ?? 0 : 0;
|
||||
const result = coalitionRule(() => setCoalitionPledge({ ...coalitionMembership(g),
|
||||
escrow:g.coalitionEscrow!, donor:id, recipient, amount, available:uncommittedSpice(g, p) }));
|
||||
escrow:g.coalitionEscrow!, donor:id, recipient, amount, available:uncommittedSpice(g, p), locked }));
|
||||
requireRule(Number.isSafeInteger(p.spice + result.walletDelta) && p.spice + result.walletDelta >= 0,
|
||||
'Group pledge exceeds the donor wallet.');
|
||||
p.spice += result.walletDelta;
|
||||
g.coalitionEscrow = result.escrow;
|
||||
log(g, `${p.name} updated group shipment funding for ${getPlayer(g, recipient).name}.`);
|
||||
log(g, `${p.name} updated group funding for ${getPlayer(g, recipient).name}.`);
|
||||
return g;
|
||||
}
|
||||
if (t === 'pledgeAid') {
|
||||
@@ -44553,13 +44591,23 @@ function applyActionInner(
|
||||
);
|
||||
const a = g.auction;
|
||||
if (t === 'bid') {
|
||||
const quotedBid = normalBidQuote(g, p, action.amount, action.allyPayment);
|
||||
let groupShares: CoalitionContribution[] | undefined;
|
||||
if (action.coalitionContributions !== undefined) {
|
||||
requireRule(Array.isArray(action.coalitionContributions) &&
|
||||
(action.allyPayment === undefined || action.allyPayment === 0),
|
||||
'Choose donor shares for this group bid.');
|
||||
groupShares = action.coalitionContributions as CoalitionContribution[];
|
||||
}
|
||||
const quotedBid = groupShares === undefined ? normalBidQuote(g, p, action.amount, action.allyPayment)
|
||||
: coalitionBidQuote(g, p, action.amount, groupShares);
|
||||
if (liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).some(promise => promise.answer) ||
|
||||
hasBribePromises(g, p.id, true)) {
|
||||
const trial = structuredClone(g);
|
||||
trial.auction!.bid = quotedBid.amount;
|
||||
trial.auction!.bidder = p.id;
|
||||
trial.auction!.allyPayment = quotedBid.allyPayment;
|
||||
if (groupShares === undefined) delete trial.auction!.coalitionContributions;
|
||||
else trial.auction!.coalitionContributions = groupShares.map(share => ({...share}));
|
||||
guardPromisedBid(trial, getPlayer(trial, p.id), action, quotedBid.amount);
|
||||
reconcileRevivalPromises(trial, { actor: p.id, action });
|
||||
reconcileBribePromises(trial, { actor: p.id, action });
|
||||
@@ -44567,6 +44615,8 @@ function applyActionInner(
|
||||
guardPromisedBid(g, p, action, quotedBid.amount);
|
||||
a.bid = quotedBid.amount;
|
||||
a.allyPayment = quotedBid.allyPayment;
|
||||
if (groupShares === undefined) delete a.coalitionContributions;
|
||||
else a.coalitionContributions = groupShares.map(share => ({...share}));
|
||||
a.bidder = id;
|
||||
a.passed = [];
|
||||
} else {
|
||||
@@ -46956,12 +47006,14 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
? g.aid[me.ally].amount
|
||||
: 0,
|
||||
},
|
||||
coalitionFunding: coalitionShipmentFundingAvailable(g, me) ? (() => {
|
||||
coalitionFunding: coalitionFundingAvailable(g, me) ? (() => {
|
||||
const group = coalitionRule(() => coalitionOf(g.players, g.coalitionState, id))!;
|
||||
const pledges = g.coalitionEscrow!.pledges;
|
||||
return {targets: group.members.filter(member => member !== id).map(member => {
|
||||
const pledged = pledges.find(entry => entry.donor === id && entry.recipient === member)?.amount ?? 0;
|
||||
return {id: member, name: getPlayer(g, member).name, pledged,
|
||||
const minimum = g.phase === 3 && !g.currentAuctionSale && g.auction?.bidder === member
|
||||
? g.auction.coalitionContributions?.find(share => share.donor === id)?.amount ?? 0 : 0;
|
||||
return {id: member, name: getPlayer(g, member).name, pledged, minimum,
|
||||
maximum: Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, uncommittedSpice(g, me)) + pledged)};
|
||||
}), incoming: pledges.filter(entry => entry.recipient === id && entry.amount > 0)
|
||||
.map(entry => ({donor:entry.donor, name:getPlayer(g, entry.donor).name, amount:entry.amount})), blocked:null};
|
||||
@@ -47219,7 +47271,10 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
bidder: g.auction.bidder,
|
||||
active: g.auction.active,
|
||||
...(g.auction.bidder === id
|
||||
? { allyPayment: g.auction.allyPayment ?? 0 }
|
||||
? { allyPayment: g.auction.allyPayment ?? 0,
|
||||
...(g.auction.coalitionContributions === undefined ? {} : {
|
||||
coalitionPayment:g.auction.coalitionContributions.reduce((sum, share) => sum + share.amount, 0),
|
||||
}) }
|
||||
: {}),
|
||||
remaining: g.auction.cards.length - g.auction.index,
|
||||
inspectionBlocked: g.homeworldOccupationPreview ? quoteOccupiedBiddingAuthority(g, 'atreidesInspection').blocked : null,
|
||||
|
||||
Reference in new issue
Block a user