Allow Basic bribes during selected pending decisions
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-29 06:42:50 +11:00
1 parent 171bc3a636
commit 72f6ec2b12
11 files changed
+435 -29

No files matched your search

+59 -20
View File
@@ -109,7 +109,7 @@ import { PhaseBidPromiseError, phaseBidPromiseModeSupported, livePhaseBidPromise
type PhaseBidPromise } from './turn-bid-promises';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, bribesCloseBeforeNextMentat, maximumBribe, type BribeOptions } from './bribe-options';
import { bribeReceiptTiming, bribeTimingBlock, bribesCloseBeforeNextMentat, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteAtreidesFullPlan } from './atreides-full-plan';
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance, type AutomaticAdvisorStorm, type AutomaticAdvisorRelocation } from './advisor-automatic-flip';
@@ -17299,8 +17299,38 @@ function bribePaymentPreservesPromises(g: Game, p: Player, recipient: string, am
? !!turnActionCompletion(trial, p.id) : !hasBribePromises(trial, p.id) || !!bribePromiseCompletion(trial, actor);
} catch (error) { if (error instanceof BribeExpressionPromiseError || error instanceof BribePromiseError || error instanceof RuleError) return false; throw error; }
}
/** Selected Basic parents accept a direct payment without replacing or
* advancing their pending state. Existing whole-promise planners retain their
* original timing boundary until those compositions are implemented. */
function basicBribeInterrupt(g: Game, p: Player): boolean {
if (g.status !== 'playing' || g.advanced || g.expansions.length !== 1 ||
g.inflation?.side === 'double' || g.techTokens || g.strongholdCards || g.leaderSkills ||
g.homeworlds || g.nexusCards || g.discoveryEnabled || g.sandtrout || g.coalitionState ||
g.ecazTreachery || g.semutaPreview || g.semutaRecruitsPreview || g.truthtrance || g.response ||
g.pendingKarama || g.pendingKull || g.pendingTreacheryDiscard || g.pendingNullentropy ||
g.bureaucratPayments?.pending || g.battle || g.pendingRicheseGift || g.pendingShipment ||
g.pendingGuildTransport || g.pendingArrivalOverlap || g.pendingAmbassador || g.pendingEmperorAuctionGift ||
liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length ||
hasMovementPromises(g, p.id) || hasBribePromises(g, p.id) ||
liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).length ||
liveLeaderRevivalPromises(g.leaderRevivalPromises ?? [], p.id, g.turn).length ||
liveRevivalExpressionPromises(g.revivalExpressionPromises ?? [], p.id, g.turn).length ||
liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length ||
livePhaseBidPromises(g.phaseBidPromises ?? [], p.id, g.turn).length ||
(g.pledgePromises ?? []).some(row => row.player === p.id && row.turn === g.turn && !row.fulfilled && !row.released) ||
(g.deals ?? []).some(row => row.status === 'active' && row.payment?.payer === p.id && row.paymentStatus === 'pending')) return false;
const classic = ['atreides', 'beneGesserit', 'emperor', 'fremen', 'guild', 'harkonnen'];
if (g.expansions[0] === 'ix') return !!g.phaseOpening && !!g.amalWindowOpen && !g.decision &&
g.players.every(seat => [...classic, 'ixians', 'tleilaxu'].includes(seat.faction));
const extortion = g.moritaniExtortion, decision = g.decision;
return g.expansions[0] === 'ecaz' && !g.phaseOpening && g.phase === 8 &&
g.players.every(seat => [...classic, 'moritani'].includes(seat.faction)) &&
extortion?.stage === 'payment' && decision?.kind === 'moritaniExtortion' &&
decision.event === extortionEvent(extortion) && decision.player === extortion.queue[extortion.cursor];
}
function projectedBribes(g: Game, p: Player): BribeOptions {
const available = Math.max(0, uncommittedSpice(g, p)), blocked = bribeTimingBlock(g);
const available = Math.max(0, uncommittedSpice(g, p)),
blocked = basicBribeInterrupt(g, p) ? null : bribeTimingBlock(g);
const constrained = liveTurnActionPromises(g.turnActionPromises ?? [], p.id, g.turn).length > 0 || hasBribePromises(g, p.id) ||
g.battle?.truthPromises?.some(promise => promise.player === p.id && !promise.released && !g.battle?.revealed && !g.battle?.plans[p.id]) ||
liveShipmentPromises(g.shipmentPromises ?? [], p.id, g.turn).length ||
@@ -17308,7 +17338,7 @@ function projectedBribes(g: Game, p: Player): BribeOptions {
liveRevivalPromises(g.revivalPromises ?? [], p.id, g.turn).some(promise => promise.answer) ||
liveLeaderRevivalPromises(g.leaderRevivalPromises ?? [], p.id, g.turn).some(promise => promise.answer) ||
liveRevivalExpressionPromises(g.revivalExpressionPromises ?? [], p.id, g.turn).some(promise => promise.answer);
return { available, incoming: p.bribes, blocked,
return { available, incoming: p.bribes, receiptTiming: bribeReceiptTiming(g), blocked,
targets: g.players.filter(target => target.id !== p.id && !bribeAllied(g, p, target.id)).map(target => {
let limit = !blocked && constrained ? maximumBribe(available,
amount => bribePaymentPreservesPromises(g, p, target.id, amount)) : available;
@@ -42205,6 +42235,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
if (action?.type === 'dealPay') {
requireRule(state.status === 'playing' && Object.keys(action).sort().join(',') === 'deal,type' &&
typeof action.deal === 'string', 'Choose your pending deal payment.');
requireRule(!bribeTimingBlock(state), 'Finish the current decision or response before paying a deal.');
const g = structuredClone(state), index = (g.deals ?? []).findIndex(deal => deal.id === action.deal);
requireRule(index >= 0, 'This deal is no longer available.');
const current = g.deals![index], payment = current.payment;
@@ -42275,6 +42306,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
// The payer consented to these immutable terms. Run the native action
// on the same cloned state so a failed payment also rolls back consent.
try {
requireRule(!bribeTimingBlock(g), 'Finish the current decision or response before paying a deal.');
requireRule(!getPlayer(g, next.payment.payer).autopilot, 'The payer is not controlling this seat.');
return applyActionWithInitiator(g, next.payment.payer, {
type: 'bribe', target: next.payment.recipient, amount: next.payment.amount,
@@ -43009,6 +43041,25 @@ export function normalizeAutomaticGame(state: Game): Game {
return g;
}
function payOrdinaryBribe(g: Game, p: Player, action: Action): void {
const blocked = bribeTimingBlock(g);
requireRule(!blocked || basicBribeInterrupt(g, p), blocked ?? 'Bribes are unavailable.');
const target = getPlayer(g, stringField(action.target));
requireRule(
target.id !== p.id && !bribeAllied(g, p, target.id),
'Bribes are between different, non-allied factions.',
);
const n = integer(action.amount, 1, uncommittedSpice(g, p), 'Spice');
checkBureaucratContributors(g,target.id,[{payer:p.id,amount:n}]);
recordBribePayment(g, p.id, target.id, n);
p.spice -= n;
log(
g,
`${p.name} paid a ${n}-spice bribe to ${target.name}; its recipient share becomes spendable ${bribeReceiptTiming(g) === 'nextTurn' ? 'at the start of next turn’s Mentat Pause' : 'at the next Mentat Pause opening'}.`,
);
const source = stampBureaucratPayment(g,'bribe',p.id,target.id,n).bureaucratPayment;
if (!offerBureaucratPayment(g,source,{kind:'bribe'})) target.bribes += n;
}
function applyActionInner(
state: Game,
id: string,
@@ -43553,6 +43604,10 @@ function applyActionInner(
decideBiddingEnd(g, p, action);
return g;
}
if (t === 'bribe' && basicBribeInterrupt(g, p)) {
payOrdinaryBribe(g, p, action);
return g;
}
if (t === 'emperorGift' && g.phaseOpening && !g.response) {
const quote = emperorPhaseOpeningGiftQuote(g, p);
requireRule(quote && Object.keys(action).sort().join(',') === 'amount,type',
@@ -45834,23 +45889,7 @@ function applyActionInner(
return g;
}
if (t === 'bribe') {
const blocked = bribeTimingBlock(g);
requireRule(!blocked, blocked ?? 'Bribes are unavailable.');
const target = getPlayer(g, stringField(action.target));
requireRule(
target.id !== id && !bribeAllied(g, p, target.id),
'Bribes are between different, non-allied factions.',
);
const n = integer(action.amount, 1, uncommittedSpice(g, p), 'Spice');
checkBureaucratContributors(g,target.id,[{payer:p.id,amount:n}]);
recordBribePayment(g, p.id, target.id, n);
p.spice -= n;
log(
g,
`${p.name} paid a ${n}-spice bribe to ${target.name}; its recipient share becomes spendable ${g.phase === 8 ? 'at the start of next turn’s Mentat Pause' : 'at the next Mentat Pause opening'}.`,
);
const source = stampBureaucratPayment(g,'bribe',p.id,target.id,n).bureaucratPayment;
if (!offerBureaucratPayment(g,source,{kind:'bribe'})) target.bribes += n;
payOrdinaryBribe(g, p, action);
return g;
}
if (t === 'stormDial') {