Allow Emperor gifts during Moritani extortion
Verify and publish checkpoint container / container (push) Successful in 11m51s
Verify and publish checkpoint container / container (push) Successful in 11m51s
This commit is contained in:
1 parent
6cec4ed78b
commit
538247e777
8 files changed
+307
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -86,6 +86,7 @@ export const FACTION_RULES: Record<
|
||||
'Start with all twenty forces in reserve and ten spice. One force revival is free each turn.',
|
||||
'Receive payments for other players’ auction purchases. Your own purchases pay the bank; a Karama purchase pays no spice. A cancellation sends one auction payment to the bank.',
|
||||
'Give spice directly to your ally. After the response window it is immediately spendable, unlike a deferred bribe between non-allies.',
|
||||
'In supported Moritani games, you may also give spice while your ally is choosing whether to pay Extortion. The original choice resumes after the gift’s Karama response.',
|
||||
'During Revival, pay two spice per extra allied force, up to three extra forces per turn. These are separate from the ally’s normal three-force allowance and may be purchased before or after ordinary revivals.',
|
||||
'Spice gifts and extra revival uses have Karama response windows. Canceling a use spends neither the Emperor’s spice nor the extra-revival allowance.',
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user