Preserve Emperor gift opportunity before bot Extortion
Verify and publish checkpoint container / container (push) Successful in 10m54s

This commit is contained in:
Quintine committed 2026-10-30 17:44:00 +11:00
1 parent a6be17fc5d
commit 7c1f4b4e09
13 files changed
+370 -13

No files matched your search

+35 -2
View File
@@ -2379,6 +2379,8 @@ export type Game = {
moritaniTerror?: TerrorState;
moritaniAtomics?: AtomicsAftermath | null;
moritaniExtortion?: ExtortionState;
/** Public release of the human Emperor's optional gift opportunity. */
emperorExtortionGiftRelease?: { event: string; emperor: string; recipient: string };
grummanCollection?: GrummanCollection;
pendingMoritaniPlacement?: {
token: string;
@@ -30345,7 +30347,7 @@ function emperorExtortionGiftSignature(g: Game, owner: string, recipient: string
return JSON.stringify([g.turn, g.phase, g.active, g.order, g.decision,
g.moritaniExtortion, g.moritaniTerror?.placementTurn, owner, recipient, amount]);
}
function emperorExtortionGiftQuote(g: Game, p: Player): {recipient: string; maximum: number} | null {
function emperorExtortionGiftPublicEligible(g: Game, p: Player): boolean {
const decision = g.decision, extortion = g.moritaniExtortion;
if (g.status !== 'playing' || g.phase !== 8 || g.expansions.length !== 1 ||
g.expansions[0] !== 'ecaz' || g.players.some(seat =>
@@ -30357,7 +30359,11 @@ function emperorExtortionGiftQuote(g: Game, p: Player): {recipient: string; maxi
!extortion || extortion.stage !== 'payment' || decision?.kind !== 'moritaniExtortion' ||
decision.event !== extortionEvent(extortion) || decision.player !== extortion.queue[extortion.cursor] ||
p.faction !== 'emperor' || !p.ally || decision.player !== p.ally ||
!g.players.some(seat => seat.id === p.ally && seat.ally === p.id) ||
!g.players.some(seat => seat.id === p.ally && seat.ally === p.id)) return false;
return true;
}
function emperorExtortionGiftQuote(g: Game, p: Player): {recipient: string; maximum: number} | null {
if (!emperorExtortionGiftPublicEligible(g, p) || !p.ally ||
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) ||
@@ -30368,6 +30374,18 @@ function emperorExtortionGiftQuote(g: Game, p: Player): {recipient: string; maxi
const maximum = Math.max(0, uncommittedSpice(g, p));
return maximum > 0 ? {recipient: p.ally, maximum} : null;
}
/** A neutral window for a human Emperor before an automated ally answers.
* The visible wait never depends on either player's private spice or promises. */
function emperorExtortionPriority(g: Game): {event: string; emperor: string; recipient: string} | null {
const emperor = byFaction(g, 'emperor'), decision = g.decision;
if (!emperor || (emperor.bot ?? emperor.autopilot) || !emperorExtortionGiftPublicEligible(g, emperor) ||
decision?.kind !== 'moritaniExtortion') return null;
const recipient = g.players.find(player => player.id === decision.player);
if (!recipient || !(recipient.bot ?? recipient.autopilot)) return null;
const release = g.emperorExtortionGiftRelease;
if (release?.event === decision.event && release.emperor === emperor.id && release.recipient === recipient.id) return null;
return {event:decision.event, emperor:emperor.id, recipient:recipient.id};
}
function validateEmperorExtortionGift(g: Game, response: ResponseWindow) {
const parent = response.emperorExtortionGift;
if (!parent) return;
@@ -44943,6 +44961,14 @@ function applyActionInner(
specialKarama(g, p, action);
return g;
}
if (t === 'emperorExtortionContinue') {
const priority = emperorExtortionPriority(g);
requireRule(priority && priority.emperor === id && action.event === priority.event &&
Object.keys(action).sort().join(',') === 'event,type',
'Continue only your current Emperor Extortion gift opportunity.');
g.emperorExtortionGiftRelease = priority;
return g;
}
if (t === 'emperorGift' && g.decision?.kind === 'moritaniExtortion' && !g.response) {
const quote = emperorExtortionGiftQuote(g, p);
requireRule(quote && Object.keys(action).sort().join(',') === 'amount,type',
@@ -45081,6 +45107,9 @@ function applyActionInner(
t === 'decision' && decision.player === id,
'Waiting for the player with the pending decision.',
);
if (decision.kind === 'moritaniExtortion')
requireRule(!emperorExtortionPriority(g),
'Wait for the Emperor to finish the allied gift opportunity.');
g.decision = null;
requireRule(decision.kind !== 'leaderSkillVisibility' && decision.kind !== 'leaderSkillRevival', 'Use the Leader Skills controls for this decision.');
if (decision.kind === 'moritaniAssassinate') {actMoritaniAssassinate(g,decision,action);return g;}
@@ -49913,6 +49942,10 @@ export function viewGame(state: Game, id: string, now?: number) {
},
emperorGuildTimingGift: emperorGuildTimingGiftQuote(g, me),
emperorExtortionGift: emperorExtortionGiftQuote(g, me),
emperorExtortionPriority: (() => {
const priority = emperorExtortionPriority(g);
return priority ? {event:priority.event, recipient:priority.recipient, canContinue:priority.emperor === id} : null;
})(),
emperorPhaseOpeningGift: emperorPhaseOpeningGiftQuote(g, me),
emperorAuctionGiftPending: g.pendingEmperorAuctionGift
? { owner: g.pendingEmperorAuctionGift.owner } : null,