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 }))} />