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) &&
+26 -12
View File
@@ -62,7 +62,9 @@ export function Truthtrance({
]);
const factId = useId();
const invalidSpice = (fact: TruthFact) =>
(fact.kind === 'spice' || fact.kind === 'handCount') &&
(fact.kind === 'spice' ||
fact.kind === 'handCount' ||
fact.kind === 'handInventory') &&
(!Number.isSafeInteger(fact.value) || fact.value < 0);
const invalidFact = clauses
.slice(0, join === 'single' ? 1 : 2)
@@ -357,9 +359,10 @@ export function Truthtrance({
<p className="notice">
Automatic enforcement currently supports the active
player’s unused shipment in base Basic games and base
Advanced games without Guild or optional modules. Select that player and finish pending
decisions first. Earlier questions and other rules
configurations remain unfinished.
Advanced games without Guild or optional modules. Select
that player and finish pending decisions first. Earlier
questions and other rules configurations remain
unfinished.
</p>
)}
<p className="notice">
@@ -442,16 +445,23 @@ export function Truthtrance({
compare: 'gte',
value: 2,
}
: e.target.value === 'spice'
: e.target.value === 'handInventory'
? {
kind: 'spice',
kind: 'handInventory',
category: 'all',
compare: 'gte',
value: 6,
}
: {
kind: 'traitor',
leader: g.allLeaders[0].id,
value: 2,
}
: e.target.value === 'spice'
? {
kind: 'spice',
compare: 'gte',
value: 6,
}
: {
kind: 'traitor',
leader: g.allLeaders[0].id,
}
: old,
),
)
@@ -461,6 +471,9 @@ export function Truthtrance({
<option value="handCount">
Number of a named card
</option>
<option value="handInventory">
Hand size or primary card role
</option>
<option value="traitor">Selected a traitor</option>
<option value="spice">
Current personal spice
@@ -487,7 +500,8 @@ export function Truthtrance({
))}
</select>
</label>
) : c.kind === 'handCount' ? (
) : c.kind === 'handCount' ||
c.kind === 'handInventory' ? (
<CardCountFields
value={c}
onChange={(next) =>