Add verified Truthtrance hand inventory questions

Players can ask total-hand and primary-role card-count comparisons using the
existing count controls and compound facts. Component presentation shares the
same default-role classification, including canonical Richese cards, Ix alternate
uses, and Ecaz slot-only specials. Only the respondent receives the verified
aggregate answer; current facts do not impose future custody promises.

Eight new tests cover all 60 catalog cards, real engine actions, malformed input,
private worlds, all four bots, JSON continuation, and a legal custody transfer.
Expanded production-room SQLite cases cover restored total/category/compound
facts, suspended auction state, and one winning concurrent answer/discard.
Independent review's two copy issues were fixed before broad verification.

Validation: types, lint and 4,200 offline tests passed; production build passed;
40 HTTP tests passed. One stable broad run (check 123036 ms, build 6505 ms,
integration 23467 ms). Browser room LC3E55GY verified combined role counts,
total hand size, invalid-input feedback and refresh at 390px phone width.
All 33 pre-integration rooms preserved unchanged; HTTP checks added 30 rooms.
No complete-mode, full-game or publication gate opened.

Private report: 20260913-hand-inventory-final/report.json
Source fingerprint: c6a69a9e69941cdd1e5734d92afedbf10d31fda36c91d26ca7e15f4fb7c59b18
Efficiency pilot milestone 3: aggregate counter 58,695,053 to 59,144,653 at
completed verification = 449,600 tokens, including context/coordination overhead.
Two bounded workhorse-model helpers, reused UI and SQLite fixtures, one browser
room. Counter is not monetary cost and this differently sized batch does not
establish a savings percentage. Final Git/push overhead follows this sample.
This commit is contained in:
Quintine committed 2026-09-13 11:29:05 +10:00
1 parent 32791ed5e2
commit 7454456755
14 files changed
+980 -151

No files matched your search

+58 -18
View File
@@ -4,29 +4,56 @@ import { useId } from 'react';
import { Input } from '@/components/ui/input';
import { TRUTH_CARD_NAMES } from '@/game/truthtrance';
import type { CardCountFact } from '@/game/truthtrance-card-count';
import {
HAND_INVENTORY_LABELS,
type HandInventoryFact,
} from '@/game/truthtrance-hand-inventory';
export function CardCountFields({
export function CardCountFields<T extends CardCountFact | HandInventoryFact>({
value,
onChange,
}: {
value: CardCountFact;
onChange: (fact: CardCountFact) => void;
value: T;
onChange: (fact: T) => void;
}) {
const id = useId();
const invalid = !Number.isSafeInteger(value.value) || value.value < 0;
return (
<>
<label>
Card name
<select
value={value.name}
onChange={(event) => onChange({ ...value, name: event.target.value })}
>
{TRUTH_CARD_NAMES.map((name) => (
<option key={name}>{name}</option>
))}
</select>
</label>
{value.kind === 'handCount' ? (
<label>
Card name
<select
value={value.name}
onChange={(event) =>
onChange({ ...value, name: event.target.value })
}
>
{TRUTH_CARD_NAMES.map((name) => (
<option key={name}>{name}</option>
))}
</select>
</label>
) : (
<label>
Cards to count
<select
value={value.category}
onChange={(event) =>
onChange({
...value,
category: event.target.value as HandInventoryFact['category'],
})
}
>
{Object.entries(HAND_INVENTORY_LABELS).map(([category, label]) => (
<option key={category} value={category}>
{label}
</option>
))}
</select>
</label>
)}
<label>
Compare card count
<select
@@ -44,7 +71,7 @@ export function CardCountFields({
</select>
</label>
<label htmlFor={`${id}-count`}>
Number of copies
{value.kind === 'handCount' ? 'Number of copies' : 'Number of cards'}
<Input
id={`${id}-count`}
type="number"
@@ -61,10 +88,23 @@ export function CardCountFields({
/>
</label>
<p id={`${id}-help`} className="fine">
Counts copies of this exact named card in the player’s hand now. Cards
in the deck, discard pile, or a separate cache do not count. The answer
reveals only whether the comparison is true.
{value.kind === 'handCount'
? 'Counts copies of this exact named card'
: value.category === 'all'
? 'Counts all physical cards'
: 'Counts physical cards of the selected primary role'}{' '}
in the player’s hand now. Cards in the deck, discard pile, or a separate
cache do not count. The answer reveals only whether the comparison is
true.
</p>
{value.kind === 'handInventory' && value.category !== 'all' && (
<p className="fine">
Primary roles follow the cards: Weirding Way is a weapon and Chemistry
is a defense. Worthless cards stay separate, including for Bene
Gesserit or CHOAM. Alternate uses and faction powers do not change
this count.
</p>
)}
{invalid && (
<p id={`${id}-error`} className="notice" role="alert">
{Number.isInteger(value.value) &&