From 5bcf5bab488a1ba784e18d3be8c665abcfe4e292 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:56:38 +1100 Subject: [PATCH] Implement High Richese mixed No-Field shipments --- components/richese-homeworld-forces.tsx | 43 ++++ components/richese-no-field.tsx | 36 +++- docs/CURRENT_STATUS.md | 37 +++- docs/HOMEWORLD_RULES.md | 63 ++++++ docs/IMPLEMENTATION_STATUS.md | 56 +++++ docs/RULE_DECISIONS.md | 1 + game/bots.ts | 34 +-- game/engine.ts | 82 ++++--- game/reference.ts | 10 +- game/richese-homeworld-shipment.ts | 45 ++++ tests/fixture-richese-homeworld-shipment.ts | 200 ++++++++++++++++++ tests/nexus-richese-cunning.test.ts | 4 +- .../richese-homeworld-forces-controls.test.ts | 73 +++++++ .../richese-homeworld-shipment-price.test.ts | 50 +++++ ...richese-homeworld-shipment-runtime.test.ts | 155 ++++++++++++++ tests/richese-homeworld-shipment.test.ts | 77 +++++++ 16 files changed, 912 insertions(+), 54 deletions(-) create mode 100644 components/richese-homeworld-forces.tsx create mode 100644 game/richese-homeworld-shipment.ts create mode 100644 tests/fixture-richese-homeworld-shipment.ts create mode 100644 tests/richese-homeworld-forces-controls.test.ts create mode 100644 tests/richese-homeworld-shipment-price.test.ts create mode 100644 tests/richese-homeworld-shipment-runtime.test.ts create mode 100644 tests/richese-homeworld-shipment.test.ts diff --git a/components/richese-homeworld-forces.tsx b/components/richese-homeworld-forces.tsx new file mode 100644 index 00000000..d0654f8e --- /dev/null +++ b/components/richese-homeworld-forces.tsx @@ -0,0 +1,43 @@ +'use client'; + +export type RicheseHomeworldForcesProps = { + enabled: boolean; + amount: number; + max: number; + busy: boolean; + onChange: (amount: number) => void; +}; + +export function RicheseHomeworldForces({ enabled, amount, max, busy, onChange }: RicheseHomeworldForcesProps) { + const blocked = amount > 0 && !enabled + ? 'Paid forces alongside a No-Field require Richese at High population. Set the amount to 0 to continue without paid reserve forces.' + : amount > max ? `Only ${max} physical reserve ${max === 1 ? 'force is' : 'forces are'} available for this selection. Reduce the amount before shipping.` : null; + return ( +
+ +

+ Paid forces enter the destination now. The No-Field’s concealed forces + stay in reserves until revelation, which uses the reserves then available. + Each paid force adds to the marker’s shipment price. +

+ {blocked ?

{blocked}

+ : !enabled &&

Adding paid reserve forces requires native Richese at High population.

} +
+ ); +} diff --git a/components/richese-no-field.tsx b/components/richese-no-field.tsx index 83fa4eae..13fdd49e 100644 --- a/components/richese-no-field.tsx +++ b/components/richese-no-field.tsx @@ -8,6 +8,8 @@ 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 { RicheseHomeworldForces } from './richese-homeworld-forces'; import { ShipmentQuote } from './shipment-quote'; import { Button } from './ui/button'; @@ -112,6 +114,7 @@ export function RicheseNoFieldControls({ const [pairConcealedSelection, setPairConcealedSelection] = useState(''); const [pairRevealedSelection, setPairRevealedSelection] = useState(''); const [useSmuggler, setUseSmuggler] = useState(false); + const [reserveForces, setReserveForces] = useState(0); const info = game.richeseNoField; const me = game.players.find((player) => player.id === game.me); if (!info || info.owner !== game.me || !info.private || !me) return null; @@ -136,22 +139,30 @@ export function RicheseNoFieldControls({ const rate = { faction: me.faction, halfRate: - me.faction === 'guild' || + (!game.guildRateCanceled && (me.faction === 'guild' || game.players.some( (player) => player.faction === 'guild' && player.id === me.ally, - ) || + ))) || game.karamaShipping?.player === me.id, }; - const cost = reserveShipmentCost(rate, selectedTerritory.type, 1); + const markerCost = reserveShipmentCost(rate, selectedTerritory.type, 1); const smuggler = info.canShip ? quoteSmugglerNoField(game, me.id, destination) : null; + let group: ReturnType | null = null; + let groupProblem: string | null = null; + try { + group = quoteRicheseHomeworldForces({ enabled: info.homeworldForcesEnabled, + reserves: me.reserves, amount: reserveForces, smuggler: useSmuggler && !!smuggler }); + } catch (error) { groupProblem = error instanceof Error ? error.message : 'Choose available reserve forces.'; } + const cost = reserveShipmentCost(rate, selectedTerritory.type, group?.priced ?? 1); const share = !me.ally || allyPayment === '' ? 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); 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.'); @@ -178,6 +189,7 @@ export function RicheseNoFieldControls({ 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.'); if (nexus?.blocked) pairProblems.push(nexus.blocked); if (!pairConcealed || !pairRevealed) pairProblems.push('Choose two different available No-Field tokens.'); @@ -270,8 +282,16 @@ export function RicheseNoFieldControls({

Destination: {selectedTerritory.name}, sector {sector}. Choose another destination on the map or in the shipment controls. This - replaces your normal shipment at the price of one force. + replaces your normal shipment. The marker is priced as one force; + selected paid reserve forces add to that invoice.

+ {(game.homeworlds || reserveForces > 0) && } Ship concealed No-Field {selected?.value ?? ''} · {cost} spice + {reserveForces ? ` · + ${reserveForces} paid forces` : ''} {useSmuggler && smuggler ? ' · + 1 real force' : ''} {nexus && ( @@ -328,7 +350,7 @@ export function RicheseNoFieldControls({ 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 ({cost} spice). + one force. Both ship for the price of one marker ({markerCost} spice). This is separate from the ordinary shipment and Smuggler.