Allow Emperor gifts during Moritani extortion
Verify and publish checkpoint container / container (push) Successful in 11m51s

This commit is contained in:
Quintine committed 2026-10-29 06:28:09 +11:00
1 parent 6cec4ed78b
commit 538247e777
8 files changed
+307 -4

No files matched your search

+69
View File
@@ -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 };
/** The original Extortion payment decision remains live during this gift. */
emperorExtortionGift?: { event: string; signature: string };
/** Links an allied auction gift to the still-pending payment opportunity. */
emperorAuctionGiftEvent?: string;
automaticAdvisorFlip?: AutomaticAdvisorFlipSource;
@@ -19429,6 +19431,7 @@ function validateKaramaUse(
requireRule(!kullBlock, kullBlock ?? 'Kull Wahad prevents this Karama effect.');
if (use.kind === 'cancel') {
validateEmperorGuildTimingGift(g, use.response);
validateEmperorExtortionGift(g, use.response);
requireRule(
use.response && use.response.owner !== p.id,
'Karama cancels another faction’s power.',
@@ -29398,6 +29401,50 @@ function emperorGuildTimingGiftIntegrity(g: Game) {
if (g.pendingKarama?.use.kind === 'cancel')
validateEmperorGuildTimingGift(g, g.pendingKarama.use.response);
}
function emperorExtortionGiftSignature(g: Game, owner: string, recipient: string, amount: number): 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 {
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 =>
!['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen', 'moritani'].includes(seat.faction)) ||
g.ecazTreachery || g.techTokens || g.strongholdCards || g.leaderSkills || g.homeworlds || g.nexusCards ||
g.discoveryEnabled || g.sandtrout || g.coalitionState || g.response || g.phaseOpening ||
g.truthtrance || g.pendingKarama || g.pendingTreacheryDiscard || g.battle ||
g.pendingRicheseGift || g.pendingNullentropy || g.pendingShipment || g.pendingArrivalOverlap ||
!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) ||
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 null;
const maximum = Math.max(0, uncommittedSpice(g, p));
return maximum > 0 ? {recipient: p.ally, maximum} : null;
}
function validateEmperorExtortionGift(g: Game, response: ResponseWindow) {
const parent = response.emperorExtortionGift;
if (!parent) return;
const owner = g.players.find(player => player.id === response.owner);
const recipient = g.players.find(player => player.id === response.recipient);
requireRule(response.kind === 'emperorGift' && typeof parent.event === 'string' && !!parent.event &&
owner?.faction === 'emperor' && recipient && owner.ally === recipient.id && recipient.ally === owner.id &&
typeof response.amount === 'number' && Number.isSafeInteger(response.amount) && response.amount > 0 &&
response.amount <= uncommittedSpice(g, owner) && g.decision?.kind === 'moritaniExtortion' &&
g.decision.player === recipient.id &&
parent.signature === emperorExtortionGiftSignature(g, owner.id, recipient.id, response.amount),
'The Emperor gift lost its original Extortion payment decision.');
}
function emperorExtortionGiftIntegrity(g: Game) {
if (g.response) validateEmperorExtortionGift(g, g.response);
if (g.pendingKarama?.use.kind === 'cancel') validateEmperorExtortionGift(g, g.pendingKarama.use.response);
}
function chooseGuildTiming(g: Game, take: boolean) {
const guild = byFaction(g, 'guild')!;
if (take) g.active = guild.id;
@@ -37166,6 +37213,7 @@ function finishResponse(g: Game, canceled: boolean, settlement?: BureaucratSettl
currentFactionPayment(g);
const response = g.response!;
validateEmperorGuildTimingGift(g, response);
validateEmperorExtortionGift(g, response);
const advisorPreparationSource = currentAdvisorPreparationResponse(g, response);
const automaticAdvisorFlip = response.automaticAdvisorFlip ? currentAutomaticAdvisorFlip(g, response) : null;
const fremenGrant = response.kind === 'fremenRevivalGrant' ? currentFremenRevivalGrant(g, response) : null;
@@ -42005,8 +42053,14 @@ 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 {
emperorExtortionGiftIntegrity(state);
emperorAuctionGiftIntegrity(state);
emperorGuildTimingGiftIntegrity(state);
const extortionGiftResponse = state.response?.emperorExtortionGift ||
(state.pendingKarama?.use.kind === 'cancel' && state.pendingKarama.use.response.emperorExtortionGift);
if (extortionGiftResponse && action?.type !== 'advanceBots')
requireRule(action?.type === 'passResponse' || action?.type === 'card' && action.mode === 'cancel',
'Resolve the Emperor gift response before another action.');
if (state.pendingEmperorAuctionGift)
requireRule(!['dealPay', 'dealOffer', 'dealRespond', 'dealWithdraw', 'dealRelease'].includes(action?.type),
'Finish the Emperor auction gift opportunity before changing a public deal.');
@@ -42754,6 +42808,7 @@ function settleAutomaticContinuations(g: Game) {
}
/** Internal authoritative continuation. Callers must persist with their usual CAS fence. */
export function normalizeAutomaticGame(state: Game): Game {
emperorExtortionGiftIntegrity(state);
emperorAuctionGiftIntegrity(state);
emperorGuildTimingGiftIntegrity(state);
twoPlayerControllersIntegrity(state);
@@ -43511,6 +43566,17 @@ function applyActionInner(
specialKarama(g, p, action);
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',
'This Extortion gift is unavailable or no longer current.');
const amount = integer(action.amount, 1, quote.maximum, 'Spice');
g.response = { kind: 'emperorGift', owner: id, recipient: quote.recipient,
amount, elite: 0, passed: [], emperorExtortionGift: {
event: crypto.randomUUID(), signature: emperorExtortionGiftSignature(g, id, quote.recipient, amount),
} };
return g;
}
if (t === 'emperorGift' && g.decision?.kind === 'guildTiming' && !g.response) {
const quote = emperorGuildTimingGiftQuote(g, p);
requireRule(quote && Object.keys(action).sort().join(',') === 'amount,type',
@@ -47308,6 +47374,7 @@ function applyActionInner(
throw new RuleError('That action is not available.');
}
export function viewGame(state: Game, id: string, now?: number) {
emperorExtortionGiftIntegrity(state);
emperorAuctionGiftIntegrity(state);
emperorGuildTimingGiftIntegrity(state);
twoPlayerControllersIntegrity(state);
@@ -48362,6 +48429,7 @@ export function viewGame(state: Game, id: string, now?: number) {
? { amount: undefined }
: {}),
...(g.response.emperorGuildTimingGift ? { emperorGuildTimingGift: undefined } : {}),
...(g.response.emperorExtortionGift ? { emperorExtortionGift: undefined } : {}),
...(g.response.emperorAuctionGiftEvent ? { emperorAuctionGiftEvent: undefined } : {}),
}
: null,
@@ -48429,6 +48497,7 @@ export function viewGame(state: Game, id: string, now?: number) {
: 0,
},
emperorGuildTimingGift: emperorGuildTimingGiftQuote(g, me),
emperorExtortionGift: emperorExtortionGiftQuote(g, me),
emperorAuctionGiftPending: g.pendingEmperorAuctionGift
? { owner: g.pendingEmperorAuctionGift.owner } : null,
emperorAuctionGift: g.pendingEmperorAuctionGift?.owner === id && !g.response && !g.pendingKarama