Files
Dune/components/truthtrance-card-count.tsx
T
Quintine 7454456755 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.
2026-09-13 11:29:05 +10:00

119 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
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<T extends CardCountFact | HandInventoryFact>({
value,
onChange,
}: {
value: T;
onChange: (fact: T) => void;
}) {
const id = useId();
const invalid = !Number.isSafeInteger(value.value) || value.value < 0;
return (
<>
{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
value={value.compare}
onChange={(event) =>
onChange({
...value,
compare: event.target.value as CardCountFact['compare'],
})
}
>
<option value="eq">Exactly</option>
<option value="gte">At least</option>
<option value="lte">At most</option>
</select>
</label>
<label htmlFor={`${id}-count`}>
{value.kind === 'handCount' ? 'Number of copies' : 'Number of cards'}
<Input
id={`${id}-count`}
type="number"
min={0}
max={Number.MAX_SAFE_INTEGER}
step={1}
required
value={Number.isNaN(value.value) ? '' : value.value}
aria-invalid={invalid}
aria-describedby={`${id}-help${invalid ? ` ${id}-error` : ''}`}
onChange={(event) =>
onChange({ ...value, value: event.currentTarget.valueAsNumber })
}
/>
</label>
<p id={`${id}-help`} className="fine">
{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) &&
value.value > Number.MAX_SAFE_INTEGER
? 'That card count is too large.'
: 'Enter a whole card count of zero or more.'}
</p>
)}
</>
);
}