Recognize group allies in bribes and deals
Verify and publish checkpoint container / container (push) Canceled after 0s

This commit is contained in:
Quintine committed 2026-10-28 23:57:55 +11:00
1 parent 2fe97ddd1f
commit 6e26ceb216
12 files changed
+318 -29

No files matched your search

+28 -17
View File
@@ -751,7 +751,7 @@ import {
} from './advisors';
import { isAlliedShipmentVisit, makeBasicAlliedShipment, quoteBasicAlliedShipment, basicAlliedShipmentHasWitness,
type BasicAlliedShipment } from './allied-separation';
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionGroups, coalitionOf,
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionAllies, coalitionGroups, coalitionOf,
leaveCoalition, CoalitionAllianceError, type CoalitionState, type CoalitionTransition } from './coalition-alliance';
import { coalitionBattleBeneficiary } from './coalition-battle-preparation';
import { reconcileCoalitionEscrow, setCoalitionPledge, type CoalitionEscrowMembership,
@@ -5892,7 +5892,7 @@ function publicDealCompletion(g: Game, payer: Player): boolean {
const allDebts = publicDealObligationRule(() => pendingPublicDealPayments(g.deals ?? [],payer.id,g.turn));
// An allied recipient cannot currently receive a bribe. Keep that term
// pending, but do not let it mask other independently payable debts.
const debts = allDebts.filter(debt => debt.recipient !== payer.ally);
const debts = allDebts.filter(debt => !bribeAllied(g, payer, debt.recipient));
if (!debts.length) return true;
publicDealObligationRule(() => publicDealPaymentTotal(g.deals ?? [],payer.id,g.turn));
if (g.status !== 'playing') return false;
@@ -5961,7 +5961,7 @@ function reconcilePublicDealTruth(g: Game, voluntary?: {actor:string;action:Acti
if (!pending.length) continue;
const beforePayer = before && getPlayer(before,p.id);
const wasPossible = !!before && !!beforePayer && voluntary?.actor === p.id &&
pendingPublicDealPayments(before.deals ?? [],p.id,before.turn).some(debt => debt.recipient !== beforePayer.ally) &&
pendingPublicDealPayments(before.deals ?? [],p.id,before.turn).some(debt => !bribeAllied(before, beforePayer, debt.recipient)) &&
publicDealCompletion(before,beforePayer);
const expiredByAction = pending.some(deal => deal.payment?.kind === 'deferredBribe' &&
(g.turn > deal.payment.dueTurn || g.status === 'finished'));
@@ -5976,7 +5976,7 @@ function reconcilePublicDealTruth(g: Game, voluntary?: {actor:string;action:Acti
const payment = deal.payment!;
if (payment.kind !== 'deferredBribe') continue;
const expired = g.turn > payment.dueTurn || g.status === 'finished';
if (!expired && payment.recipient === p.ally) continue;
if (!expired && bribeAllied(g, p, payment.recipient)) continue;
const baseMentatShortfall = quiet && g.turn === payment.dueTurn && g.phase === 8 &&
g.expansions.length === 0 && bribePromiseModeSupported(g) && available < payment.amount;
if (expired || baseMentatShortfall) {
@@ -6000,7 +6000,7 @@ function protectInsolventDueDeal(g: Game, payer: string, payment: PendingPublicD
if (payment.dueTurn === g.turn || !publicDealInsolventAtBaseMentat(g,payer)) return;
const seat = getPlayer(g,payer), available = Math.max(0,uncommittedSpice(g,seat));
const affordable = publicDealObligationRule(() => pendingPublicDealPayments(g.deals ?? [],payer,g.turn));
requireRule(!affordable.some(debt => debt.recipient !== seat.ally && debt.amount <= available &&
requireRule(!affordable.some(debt => !bribeAllied(g, seat, debt.recipient) && debt.amount <= available &&
debt.amount > available - payment.amount),
'Keep the funding for a currently payable deal before paying a later deal.');
}
@@ -16019,6 +16019,16 @@ function coalitionRule<T>(work: () => T): T {
try { return work(); }
catch (error) { if (error instanceof CoalitionAllianceError || error instanceof CoalitionFundingError) throw new RuleError(error.message); throw error; }
}
/** Preserve legacy one-sided ally restrictions while recognizing every
* member of an explicitly staged larger alliance. */
function bribeAllied(g: Game, owner: Player, recipient: string): boolean {
return owner.ally === recipient || !!g.coalitionState &&
coalitionRule(() => coalitionAllies(g.players, g.coalitionState, owner.id)).includes(recipient);
}
function publicDealSeats(g: Game) {
return g.coalitionState ? g.players.map(player => ({...player,
allies: coalitionRule(() => coalitionAllies(g.players, g.coalitionState, player.id))})) : g.players;
}
function coalitionMembership(g: Game): CoalitionEscrowMembership {
return { seats: g.players.map(({id, ally, allySinceTurn}) => ({id, ally, allySinceTurn})),
...(g.coalitionState ? {coalitionState: structuredClone(g.coalitionState)} : {}) };
@@ -16443,7 +16453,7 @@ function quoteOrdinaryPayments(g: Game, actor: string, payments: readonly Ordina
const owner = getPlayer(g, actor);
for (const payment of payments) {
const recipient = getPlayer(g, payment.recipient);
requireRule(recipient.id !== actor && recipient.id !== owner.ally &&
requireRule(recipient.id !== actor && !bribeAllied(g, owner, recipient.id) &&
payment.amount <= uncommittedSpice(g, owner), 'Retain legal ordinary payment funding and recipients.');
checkBureaucratContributors(g, recipient.id, [{ payer: actor, amount: payment.amount }]);
recordBribePayment(g, actor, recipient.id, payment.amount);
@@ -16505,7 +16515,7 @@ function bribeCompatibleLaterPromises(g: Game, p: Player,
}
function bribeTruthAnswers(g: Game, p: Player, claim: BribeClaim): ('yes' | 'no' | 'unknown')[] {
if (!bribeQuestionWindow(g) || claim.turn !== g.turn || claim.recipient === p.id ||
claim.recipient === p.ally || !g.players.some(player => player.id === claim.recipient)) return [];
bribeAllied(g, p, claim.recipient) || !g.players.some(player => player.id === claim.recipient)) return [];
const source = g.phase === 2 && !g.charityOrder ? { ...g, ready: g.ready.filter(id => id !== p.id) } : g;
return (['yes', 'no'] as const).filter(answer => {
const promises = [...(g.bribePromises ?? []), { ...claim, event: 'candidate', player: p.id,
@@ -16518,7 +16528,7 @@ function bribeTruthAnswers(g: Game, p: Player, claim: BribeClaim): ('yes' | 'no'
}
function bribeExpressionTruthAnswers(g: Game, p: Player, claim: BribeExpressionClaim): ('yes' | 'no' | 'unknown')[] {
if (!bribeQuestionWindow(g) || claim.turn !== g.turn || bribeExpressionRecipients(claim.claim)
.some(recipient => recipient === p.id || recipient === p.ally || !g.players.some(player => player.id === recipient))) return [];
.some(recipient => recipient === p.id || bribeAllied(g, p, recipient) || !g.players.some(player => player.id === recipient))) return [];
const source = g.phase === 2 && !g.charityOrder ? { ...g, ready: g.ready.filter(id => id !== p.id) } : g;
return (['yes', 'no'] as const).filter(answer => {
const expressions = [...(g.bribeExpressionPromises ?? []), { ...claim, event: 'candidate', player: p.id,
@@ -16692,7 +16702,7 @@ function projectedBribes(g: Game, p: Player): BribeOptions {
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,
targets: g.players.filter(target => target.id !== p.id && target.id !== p.ally).map(target => {
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;
let limitation: string | null = limit < available ? 'Your Truthtrance answers limit this recipient’s payment.' : null;
@@ -17011,7 +17021,7 @@ function charityPromiseCompletion(g: Game, player: string): Action | null {
const actor = getPlayer(trial,player), amount = actor.spice - 1;
if (!Number.isSafeInteger(amount) || amount <= 0 || amount > uncommittedSpice(trial,actor)) return null;
for (const recipient of trial.players) {
if (recipient.id === player || recipient.id === actor.ally) continue;
if (recipient.id === player || bribeAllied(trial, actor, recipient.id)) continue;
const action: Action = {type:'bribe',target:recipient.id,amount};
try {
const paid = applyActionInner(trial,player,action,'shipmentPreparation');
@@ -21836,7 +21846,7 @@ function turnActionCompletionAfterFreshDiscard(source: Game, actor: string, unkn
if (required.length && bribeTimingBlock(paymentWindow)) continue;
const total = required.reduce((sum, payment) => sum + payment.amount, 0);
if (!Number.isSafeInteger(total) ||
required.some(payment => payment.recipient === actor || payment.recipient === owner.ally ||
required.some(payment => payment.recipient === actor || bribeAllied(trial, owner, payment.recipient) ||
!trial.players.some(player => player.id === payment.recipient))) continue;
for (const alliancePlan of alliancePlans) {
try {
@@ -40893,13 +40903,14 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
'Choose recipients and write the public terms.');
const payment = hasPayment ? action.payment as PublicDealPayment : undefined;
const event = crypto.randomUUID();
const seats = publicDealSeats(g);
const offer = publicDealRule(() => single
? hasPayment
? createPublicDeal(g.players, id, action.target as string, action.terms as string, g.turn, event, payment!)
: createPublicDeal(g.players, id, action.target as string, action.terms as string, g.turn, event)
? createPublicDeal(seats, id, action.target as string, action.terms as string, g.turn, event, payment!)
: createPublicDeal(seats, id, action.target as string, action.terms as string, g.turn, event)
: hasPayment
? createMultipartyPublicDeal(g.players, id, action.targets as string[], action.terms as string, g.turn, event, payment!)
: createMultipartyPublicDeal(g.players, id, action.targets as string[], action.terms as string, g.turn, event));
? createMultipartyPublicDeal(seats, id, action.targets as string[], action.terms as string, g.turn, event, payment!)
: createMultipartyPublicDeal(seats, id, action.targets as string[], action.terms as string, g.turn, event));
requireRule(!deals.some(deal => deal.id === offer.id), 'This deal identifier is already in use.');
g.deals = [...deals, offer];
} else {
@@ -40915,7 +40926,7 @@ function applyActionWithInitiator(state: Game, id: string, action: Action, initi
requireRule(typeof action.accept === 'boolean', 'Choose whether to accept or decline.');
requireRule(!action.accept || current.payment?.kind !== 'deferredBribe' || current.payment.dueTurn >= g.turn,
'The deferred payment deadline has passed; decline or withdraw this offer.');
next = publicDealRule(() => respondPublicDeal(current, g.players, id, action.accept as boolean));
next = publicDealRule(() => respondPublicDeal(current, publicDealSeats(g), id, action.accept as boolean));
} else if (action.type === 'dealRelease') {
requireRule(typeof action.agree === 'boolean', 'Choose whether to request cancellation.');
next = publicDealRule(() => releasePublicDeal(current, g.players, id, action.agree as boolean));
@@ -44388,7 +44399,7 @@ function applyActionInner(
requireRule(!blocked, blocked ?? 'Bribes are unavailable.');
const target = getPlayer(g, stringField(action.target));
requireRule(
target.id !== id && target.id !== p.ally,
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');