From 61c269123c1d5796a38b3de6d70b94a5db8d275a Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:35:34 +1100 Subject: [PATCH] Combine High Richese reserves with Nexus Cunning --- components/richese-no-field.tsx | 49 ++++-- docs/CURRENT_STATUS.md | 33 ++++ docs/HOMEWORLD_RULES.md | 6 +- docs/IMPLEMENTATION_STATUS.md | 49 ++++++ docs/NEXUS_RICHESE_RULES.md | 70 ++++++++ docs/RULE_DECISIONS.md | 1 + game/bots.ts | 23 ++- game/engine.ts | 55 ++++-- game/homeworld-payment-options.ts | 22 ++- game/reference.ts | 7 +- game/richese-homeworld-shipment.ts | 14 ++ tests/fixture-richese-homeworld-cunning.ts | 61 +++++++ tests/fixture-richese-homeworld-shipment.ts | 98 ++++++++++- .../richese-homeworld-cunning-runtime.test.ts | 162 ++++++++++++++++++ tests/richese-homeworld-shipment.test.ts | 53 +++++- 15 files changed, 643 insertions(+), 60 deletions(-) create mode 100644 tests/fixture-richese-homeworld-cunning.ts create mode 100644 tests/richese-homeworld-cunning-runtime.test.ts diff --git a/components/richese-no-field.tsx b/components/richese-no-field.tsx index 13fdd49e..0bc30ca7 100644 --- a/components/richese-no-field.tsx +++ b/components/richese-no-field.tsx @@ -8,7 +8,7 @@ import { fighterCount } from '@/game/advisors'; import { reserveShipmentCost } from '@/game/shipment-price'; import type { RicheseNoFieldView } from '@/game/richese-no-field'; import { quoteSmugglerNoField } from '@/game/smuggler-no-field'; -import { quoteRicheseHomeworldForces } from '@/game/richese-homeworld-shipment'; +import { quoteRicheseHomeworldForces, quoteRicheseHomeworldPair } from '@/game/richese-homeworld-shipment'; import { RicheseHomeworldForces } from './richese-homeworld-forces'; import { ShipmentQuote } from './shipment-quote'; import { Button } from './ui/button'; @@ -145,7 +145,6 @@ export function RicheseNoFieldControls({ ))) || game.karamaShipping?.player === me.id, }; - const markerCost = reserveShipmentCost(rate, selectedTerritory.type, 1); const smuggler = info.canShip ? quoteSmugglerNoField(game, me.id, destination) : null; @@ -161,24 +160,25 @@ export function RicheseNoFieldControls({ ? Math.max(0, cost - (me.spice ?? 0)) : Number(allyPayment); const validShare = Number.isSafeInteger(share) && share >= 0 && share <= cost; - const problems: string[] = []; - if (groupProblem) problems.push(groupProblem); + const commonProblems: string[] = []; if (!info.canShip) - problems.push(info.shipBlock ?? 'No-Field shipment is not available now.'); - if (!selected) problems.push('No different No-Field token is available.'); + commonProblems.push(info.shipBlock ?? 'No-Field shipment is not available now.'); + if (!selected) commonProblems.push('No different No-Field token is available.'); if (destination === MOBILE_STRONGHOLD) - problems.push('You cannot ship directly into the mobile stronghold.'); + commonProblems.push('You cannot ship directly into the mobile stronghold.'); if ( selectedTerritory.type === 'stronghold' && game.players.filter( (player) => player.id !== me.id && fighterCount(player, destination) > 0, ).length >= 2 ) - problems.push('Two other factions already occupy this stronghold.'); + commonProblems.push('Two other factions already occupy this stronghold.'); if (!selectedTerritory.sectors.includes(sector)) - problems.push('Select a sector in the destination territory.'); + commonProblems.push('Select a sector in the destination territory.'); if (sector === game.storm) - problems.push('You cannot ship into the storm sector.'); + commonProblems.push('You cannot ship into the storm sector.'); + const problems = [...commonProblems]; + if (groupProblem) problems.push(groupProblem); if (!validShare) problems.push(`Choose an ally payment from 0 to ${cost}.`); else { if (share > game.aid.available) @@ -188,8 +188,14 @@ export function RicheseNoFieldControls({ if (cost - share > (me.spice ?? 0)) problems.push('Your spice does not cover the chosen payment.'); } - const pairProblems = [...problems]; - if (reserveForces > 0) pairProblems.push('Set paid reserve forces to 0 to use the separate two-token Cunning shipment.'); + const pairProblems = [...commonProblems]; + let pairGroup: ReturnType | null = null; + try { + pairGroup = quoteRicheseHomeworldPair({ enabled: info.homeworldForcesEnabled, + reserves: me.reserves, amount: reserveForces, revealedValue: pairRevealed?.value ?? 0 }); + } catch (error) { pairProblems.push(error instanceof Error ? error.message : 'Choose available paid reserve forces.'); } + const pairCost = reserveShipmentCost({ faction: me.faction, halfRate: false }, selectedTerritory.type, pairGroup?.priced ?? 1); + if (pairCost > (me.spice ?? 0)) pairProblems.push(`This Cunning group needs ${pairCost} own spice.`); if (nexus?.blocked) pairProblems.push(nexus.blocked); if (!pairConcealed || !pairRevealed) pairProblems.push('Choose two different available No-Field tokens.'); @@ -288,7 +294,7 @@ export function RicheseNoFieldControls({ {(game.homeworlds || reserveForces > 0) && } @@ -348,10 +354,13 @@ export function RicheseNoFieldControls({

Choose one token to reveal immediately and one to keep concealed at {selectedTerritory.name}, sector {sector}. The - revealed token brings up to its printed value in forces from - your current reserves; the concealed marker still counts as - one force. Both ship for the price of one marker ({markerCost} spice). - This is separate from the ordinary shipment and Smuggler. + selected paid group reserves {reserveForces} physical counters + first. The faceup token then brings {pairGroup?.revealed ?? 0} + {' '}from the remaining reserves; the concealed marker still + counts as one force. Both tokens are priced as one, plus the + paid group: {pairCost} spice. The paid selection above also + applies to this pair. Smuggler is a separate selection for + the ordinary single-marker route.