From 788453e07ed27289d390322c33f969a1efcc9540 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:37:50 +1000 Subject: [PATCH] Integrate Smuggler companion with owned Richese No-Field Connect the GF9 FAQ combination: one free physical reserve force accompanies an owned concealed No-Field at its unchanged one-marker price. Preserve zero/three/five token values, actual supply at later reveal, optional human choice and all four legal AI paths. Genuine CHOAM & Richese Leader Skills development setup adds the two sourced battle cards without duplicating their Ix identities. Public mode and publication gates stay closed; allied and remaining combined routes are unfinished. Bind the private declaration across initial No-Field cancellation and later Advanced Guild interception, including saved interruptions. Validate before projection/actions/normalization, hide the internal proof, reject altered declarations immutably, and withdraw the companion once only on successful commitment. Independent rules/privacy/persistence review found no blocker. Preserve other audit setup gates despite expanded deck assembly. Repair the existing randomized recovery test with a genuine seeded Rihani offer and explicit human choice, replacing an inappropriate AI-policy call on a human seat. Initial Karama and later Guild stop retain their distinct existing opportunity outcomes. Validation: 72 focused regressions plus 10 deck tests; typecheck, lint, 4520 offline tests, production build and 43 HTTP tests passed. SQLite restart, concurrent allowance/cancellation and exact physical custody passed. Browser genuine assignment, private token selection, opt-in, one-spice shipment, refresh, five-force reveal and final refresh preserved six physical Arrakeen forces, 14 reserves, zero Tanks, four spice, Crysknife and Soo-Soo Sook. Genuine four-profile Basic/Advanced games completed in 77/116 actions with 3/4 JSON restores and no rejected candidates; Advanced exercised one companion shipment and later reveal. After the outage the existing server was healthy. Preserved all 529 opening rooms and original seat/recovery records (563 after QA/HTTP). SQLite integrity and foreign keys passed. No restart, database reset or automation. Verified source tree: c2aee4138bcb46a05814a340d62eadace3470247ca620844eb0375e78ea4d6c4 --- components/richese-no-field.tsx | 50 +++- docs/CURRENT_STATUS.md | 8 + docs/IMPLEMENTATION_STATUS.md | 22 ++ docs/LEADER_SKILLS_RUNTIME.md | 7 +- docs/RULE_DECISIONS.md | 2 +- docs/SMUGGLER_NO_FIELD.md | 79 ++++++ docs/SMUGGLER_SHIPMENT.md | 7 +- game/bots.ts | 16 +- game/cards.ts | 11 +- game/engine.ts | 127 ++++++++- game/reference.ts | 10 +- game/smuggler-no-field.ts | 39 +++ tests/homeworld-setup-engine.test.ts | 2 +- tests/ix-deck.test.ts | 13 +- tests/leader-skills-recovery.test.ts | 56 +++- tests/richese-card-presentation.test.ts | 4 +- tests/smuggler-no-field-controls.test.tsx | 128 +++++++++ tests/smuggler-no-field-fixture.ts | 122 +++++++++ tests/smuggler-no-field-recovery.test.ts | 316 ++++++++++++++++++++++ tests/smuggler-no-field.test.ts | 245 +++++++++++++++++ 20 files changed, 1213 insertions(+), 51 deletions(-) create mode 100644 docs/SMUGGLER_NO_FIELD.md create mode 100644 game/smuggler-no-field.ts create mode 100644 tests/smuggler-no-field-controls.test.tsx create mode 100644 tests/smuggler-no-field-fixture.ts create mode 100644 tests/smuggler-no-field-recovery.test.ts create mode 100644 tests/smuggler-no-field.test.ts diff --git a/components/richese-no-field.tsx b/components/richese-no-field.tsx index 98a78828..ccc9797b 100644 --- a/components/richese-no-field.tsx +++ b/components/richese-no-field.tsx @@ -7,9 +7,42 @@ import { territory, MOBILE_STRONGHOLD } from '@/game/board'; 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 { ShipmentQuote } from './shipment-quote'; import { Button } from './ui/button'; +export function SmugglerNoFieldChoice({ + quote, + use, + busy, + onChange, +}: { + quote: ReturnType; + use: boolean; + busy: boolean; + onChange: (use: boolean) => void; +}) { + if (!quote) return null; + return ( +
+ +

+ The companion force enters the empty territory immediately. The No-Field + token still uses the normal one-force shipment price, and its hidden + forces remain in reserves until revelation. +

+
+ ); +} + /** This renderer receives public player data only, never the private token model. */ export function NoFieldBoardMarkers({ players, @@ -76,6 +109,7 @@ export function RicheseNoFieldControls({ }) { const id = useId(); const [selection, setSelection] = useState(''); + const [useSmuggler, setUseSmuggler] = useState(false); 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; @@ -97,6 +131,9 @@ export function RicheseNoFieldControls({ game.karamaShipping?.player === me.id, }; const cost = reserveShipmentCost(rate, selectedTerritory.type, 1); + const smuggler = info.canShip + ? quoteSmugglerNoField(game, me.id, destination) + : null; const share = !me.ally || allyPayment === '' ? Math.max(0, cost - (me.spice ?? 0)) @@ -137,8 +174,9 @@ export function RicheseNoFieldControls({

Your token values are private until revealed. A concealed marker - counts as one force, even when its value is zero. Physical forces - remain in reserves until revelation. + counts as one force, even when its value is zero. Its hidden forces + remain in reserves until revelation; a Smuggler companion is a + separate real force that leaves reserves immediately.