From 9cb65a21f5a4e41ae7c563ce4f5955010d553bbd Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:33:42 +1100 Subject: [PATCH] Prototype unlimited alliance variant --- components/coalition-nexus.tsx | 16 +- components/game-table.tsx | 4 +- docs/CURRENT_STATUS.md | 15 +- docs/OPTIONAL_ALLIANCE_VICTORY.md | 31 ++- docs/RULE_DECISIONS.md | 7 + game/allied-separation.ts | 2 +- game/board-resolution-quote.ts | 2 +- game/bots.ts | 8 +- game/coalition-alliance.ts | 47 ++-- game/coalition-battle-preparation.ts | 2 +- game/coalition-worm-protection.ts | 4 +- game/engine.ts | 11 +- game/fremen-revival-grant.ts | 2 +- game/occupancy.ts | 2 +- game/reference.ts | 4 +- game/terminal-cancellation.ts | 2 +- game/victory-continuation.ts | 4 +- game/victory-progress.ts | 4 +- tests/coalition-unlimited-controls.test.tsx | 64 ++++++ tests/coalition-unlimited-emperor.test.ts | 108 ++++++++++ tests/coalition-unlimited-engine.test.ts | 213 +++++++++++++++++++ tests/coalition-unlimited-membership.test.ts | 95 +++++++++ tests/coalition-unlimited-powers.test.ts | 82 +++++++ 23 files changed, 677 insertions(+), 52 deletions(-) create mode 100644 tests/coalition-unlimited-controls.test.tsx create mode 100644 tests/coalition-unlimited-emperor.test.ts create mode 100644 tests/coalition-unlimited-engine.test.ts create mode 100644 tests/coalition-unlimited-membership.test.ts create mode 100644 tests/coalition-unlimited-powers.test.ts diff --git a/components/coalition-nexus.tsx b/components/coalition-nexus.tsx index a88261dd..09b09fd9 100644 --- a/components/coalition-nexus.tsx +++ b/components/coalition-nexus.tsx @@ -10,6 +10,8 @@ export type CoalitionNexusView = { groups: readonly CoalitionGroup[]; proposal: CoalitionAdmission | null; revision: number; + /** Absent preserves the original, at-most-three alliance prototype. */ + variant?: 'unlimited'; }; export type CoalitionNexusIntent = @@ -28,7 +30,7 @@ export function coalitionNexusOptions(view: CoalitionNexusView): { const ownGroup = view.groups.find(group => group.members.includes(view.me)); const occupied = new Set(view.groups.flatMap(group => group.members)); const invitations = !seated || view.proposal ? [] : view.groups.flatMap(group => - group.members.length !== 2 ? [] : view.players + (group.members.length < 2 || group.members.length >= (view.variant === 'unlimited' ? 6 : 3)) ? [] : view.players .filter(player => !occupied.has(player.id) && (player.id === view.me || group.members.includes(view.me))) .map(player => ({ groupId: group.id, entrant: player.id, members: group.members }))); @@ -57,8 +59,10 @@ export function CoalitionNexus({ view, disabled = false, onIntent }: { const submit = (intent: CoalitionNexusIntent) => { if (!disabled) onIntent(intent); }; return

Group alliance negotiations

-

All three players must agree before an invitation forms an alliance. - The existing pair remains allied while an invitation is pending.

+ {view.variant === 'unlimited' ?

Every current member and the invited player must agree. + The existing alliance remains in place while an invitation is pending.

: +

All three players must agree before an invitation forms an alliance. + The existing pair remains allied while an invitation is pending.

} {view.groups.length ? :

There are no alliances yet.

} {proposal ?
@@ -77,7 +81,9 @@ export function CoalitionNexus({ view, disabled = false, onIntent }: { proposal.approvals.includes(view.me) ? 'Withdraw approval and cancel invitation' : 'Decline and cancel invitation'}}
:
- {options.invitations.length > 0 &&

Invite an unallied player to an existing pair, or ask to join a pair.

} + {options.invitations.length > 0 &&

{view.variant === 'unlimited' + ? 'Invite an unallied player to an existing alliance, or ask to join one.' + : 'Invite an unallied player to an existing pair, or ask to join a pair.'}

} {options.invitations.map(option => } {ownGroup?.members.length === 3 &&

Leaving keeps the other two players allied under the provisional online policy.

} + {view.variant === 'unlimited' && (ownGroup?.members.length ?? 0) > 3 && +

Leaving keeps the other players allied under the provisional online policy.

}
}
; } diff --git a/components/game-table.tsx b/components/game-table.tsx index 66f8c919..24cc91d8 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -306,7 +306,7 @@ export function GameTable({ }) { const me = g.players.find((p) => p.id === g.me)!; const groupNexus = g.coalitionNexus; - const inGroup = groupNexus?.groups.some(group => group.members.length === 3 && group.members.includes(me.id)); + const inGroup = groupNexus?.groups.some(group => group.members.length >= 3 && group.members.includes(me.id)); const outgoingAlliance = g.allianceOffers[me.id]; const outgoingAllianceName = g.players.find((p) => p.id === outgoingAlliance)?.name; const shipmentAvailable = nexusGuildShipmentAvailable(g); @@ -4395,7 +4395,7 @@ export function GameTable({ {g.players .filter((p) => p.id !== me.id && !groupNexus?.groups.some(group => - group.members.length === 3 && group.members.includes(p.id))) + group.members.length >= 3 && group.members.includes(p.id))) .map((p) => (