Interpret bounded Truthtrance fact questions
Verify and publish checkpoint container / container (push) Successful in 10m38s

This commit is contained in:
Quintine committed 2026-10-29 04:31:15 +11:00
1 parent 88c0ab1547
commit 8ae275f598
16 files changed
+431 -31

No files matched your search

+27 -7
View File
@@ -64,6 +64,7 @@ import { parseForceCountFact, forceCountFactMatches, forceCountFactText, forceCo
import type { Action, Game, Player } from './engine';
import { coalitionAllies } from './coalition-alliance';
import { TRUTH_CARD_NAMES } from './truthtrance-card-names';
import { parseFreeformFactV1 } from './truthtrance-freeform-facts';
import { CHEAP_HERO_TRAITOR } from './traitors';
import {
TRUTH_FACT_MAX_NODES, TRUTH_FACT_MAX_DEPTH, TRUTH_FACT_MAX_TERMS,
@@ -126,6 +127,8 @@ export type TruthQuestion =
target: string;
text: string;
scope: 'fact' | 'currentTurn';
/** Only newly asked, explicitly recognized facts carry this stable interpretation. */
interpreted?: { version: 1; fact: Extract<TruthFact, { kind: 'hand' | 'spice' }> };
};
export type TruthAnswer = 'yes' | 'no' | 'unknown';
export type TruthQueueEntry = {
@@ -643,12 +646,20 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
v.scope === 'fact' || v.scope === 'currentTurn',
'Commitments may only concern the current turn.',
);
return {
const question: Extract<TruthQuestion, { kind: 'freeform' }> = {
kind: 'freeform',
target: v.target,
text: v.text.trim(),
scope: v.scope,
};
if (v.interpreted !== undefined) {
const fact = question.scope === 'fact' ? parseFreeformFactV1(question.text) : null;
const expected = fact ? { version: 1 as const, fact } : null;
check(expected !== null && JSON.stringify(v.interpreted) === JSON.stringify(expected),
'The saved freeform interpretation does not match its original question.');
question.interpreted = expected;
}
return question;
}
/** Reparse a committed question without recomputing its answer or binding again.
* The caller supplies the unchanged parent opportunity with its controls restored. */
@@ -691,6 +702,7 @@ export function validateSavedTruthtrance(g: Game) {
);
}
if (window?.question?.kind === 'fact' || window?.question?.kind === 'movement' ||
window?.question?.kind === 'freeform' && window.question.interpreted ||
window?.question?.kind === 'traitorCall' || window?.question?.kind === 'futureBattleLeader') {
check((window.stage === 'answer' || window.stage === 'unknown') && window.queue.length > 0,
'The saved fact question must retain its asking player and answer stage.');
@@ -1095,7 +1107,15 @@ export function resolveTruthAction(
p.id === current.player,
'Waiting for the Truthtrance holder to ask a question.',
);
w.question = parseQuestion(g, p.id, a.question);
// Derive the v1 interpretation at submission, never from client state.
// Old saved questions without this stamp retain their previous semantics.
const supplied = record(a.question);
const { interpreted: _ignoredInterpretation, ...questionInput } = supplied;
w.question = parseQuestion(g, p.id, questionInput);
if (w.question.kind === 'freeform' && w.question.scope === 'fact') {
const fact = parseFreeformFactV1(w.question.text);
if (fact) w.question.interpreted = { version: 1, fact };
}
if (w.question.kind === 'mixedBattlePlan')
check(effects.mixedBattleAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
'Choose a whole mixed battle-plan question compatible with this battle.');
@@ -1192,9 +1212,9 @@ export function resolveTruthAction(
a.answer === 'yes' || a.answer === 'no' || a.answer === 'unknown',
'Answer Yes, No, or I don’t know.',
);
if (q.kind === 'fact')
if (q.kind === 'fact' || q.kind === 'freeform' && q.interpreted)
check(
a.answer === truthFactAnswer(p, q.fact, truthKnowledgeOf(g, p)),
a.answer === truthFactAnswer(p, q.kind === 'fact' ? q.fact : q.interpreted!.fact, truthKnowledgeOf(g, p)),
'Answer this fact question truthfully.',
);
if (q.kind === 'shipment' || q.kind === 'mixedShipment') {
@@ -1347,11 +1367,11 @@ export function resolveTruthAction(
'Choose an answer compatible with your available native leaders and earlier commitments.');
if (a.answer !== 'unknown') effects.bindFutureBattleLeader(g, p, q, a.answer === 'yes', current.player);
}
// Freeform semantics cannot be certified by a deterministic engine. AI does not invent an answer.
if (q.kind === 'freeform' && p.bot)
// Unrecognized freeform semantics cannot be certified; AI does not invent an answer.
if (q.kind === 'freeform' && !q.interpreted && p.bot)
check(
a.answer === 'unknown',
'AI cannot yet interpret freeform questions; ask a structured fact question.',
'AI cannot interpret this freeform question; ask a recognized or structured fact question.',
);
(g.truthHistory ??= []).push({
turn: g.turn,