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
This commit is contained in:
Quintine committed 2026-09-13 21:37:50 +10:00
1 parent bf8036dfc1
commit 788453e07e
20 files changed
+1213 -51

No files matched your search

+48 -2
View File
@@ -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<typeof quoteSmugglerNoField>;
use: boolean;
busy: boolean;
onChange: (use: boolean) => void;
}) {
if (!quote) return null;
return (
<div className="rounded border border-[#8e8159] p-3">
<label className="flex min-h-11 items-center gap-3">
<input
type="checkbox"
checked={use}
disabled={busy}
onChange={(event) => onChange(event.target.checked)}
/>
Use Smuggler to add 1 real reserve force at no added cost
</label>
<p className="fine mt-2">
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.
</p>
</div>
);
}
/** 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({
<div className="flex flex-col gap-3 py-3">
<p className="fine">
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.
</p>
<ul
className="m-0 flex flex-wrap gap-3 p-0"
@@ -240,6 +278,12 @@ export function RicheseNoFieldControls({
}}
unavailableReasons={problems}
/>
<SmugglerNoFieldChoice
quote={smuggler}
use={useSmuggler}
busy={busy}
onChange={setUseSmuggler}
/>
<Button
className="min-h-11 whitespace-normal"
disabled={busy || problems.length > 0}
@@ -253,10 +297,12 @@ export function RicheseNoFieldControls({
territory: destination,
sector,
allyPayment: share,
...(useSmuggler && smuggler ? { smuggler: true } : {}),
});
}}
>
Ship concealed No-Field {selected?.value ?? ''} · {cost} spice
{useSmuggler && smuggler ? ' · + 1 real force' : ''}
</Button>
</>
)}