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

+2
View File
@@ -1,6 +1,7 @@
'use client';
import type { Action, GameView } from '@/game/engine';
import { areCoalitionAllies } from '@/game/coalition-alliance';
import { bribeAction } from '@/game/bribe-options';
import { bribeClaimText, bribePromiseModeSupported, liveBribePromises } from '@/game/bribe-promises';
import { liveBribeExpressionPromises } from '@/game/bribe-expression-promises';
@@ -27,6 +28,7 @@ export function BribePromiseGuideButton(props: BribePromiseProps) {
const target = typeof action.target === 'string' ? current.players.find(player => player.id === action.target) : undefined;
const limit = options.targets.find(recipient => recipient.id === action.target);
const payment = action.type === 'bribe' && !!me && !!target && target.id !== actor && target.id !== me.ally &&
(!current.coalitionState || !areCoalitionAllies(current.players, current.coalitionState, actor, target.id)) &&
options.blocked === null && Number.isSafeInteger(options.available) &&
typeof action.amount === 'number' && action.amount <= options.available &&
!!limit && Number.isSafeInteger(limit.maximum) && action.amount <= limit.maximum &&
+3 -1
View File
@@ -1,6 +1,7 @@
'use client';
import type { GameView } from '@/game/engine';
import { areCoalitionAllies } from '@/game/coalition-alliance';
import type { MixedActionExpression } from '@/game/mixed-action-question';
import { BRIBE_EXPRESSION_MAX_DEPTH, BRIBE_EXPRESSION_MAX_LEAVES } from '@/game/bribe-expression-promises';
import { parseBribePaymentClaim, type BribePaymentClaim } from '@/game/mixed-bribe-question';
@@ -9,7 +10,8 @@ import { Input } from './ui/input';
const recipients = (game: GameView, target: string) => {
const player = game.players.find(player => player.id === target);
return player ? game.players.filter(recipient => recipient.id !== target && recipient.id !== player.ally) : [];
return player ? game.players.filter(recipient => recipient.id !== target && recipient.id !== player.ally &&
(!game.coalitionState || !areCoalitionAllies(game.players, game.coalitionState, target, recipient.id))) : [];
};
const config: MixedActionConfig<BribePaymentClaim> = {
prefix: 'mixed-bribe',
+12 -6
View File
@@ -3,6 +3,7 @@
import { useEffect, useRef, useState } from 'react';
import { faction } from '@/game/catalog';
import { publicDealAcceptVotes, publicDealParticipants } from '@/game/public-deals';
import { areCoalitionAllies } from '@/game/coalition-alliance';
import type { Action, GameView } from '@/game/engine';
import { Button } from '@/components/ui/button';
@@ -15,6 +16,13 @@ type DeferredPaymentProjection = GameView & {
deferredDealPaymentOptions?: Record<string, { blocked: string | null }>;
};
/** Alliance membership is public. Scalar links retain legacy pair behavior;
* a staged group uses the same coalition quote as authoritative rules. */
function allied(game: GameView, a: GameView['players'][number], b: GameView['players'][number]): boolean {
return a.ally === b.id || b.ally === a.id ||
!!game.coalitionState && areCoalitionAllies(game.players, game.coalitionState, a.id, b.id);
}
/** Requests are local to the active seat; an owed payment is as important to
* surface as a pending invitation or a request to release an agreement. */
export function publicDealRequests(game: GameView): string[][] {
@@ -71,7 +79,7 @@ export function PublicDealList({ game, act, disabled }: {
const recipient = party && game.me !== deal.from;
const seats = participants.map(id => game.players.find(player => player.id === id));
const eligible = seats.every((player, index) => player && seats.slice(index + 1).every(other =>
other && player.ally !== other.id && other.ally !== player.id));
other && !allied(game, player, other)));
const payment = (deal as unknown as { payment?: DealPayment }).payment;
const paymentStatus = (deal as unknown as { paymentStatus?: DeferredPaymentStatus }).paymentStatus;
const finalAcceptance = recipient && !accepted.includes(game.me) && accepted.length === participants.length - 1;
@@ -141,11 +149,9 @@ export function PublicDeals({ game, act, busy }: {
const me = game.players.find(player => player.id === game.me);
const targets = game.players.filter(player => me && player.id !== me.id && !player.bot);
const selectedPlayers = targets.filter(player => selected.includes(player.id));
const allied = (a: typeof game.players[number], b: typeof game.players[number]) =>
a.ally === b.id || b.ally === a.id;
const legalSelection = !!me && selected.length > 0 && selected.length === selectedPlayers.length &&
selectedPlayers.every((player, index) => !allied(me, player) &&
selectedPlayers.slice(index + 1).every(other => !allied(player, other)));
selectedPlayers.every((player, index) => !allied(game, me, player) &&
selectedPlayers.slice(index + 1).every(other => !allied(game, player, other)));
const parties = me ? [me, ...selectedPlayers] : [];
const amount = Number(paymentAmount);
const dueTurn = Number(paymentDueTurn);
@@ -174,7 +180,7 @@ export function PublicDeals({ game, act, busy }: {
<legend>Deal recipients</legend>
{targets.map(player => {
const checked = selected.includes(player.id);
const conflicts = !!me && (allied(me, player) || selectedPlayers.some(other => other.id !== player.id && allied(other, player)));
const conflicts = !!me && (allied(game, me, player) || selectedPlayers.some(other => other.id !== player.id && allied(game, other, player)));
return <label key={player.id} className="checkbox-row">
<input type="checkbox" checked={checked} disabled={disabled || (!checked && conflicts)}
onChange={event => {
+3 -1
View File
@@ -1,4 +1,5 @@
'use client';
import { areCoalitionAllies } from '@/game/coalition-alliance';
import { SaphoTruthOrder } from './sapho-truth-order';
import { TruthFutureBattleLeader } from './truth-future-battle-leader';
import { RicheseBidPromiseFields, richeseBidMinimumError } from './richese-bid-promise-fields';
@@ -156,7 +157,8 @@ export function Truthtrance({
const [bribeSelection, setBribeSelection] = useState<Pick<BribeClaim, 'recipient' | 'minimum'>>({ recipient: g.me, minimum: 3 });
const bribeClaim: BribeClaim = { turn: g.turn, ...bribeSelection };
const bribeRespondent = g.players.find(player => player.id === target);
const bribeRecipients = g.players.filter(player => player.id !== target && player.id !== bribeRespondent?.ally);
const bribeRecipients = g.players.filter(player => player.id !== target && player.id !== bribeRespondent?.ally &&
(!g.coalitionState || !bribeRespondent || !areCoalitionAllies(g.players, g.coalitionState, target, player.id)));
const [bribeExpression, setBribeExpression] = useState<BribeExpression>({ kind: 'and', terms: [
{ kind: 'payment', recipient: g.me, minimum: 3 }, { kind: 'payment', recipient: '', minimum: 2 },
] });