Expand recognized Truthtrance fact questions
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
8ae275f598
commit
9dd386ff7a
11 files changed
+279
-16
No files matched your search
+1
-1
@@ -3989,7 +3989,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
steps: [
|
||||
'Declare the card from your hand at any time after cards are dealt. Other players may declare a competing Truthtrance or pass; questions are then asked in storm order. The interrupted phase, response, or battle decision resumes afterward.',
|
||||
'Ask a game-related yes/no question. Named-card, selected-traitor, currently unrevealed Face Dancer and current personal-spice questions are checked by the server. Face Dancer and ordinary Traitor holdings stay distinct. AND and OR combine facts; only the combined answer is published, with no extra inventory details or separate answer for each clause.',
|
||||
'In a new freeform game-fact question, the exact forms “Do you have at least/exactly/at most N spice?” and “Do you have a [card name]?” are checked as current personal spice or hand facts. The recognized interpretation appears before asking and with the question. Other wording and current-turn promises remain player-managed; bots answer unknown. Already-pending older questions keep their original behavior.',
|
||||
'In a new freeform game-fact question, exact personal-spice, named-card and total-hand-size forms are checked as current facts. Two spice or card facts may be joined by one AND or OR; only their aggregate answer is published. The recognized interpretation appears before asking and with the question. Other wording and current-turn promises remain player-managed; bots answer unknown. Already-pending older questions keep their original behavior.',
|
||||
'For current spice, choose Exactly, At least or At most and a nonnegative whole amount. This counts spice personally held when answering, excluding separately pledged aid and incoming payments. It does not ask whether a particular action is affordable or promise that the balance will stay unchanged. A true current-spice answer does not prevent later legal spending.',
|
||||
'For a concealed No-Field, choose its current printed value0,3 or5, not the prospective force pool. A missing or already revealed marker answers No, including for0; a concealed5 with empty reserves still answers Yes to5. Only the respondent sees the expected aggregate before public reply. Neither question nor answer turns up the physical token or commits later use; current-force facts continue to exclude markers.',
|
||||
'The questioned player answers publicly. A definite answer discards the card. If the answer cannot be known, the holder may ask a different question or save Truthtrance for later.',
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { parseFreeformFactV1 } from './truthtrance-freeform-facts';
|
||||
import type { HandInventoryFact } from './truthtrance-hand-inventory';
|
||||
import type { TruthFact } from './truthtrance';
|
||||
|
||||
type V1Fact = Extract<TruthFact, { kind: 'hand' | 'spice' }>;
|
||||
export type FreeformFactV2 = V1Fact | HandInventoryFact |
|
||||
{ kind: 'and' | 'or'; terms: [V1Fact, V1Fact] };
|
||||
|
||||
/**
|
||||
* Version 2 preserves version 1 and recognizes a small additional set of
|
||||
* complete current facts. Do not change this grammar for saved v2 questions.
|
||||
* Callers must still restrict interpretation to fact-scoped questions.
|
||||
*/
|
||||
export function parseFreeformFactV2(text: string): FreeformFactV2 | null {
|
||||
if (typeof text !== 'string' || text.length > 500) return null;
|
||||
const input = text.trim();
|
||||
// Check both whole-name and compound parses before deciding; a future
|
||||
// printed name might itself contain a conjunction between known names.
|
||||
const existing = parseFreeformFactV1(input);
|
||||
const count = /^do[ \t]+you[ \t]+have[ \t]+(at[ \t]+least|exactly|at[ \t]+most)[ \t]+([0-9]+)[ \t]+cards?[ \t]+in[ \t]+your[ \t]+hand[ \t]*\??$/i.exec(input);
|
||||
if (count) {
|
||||
const value = Number(count[2]);
|
||||
if (!Number.isSafeInteger(value) || value < 0) return null;
|
||||
const relation = count[1].toLowerCase().replace(/[ \t]+/g, ' ');
|
||||
return { kind: 'handInventory', category: 'all',
|
||||
compare: relation === 'at least' ? 'gte' : relation === 'at most' ? 'lte' : 'eq', value };
|
||||
}
|
||||
const compound = /^do[ \t]+you[ \t]+have[ \t]+(.+?)[ \t]*\??$/i.exec(input);
|
||||
if (!compound) return existing;
|
||||
const body = compound[1];
|
||||
const separator = /[ \t]+(and|or)[ \t]+/gi;
|
||||
const matches: Array<Extract<FreeformFactV2, { kind: 'and' | 'or' }>> = [];
|
||||
for (const match of body.matchAll(separator)) {
|
||||
const index = match.index ?? 0;
|
||||
const left = body.slice(0, index);
|
||||
const right = body.slice(index + match[0].length);
|
||||
const first = parseFreeformFactV1(`Do you have ${left}`);
|
||||
const second = parseFreeformFactV1(/^do[ \t]+you[ \t]+have[ \t]+/i.test(right)
|
||||
? right : `Do you have ${right}`);
|
||||
if (first && second) matches.push({ kind: match[1].toLowerCase() as 'and' | 'or', terms: [first, second] });
|
||||
}
|
||||
// A whole printed name and a compound are also ambiguous if both fit.
|
||||
if (matches.length > 1 || existing && matches.length > 0) return null;
|
||||
return existing ?? matches[0] ?? null;
|
||||
}
|
||||
+10
-6
@@ -65,6 +65,7 @@ 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 { parseFreeformFactV2 } from './truthtrance-freeform-facts-v2';
|
||||
import { CHEAP_HERO_TRAITOR } from './traitors';
|
||||
import {
|
||||
TRUTH_FACT_MAX_NODES, TRUTH_FACT_MAX_DEPTH, TRUTH_FACT_MAX_TERMS,
|
||||
@@ -128,7 +129,7 @@ export type TruthQuestion =
|
||||
text: string;
|
||||
scope: 'fact' | 'currentTurn';
|
||||
/** Only newly asked, explicitly recognized facts carry this stable interpretation. */
|
||||
interpreted?: { version: 1; fact: Extract<TruthFact, { kind: 'hand' | 'spice' }> };
|
||||
interpreted?: { version: 1; fact: Extract<TruthFact, { kind: 'hand' | 'spice' }> } | { version: 2; fact: TruthFact };
|
||||
};
|
||||
export type TruthAnswer = 'yes' | 'no' | 'unknown';
|
||||
export type TruthQueueEntry = {
|
||||
@@ -653,8 +654,11 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
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;
|
||||
const marker = record(v.interpreted);
|
||||
const oldFact = question.scope === 'fact' && marker.version === 1 ? parseFreeformFactV1(question.text) : null;
|
||||
const newFact = question.scope === 'fact' && marker.version === 2 ? parseFreeformFactV2(question.text) : null;
|
||||
const expected = oldFact ? { version: 1 as const, fact: oldFact }
|
||||
: newFact ? { version: 2 as const, fact: newFact } : null;
|
||||
check(expected !== null && JSON.stringify(v.interpreted) === JSON.stringify(expected),
|
||||
'The saved freeform interpretation does not match its original question.');
|
||||
question.interpreted = expected;
|
||||
@@ -1107,14 +1111,14 @@ export function resolveTruthAction(
|
||||
p.id === current.player,
|
||||
'Waiting for the Truthtrance holder to ask a question.',
|
||||
);
|
||||
// Derive the v1 interpretation at submission, never from client state.
|
||||
// Derive the current 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 };
|
||||
const fact = parseFreeformFactV2(w.question.text);
|
||||
if (fact) w.question.interpreted = { version: 2, fact };
|
||||
}
|
||||
if (w.question.kind === 'mixedBattlePlan')
|
||||
check(effects.mixedBattleAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
|
||||
|
||||
Reference in new issue
Block a user