Offer Emperor gifts before allied auction payment
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
93b0146de9
commit
6cec4ed78b
11 files changed
+528
-2
No files matched your search
+110
@@ -1234,6 +1234,8 @@ type SpiceBankerIncomePayment = {
|
||||
export type ResponseWindow = {
|
||||
/** The original public Guild timing decision remains live during this gift. */
|
||||
emperorGuildTimingGift?: { event: string; signature: string };
|
||||
/** Links an allied auction gift to the still-pending payment opportunity. */
|
||||
emperorAuctionGiftEvent?: string;
|
||||
automaticAdvisorFlip?: AutomaticAdvisorFlipSource;
|
||||
advisorPreparationSource?: AdvisorPreparationSource;
|
||||
advisorMatchingSource?: AdvisorMatchingSource;
|
||||
@@ -2071,6 +2073,14 @@ export type Game = {
|
||||
phaseOpening: Game['phaseOpening'];
|
||||
};
|
||||
} | null;
|
||||
pendingEmperorAuctionGift?: {
|
||||
event: string;
|
||||
owner: string;
|
||||
recipient: string;
|
||||
turn: number;
|
||||
signature: string;
|
||||
giftAmount?: number;
|
||||
} | null;
|
||||
richeseGiftBlocked?: { turn: number; phase: number; cards: string[] };
|
||||
richeseAllyOffer?: RicheseAllyOffer | null;
|
||||
richeseAllyOpportunity?: { turn: number; recipient: string };
|
||||
@@ -18386,11 +18396,72 @@ function auctionNext(g: Game) {
|
||||
g.decision = { kind: 'auctionPayment', player: next.player };
|
||||
g.active = next.player;
|
||||
recoverAuctionPayment(g, getPlayer(g, next.player));
|
||||
beginEmperorAuctionGift(g);
|
||||
return;
|
||||
}
|
||||
a.active = next.player;
|
||||
g.active = a.active;
|
||||
}
|
||||
function emperorAuctionGiftSignature(g: Game): string {
|
||||
return JSON.stringify([g.turn, g.phase, g.active, g.decision, g.order, g.auction, g.aid]);
|
||||
}
|
||||
/** 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.advanced || g.status !== 'playing' || g.phase !== 3 ||
|
||||
g.expansions.length !== 0 || g.players.some(seat => !['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'].includes(seat.faction)) ||
|
||||
g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || g.nexusCards ||
|
||||
g.discoveryEnabled || g.sandtrout || g.coalitionState || g.pendingEmperorAuctionGift ||
|
||||
g.response || g.phaseOpening || g.truthtrance || g.pendingKarama ||
|
||||
g.pendingTreacheryDiscard || g.pendingRicheseGift || g.pendingNullentropy ||
|
||||
g.currentAuctionSale || g.richeseAuction || g.ixAuction ||
|
||||
g.decision?.kind !== 'auctionPayment' || !g.auction ||
|
||||
g.auction.bidder !== g.decision.player) return;
|
||||
const emperor = byFaction(g, 'emperor');
|
||||
const buyer = g.players.find(player => player.id === g.decision!.player);
|
||||
if (!emperor || !buyer || emperor.id === buyer.id ||
|
||||
emperor.ally !== buyer.id || buyer.ally !== emperor.id) return;
|
||||
g.pendingEmperorAuctionGift = { event: crypto.randomUUID(), owner: emperor.id,
|
||||
recipient: buyer.id, turn: g.turn, signature: emperorAuctionGiftSignature(g) };
|
||||
}
|
||||
function emperorAuctionGiftIntegrity(g: Game) {
|
||||
const pending = g.pendingEmperorAuctionGift;
|
||||
if (!pending) {
|
||||
requireRule(!g.response?.emperorAuctionGiftEvent &&
|
||||
!(g.pendingKarama?.use.kind === 'cancel' && g.pendingKarama.use.response.emperorAuctionGiftEvent),
|
||||
'The Emperor auction gift opportunity is missing.');
|
||||
return;
|
||||
}
|
||||
const owner = g.players.find(player => player.id === pending.owner);
|
||||
const recipient = g.players.find(player => player.id === pending.recipient);
|
||||
requireRule(g.status === 'playing' && g.advanced && 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 &&
|
||||
g.decision?.kind === 'auctionPayment' && g.decision.player === recipient.id &&
|
||||
g.auction?.bidder === recipient.id && pending.signature === emperorAuctionGiftSignature(g),
|
||||
'The Emperor auction gift lost its original payment opportunity.');
|
||||
const response = g.pendingKarama?.use.kind === 'cancel'
|
||||
? g.pendingKarama.use.response : g.response;
|
||||
if (response)
|
||||
requireRule(response.kind === 'emperorGift' && response.owner === owner.id &&
|
||||
response.recipient === recipient.id && response.emperorAuctionGiftEvent === pending.event &&
|
||||
typeof response.amount === 'number' && Number.isSafeInteger(response.amount) &&
|
||||
response.amount > 0 && response.amount === pending.giftAmount &&
|
||||
response.amount <= uncommittedSpice(g, owner),
|
||||
'The pending Emperor gift differs from its original auction opportunity.');
|
||||
else requireRule(pending.giftAmount === undefined,
|
||||
'The Emperor auction gift lost its original response.');
|
||||
}
|
||||
function emperorAuctionGiftMaximum(g: Game, p: Player): number {
|
||||
if (liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length ||
|
||||
hasMovementPromises(g, p.id, true) || hasBribePromises(g, p.id, true) ||
|
||||
liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).some(row => row.answer) ||
|
||||
liveLeaderRevivalPromises(g.leaderRevivalPromises ?? [], p.id, g.turn).some(row => row.answer) ||
|
||||
liveRevivalExpressionPromises(g.revivalExpressionPromises ?? [], p.id, g.turn).some(row => row.answer) ||
|
||||
liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length ||
|
||||
livePhaseBidPromises(g.phaseBidPromises ?? [], p.id, g.turn).some(row => row.answer)) return 0;
|
||||
return Math.max(0, uncommittedSpice(g, p));
|
||||
}
|
||||
function settleAuction(g: Game, free = false, automatic = false, emperorNexus = false) {
|
||||
const a = g.auction!;
|
||||
const winner = getPlayer(g, a.bidder!);
|
||||
@@ -20438,6 +20509,7 @@ function finishHarkonnenExchange(
|
||||
{ faction: 'harkonnen', name: 'Harkonnen exchange' });
|
||||
}
|
||||
function finishAutomaticDecision(g: Game): boolean {
|
||||
if (g.pendingEmperorAuctionGift) return false;
|
||||
if (g.biddingEnd && biddingEndQuiet(g)) return finishBiddingEnd(g);
|
||||
if (advanceHomeworldReveal(g)) return true;
|
||||
const decision = g.decision;
|
||||
@@ -38255,6 +38327,11 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
|
||||
`${emperor.name} ${response.kind === 'emperorGift' ? 'shared spice with' : `offered ${amount} extra force revivals to`} ${recipient.name}.`,
|
||||
);
|
||||
}
|
||||
if (response.emperorAuctionGiftEvent) {
|
||||
requireRule(g.pendingEmperorAuctionGift?.event === response.emperorAuctionGiftEvent,
|
||||
'The Emperor gift lost its original auction opportunity.');
|
||||
delete g.pendingEmperorAuctionGift.giftAmount;
|
||||
}
|
||||
} else if (response.kind === 'guildIncome') {
|
||||
const payment = !canceled ? creditFactionPayment(g, response.owner, 'shipment', response.amount!-(bureaucratDiversion ?? 0)) : null;
|
||||
awardFinalFactionBankerIncome(g, response.spiceBankerIncomePayment,
|
||||
@@ -41928,7 +42005,11 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
* automatic deal payment; it is never supplied by a client action. */
|
||||
function applyActionWithInitiator(state: Game, id: string, action: Action, initiator: string,
|
||||
options?: {insolventDealPayment?: boolean}): Game {
|
||||
emperorAuctionGiftIntegrity(state);
|
||||
emperorGuildTimingGiftIntegrity(state);
|
||||
if (state.pendingEmperorAuctionGift)
|
||||
requireRule(!['dealPay', 'dealOffer', 'dealRespond', 'dealWithdraw', 'dealRelease'].includes(action?.type),
|
||||
'Finish the Emperor auction gift opportunity before changing a public deal.');
|
||||
twoPlayerControllersIntegrity(state);
|
||||
factionDraftIntegrity(state);
|
||||
factionSwapIntegrity(state);
|
||||
@@ -42673,6 +42754,7 @@ function settleAutomaticContinuations(g: Game) {
|
||||
}
|
||||
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
|
||||
export function normalizeAutomaticGame(state: Game): Game {
|
||||
emperorAuctionGiftIntegrity(state);
|
||||
emperorGuildTimingGiftIntegrity(state);
|
||||
twoPlayerControllersIntegrity(state);
|
||||
factionDraftIntegrity(state);
|
||||
@@ -42946,6 +43028,27 @@ function applyActionInner(
|
||||
);
|
||||
requireRule(g.status !== 'finished', 'This game has ended.');
|
||||
const t = action.type;
|
||||
if (g.pendingEmperorAuctionGift && !g.response && !g.pendingKarama) {
|
||||
const pending = g.pendingEmperorAuctionGift;
|
||||
if (t === 'advanceBots') return g;
|
||||
requireRule(id === pending.owner, 'Wait for the Emperor to finish the allied gift opportunity.');
|
||||
if (t === 'emperorAuctionGiftContinue') {
|
||||
requireRule(Object.keys(action).sort().join(',') === 'event,type' && action.event === pending.event,
|
||||
'This Emperor auction opportunity is no longer current.');
|
||||
g.pendingEmperorAuctionGift = null;
|
||||
return g;
|
||||
}
|
||||
requireRule(t === 'emperorGift' && Object.keys(action).sort().join(',') === 'amount,type',
|
||||
'Choose an allied spice gift or continue to the original payment.');
|
||||
const amount = integer(action.amount, 1, emperorAuctionGiftMaximum(g, p), 'Spice');
|
||||
pending.giftAmount = amount;
|
||||
g.response = { kind: 'emperorGift', owner: id, recipient: pending.recipient,
|
||||
amount, elite: 0, passed: [], emperorAuctionGiftEvent: pending.event };
|
||||
return g;
|
||||
}
|
||||
if (g.pendingEmperorAuctionGift && (g.response || g.pendingKarama) && t !== 'advanceBots')
|
||||
requireRule(t === 'passResponse' || t === 'card' && action.mode === 'cancel',
|
||||
'Resolve the Emperor gift response before another action.');
|
||||
if (g.nexusFremenRideBetrayal?.stage === 'choose') {
|
||||
if (t === 'advanceBots') return g;
|
||||
requireRule(t === 'decision', 'Finish the current before-ride Betrayal opportunity first.');
|
||||
@@ -47205,6 +47308,7 @@ function applyActionInner(
|
||||
throw new RuleError('That action is not available.');
|
||||
}
|
||||
export function viewGame(state: Game, id: string, now?: number) {
|
||||
emperorAuctionGiftIntegrity(state);
|
||||
emperorGuildTimingGiftIntegrity(state);
|
||||
twoPlayerControllersIntegrity(state);
|
||||
factionDraftIntegrity(state);
|
||||
@@ -48258,6 +48362,7 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
? { amount: undefined }
|
||||
: {}),
|
||||
...(g.response.emperorGuildTimingGift ? { emperorGuildTimingGift: undefined } : {}),
|
||||
...(g.response.emperorAuctionGiftEvent ? { emperorAuctionGiftEvent: undefined } : {}),
|
||||
}
|
||||
: null,
|
||||
paymentIncome: currentFactionPayment(g),
|
||||
@@ -48324,6 +48429,11 @@ export function viewGame(state: Game, id: string, now?: number) {
|
||||
: 0,
|
||||
},
|
||||
emperorGuildTimingGift: emperorGuildTimingGiftQuote(g, me),
|
||||
emperorAuctionGiftPending: g.pendingEmperorAuctionGift
|
||||
? { owner: g.pendingEmperorAuctionGift.owner } : null,
|
||||
emperorAuctionGift: g.pendingEmperorAuctionGift?.owner === id && !g.response && !g.pendingKarama
|
||||
? { event: g.pendingEmperorAuctionGift.event, recipient: g.pendingEmperorAuctionGift.recipient,
|
||||
maximum: emperorAuctionGiftMaximum(g, me) } : null,
|
||||
coalitionEmperor: (() => {
|
||||
const group = coalitionEmperorGroup(g, me);
|
||||
return group ? {allies: group.members.filter(member => member !== id),
|
||||
|
||||
Reference in new issue
Block a user