From 1eeeb32566652126a97c73f7044de3f6a48f1b9f Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:22:50 +1100 Subject: [PATCH] Support staged group bid funding --- components/coalition-funding.tsx | 14 +- components/game-table.tsx | 21 +- docs/CURRENT_STATUS.md | 17 +- docs/OPTIONAL_ALLIANCE_VICTORY.md | 21 +- docs/RULE_DECISIONS.md | 6 + game/bots.ts | 2 +- game/engine.ts | 85 ++++++-- game/normal-auction-retirement.ts | 1 + game/reference.ts | 6 +- tests/coalition-bid-funding.test.ts | 252 ++++++++++++++++++++++ tests/coalition-funding-controls.test.tsx | 15 ++ tests/nexus-offer-controls.test.tsx | 23 ++ 12 files changed, 426 insertions(+), 37 deletions(-) create mode 100644 tests/coalition-bid-funding.test.ts diff --git a/components/coalition-funding.tsx b/components/coalition-funding.tsx index 846ba977..5912a22d 100644 --- a/components/coalition-funding.tsx +++ b/components/coalition-funding.tsx @@ -4,7 +4,7 @@ import { useId, useState } from 'react'; /** Only the viewer's own pledges and contributions offered to that viewer. */ export type CoalitionFundingView = { - targets: readonly { id: string; name: string; pledged: number; maximum: number }[]; + targets: readonly { id: string; name: string; pledged: number; maximum: number; minimum?: number }[]; incoming: readonly { donor: string; name: string; amount: number }[]; blocked: string | null; }; @@ -35,13 +35,15 @@ export function coalitionPledgeAction( ): CoalitionPledgeAction | null { if (busy || blocked || !/^(0|[1-9]\d*)$/.test(raw)) return null; const amount = Number(raw); - if (!Number.isSafeInteger(amount) || amount > safeMaximum(target.maximum)) return null; + if (!Number.isSafeInteger(amount) || amount < safeMaximum(target.minimum ?? 0) || + amount > safeMaximum(target.maximum)) return null; return { type: 'coalitionPledge', recipient: target.id, amount }; } -export function CoalitionFunding({ view, busy = false, act, onContributionsChange }: { +export function CoalitionFunding({ view, busy = false, act, onContributionsChange, purpose = 'shipment' }: { view: CoalitionFundingView; busy?: boolean; + purpose?: 'bid' | 'shipment'; act: (action: CoalitionPledgeAction) => void; onContributionsChange?: (contributions: CoalitionFundingContribution[]) => void; }) { @@ -66,9 +68,11 @@ export function CoalitionFunding({ view, busy = false, act, onContributionsChang const raw = pledges[target.id] ?? String(target.pledged); const action = coalitionPledgeAction(target, raw, view.blocked, busy); const inputId = `${id}-pledge-${index}`; + const minimum = safeMaximum(target.minimum ?? 0); return
- 0 &&

The current high bid commits {minimum} spice of this pledge.

} + setPledges(previous => ({ ...previous, [target.id]: event.target.value }))} />