Recognize group allies in bribes and deals
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
2fe97ddd1f
commit
6e26ceb216
12 files changed
+318
-29
No files matched your search
+28
-17
@@ -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');
|
||||
|
||||
Reference in new issue
Block a user