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

+18 -3
View File
@@ -9,6 +9,12 @@ import {
type CardCountFact,
} from './truthtrance-card-count';
import { richeseCards } from './richese-cards';
import {
parseHandInventoryFact,
handInventoryFactMatches,
handInventoryFactText,
type HandInventoryFact,
} from './truthtrance-hand-inventory';
import {
parsePlanClaim,
planClaimText,
@@ -22,6 +28,7 @@ import { DUKE_VIDAL_ID } from './duke-vidal';
export type TruthFact =
| CardCountFact
| HandInventoryFact
| { kind: 'hand'; name: string }
| { kind: 'traitor'; leader: string }
| { kind: 'spice'; compare: 'eq' | 'gte' | 'lte'; value: number }
@@ -75,9 +82,11 @@ function parseFact(
'Truthtrance supports at most 16 clauses and four levels of grouping.',
);
const v = record(value);
if (v.kind === 'handCount') {
if (v.kind === 'handCount' || v.kind === 'handInventory') {
try {
return parseCardCountFact(v, TRUTH_CARD_NAMES);
return v.kind === 'handCount'
? parseCardCountFact(v, TRUTH_CARD_NAMES)
: parseHandInventoryFact(v);
} catch (error) {
throw new TruthError(
error instanceof Error ? error.message : String(error),
@@ -146,7 +155,10 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
g.status === 'playing' &&
g.phase === 5 &&
g.active === v.target &&
shipmentAvailable(g,g.players.find((p) => p.id === v.target)!) &&
shipmentAvailable(
g,
g.players.find((p) => p.id === v.target)!,
) &&
!g.response &&
!g.decision &&
!g.phaseOpening &&
@@ -252,6 +264,8 @@ export function truthFactAnswer(
p: Pick<Player, 'hand' | 'traitors' | 'traitorChoices' | 'spice'>,
fact: TruthFact,
): TruthAnswer {
if (fact.kind === 'handInventory')
return handInventoryFactMatches(p.hand, fact) ? 'yes' : 'no';
if (fact.kind === 'handCount')
return cardCountFactMatches(p.hand, fact) ? 'yes' : 'no';
if (fact.kind === 'hand')
@@ -293,6 +307,7 @@ export function truthQuestionText(
if (q.kind === 'battlePlan')
return `In this battle in ${territory(q.territory).name}, will it be true that ${planClaimText(q.claim, leaderName)}?`;
const clause = (f: TruthFact): string => {
if (f.kind === 'handInventory') return handInventoryFactText(f);
if (f.kind === 'handCount') return cardCountFactText(f);
if (f.kind === 'hand') return `you hold ${f.name}`;
if (f.kind === 'traitor') return `${leaderName(f.leader)} is your traitor`;