Connect Truthtrance questions about recorded predictions and storm knowledge
Add typed questions about the respondent's stored Bene Gesserit prediction faction/turn, active submitted storm dial and actually known native Advanced Fremen storm card. Use the same eligible private facts for authoritative validation and target-only answer projection. Preserve unknown/rephrase/save continuation, compound aggregate privacy and physical card custody. Future promises, freeform interpretation and strategic AI question selection remain incomplete; no mode or publication gate opens. Connect four question controls with all numeric comparisons, canonical faction choices and shared input guards. All four AI profiles answer the owner-only verified scalar. Independent source/privacy review and integration checks fixed missing comparisons, parent selector mapping and invalid submission guards. Thirteen new cases and the 44-case focused union cover normal/unknown/dishonest answers, private views, JSON and authenticated SQLite restoration, interrupted storm-peek continuation, stale/duplicate/competing requests and corrupt saved predicates without writes. Validation: types, lint, 4662 offline tests, production build and 43 HTTP tests passed with unchanged source. Two genuine-setup four-profile games completed without rejected actions: Basic 192 actions/five periodic JSON rounds and Advanced 518/fourteen. Exact Treachery/force/elite custody and all eight final restored seat views pass. Three explicitly scripted legal questions use naturally held cards, covering prediction faction, prediction turn and submitted storm dial. Forecast questioning is not exercised by these complete games; the samples do not measure autonomous AI asking strategy or strength. The separate Advanced browser exercise uses real prediction and forecast progression, staging only physical Truthtrance acquisition by relocation. Invalid dial input is blocked; an unknown answer can be rephrased; both compound prediction clauses and the forecast comparison work. Refresh after submission and final resolution restores history unknown/Yes/Yes and the remaining Shield. Both physical Truthtrances are discarded once, unrelated resources remain unchanged and private projection passes. SQLite tests establish pending-answer restoration; the browser observation does not claim a paused pending answer after reconnect. After the reported outage, all 767 opening games and original seat/recovery records were preserved (801 after HTTP checks). All 768 rooms and original records immediately before verification also remained unchanged. Database integrity and foreign keys passed. Reused the healthy server without restart, reset or recurring automation. Verified source tree: 09daaac288cb91ea407f4573f853a59d307e12ed30d2eccd39e671a47336f40d
This commit is contained in:
1 parent
8e1cd7b75b
commit
8906b41bda
14 files changed
+989
-13
No files matched your search
+2
-1
@@ -27,6 +27,7 @@ import { quoteJacurutuBattleIncome } from './discovery-battle';
|
||||
import { greatMakerSignature, greatMakerMajority, validateGreatMaker, type GreatMaker } from './great-maker';
|
||||
import { quoteNexusChoamTrade, validateNexusChoamTrade, nexusChoamTradeSignature, type NexusChoamTrade } from './nexus-choam-trade';
|
||||
import { EMPEROR_NEXUS_REVIVALS, emperorNexusEvent, emperorNexusSignature, emperorNexusModeSupported, emperorNexusPools, emperorNexusRevivalElites, validateEmperorNexusRevival, type NexusEmperorRevival } from './nexus-emperor-secret-ally';
|
||||
import { truthKnowledgeOf } from './truthtrance-knowledge';
|
||||
import { createNexusGuildSecretAlly, validateNexusGuildSecretAlly, quoteNexusGuildSecretShipment, type NexusGuildSecretAllyReceipt } from './nexus-guild-secret-ally';
|
||||
import { shipmentAvailable } from './shipment-opportunity';
|
||||
import { createNexusGuildCunning, validateNexusGuildCunning, nexusGuildCunningMoves, type NexusGuildCunningReceipt } from './nexus-guild-cunning';
|
||||
@@ -23482,7 +23483,7 @@ export function viewGame(state: Game, id: string) {
|
||||
g.truthtrance?.stage === 'answer' &&
|
||||
g.truthtrance.question?.target === id &&
|
||||
g.truthtrance.question.kind === 'fact'
|
||||
? truthFactAnswer(me, g.truthtrance.question.fact)
|
||||
? truthFactAnswer(me, g.truthtrance.question.fact, truthKnowledgeOf(g, me))
|
||||
: null,
|
||||
schema: g.schema,
|
||||
botsPending: g.botsPending ?? false,
|
||||
|
||||
@@ -2213,6 +2213,29 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-knowledge',
|
||||
title: 'Truthtrance: recorded predictions and storm knowledge',
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Ask about the target’s recorded Bene Gesserit prediction, submitted storm dial or currently known Fremen storm card, with a single fact or an AND/OR combination.',
|
||||
steps: [
|
||||
'Choose a recorded prediction faction, compare its turn, or compare the target’s current submitted storm dial or known Fremen forecast. Exactly, at least and at most are available for numeric facts.',
|
||||
'These questions describe a choice already recorded or information currently known. They do not require anyone to win on a future turn, change a choice, or make the storm move a promised distance.',
|
||||
'A missing prediction, an unsubmitted or expired storm dial, or an unknown forecast yields I don’t know. A forecast question never gives another faction the Fremen’s private knowledge.',
|
||||
'Only the target sees the verified answer before replying. The published response gives the combined result, without identifying which clause matched or separately displaying the stored values.',
|
||||
'As with other Truthtrance questions, an unknown answer lets the holder ask something else or save the card. A definite answer spends the physical card once and restores the interrupted interaction.',
|
||||
],
|
||||
related: ['card-truthtrance', 'truthtrance-spice', 'privacy'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Stored prediction and entitled current storm knowledge use shared three-valued fact evaluation; no future commitment or unknown deck lookup.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Four fact choices, numeric comparisons, public faction names, bounded inputs and compound question previews.', evidence: ['components/truthtrance-knowledge.tsx', 'components/truthtrance.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'All four profiles answer the owner-only verified scalar through the existing Truthtrance flow. Strategic question selection and freeform understanding remain incomplete.', evidence: ['game/bots.ts', 'tests/truthtrance-knowledge.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Partial', detail: 'Current-knowledge semantics, unknown cases and original FAQ authority are explicit.', evidence: ['docs/TRUTHTRANCE_KNOWLEDGE.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused rules, UI, compound privacy, bots, JSON and production SQLite restoration; broader Truthtrance acceptance remains open.', evidence: ['tests/truthtrance-knowledge.test.ts', 'tests/truthtrance-knowledge-controls.test.tsx', 'tests/truthtrance-knowledge-recovery.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-shipment',
|
||||
title: 'Truthtrance: shipment promises',
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { FACTIONS, type FactionId } from './catalog';
|
||||
import type { Game, Player } from './engine';
|
||||
|
||||
type Comparison = 'eq' | 'gte' | 'lte';
|
||||
export type KnowledgeFact =
|
||||
| { kind: 'prediction'; field: 'faction'; faction: FactionId }
|
||||
| { kind: 'prediction'; field: 'turn'; compare: Comparison; value: number }
|
||||
| { kind: 'stormDial' | 'stormForecast'; compare: Comparison; value: number };
|
||||
|
||||
/** Only values already chosen or actually known by this respondent. */
|
||||
export type TruthKnowledge = {
|
||||
prediction: { faction: FactionId; turn: number } | null;
|
||||
stormDial: number | null;
|
||||
stormForecast: number | null;
|
||||
};
|
||||
|
||||
export function isKnowledgeFact(value: unknown): value is KnowledgeFact {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const kind = (value as { kind?: unknown }).kind;
|
||||
return kind === 'prediction' || kind === 'stormDial' || kind === 'stormForecast';
|
||||
}
|
||||
|
||||
export function knowledgeFactInputError(value: unknown): string | null {
|
||||
if (!isKnowledgeFact(value)) return 'Choose a stored prediction, storm dial or known forecast.';
|
||||
if (value.kind === 'prediction') {
|
||||
if (value.field === 'faction')
|
||||
return FACTIONS.some((faction) => faction.id === value.faction)
|
||||
? null : 'Choose a known faction for the recorded prediction.';
|
||||
if (value.field !== 'turn') return 'Choose the prediction faction or turn.';
|
||||
}
|
||||
if (value.compare !== 'eq' && value.compare !== 'gte' && value.compare !== 'lte')
|
||||
return 'Compare the stored value using exactly, at least, or at most.';
|
||||
const minimum = value.kind === 'stormDial' ? 0 : 1;
|
||||
const maximum = value.kind === 'prediction' ? 10 : value.kind === 'stormDial' ? 20 : 6;
|
||||
return Number.isSafeInteger(value.value) && value.value >= minimum && value.value <= maximum
|
||||
? null : `Choose a whole ${value.kind === 'prediction' ? 'prediction turn' : value.kind === 'stormDial' ? 'storm dial' : 'forecast distance'} from ${minimum} to ${maximum}.`;
|
||||
}
|
||||
|
||||
export function parseKnowledgeFact(value: unknown): KnowledgeFact {
|
||||
const error = knowledgeFactInputError(value);
|
||||
if (error || !isKnowledgeFact(value)) throw new Error(error ?? 'Choose a valid private-knowledge question.');
|
||||
if (value.kind === 'prediction' && value.field === 'faction')
|
||||
return { kind: value.kind, field: value.field, faction: value.faction };
|
||||
if (value.kind === 'prediction')
|
||||
return { kind: value.kind, field: value.field, compare: value.compare, value: value.value };
|
||||
return { kind: value.kind, compare: value.compare, value: value.value };
|
||||
}
|
||||
|
||||
export function truthKnowledgeOf(
|
||||
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown'>,
|
||||
p: Pick<Player, 'id' | 'faction' | 'prediction'>,
|
||||
): TruthKnowledge {
|
||||
const dial = g.stormDials[p.id];
|
||||
return {
|
||||
prediction: p.faction === 'beneGesserit' && p.prediction
|
||||
? { ...p.prediction } : null,
|
||||
stormDial: g.phase === 0 && g.stormDialers.includes(p.id) &&
|
||||
Number.isSafeInteger(dial) && dial >= (g.turn === 1 ? 0 : 1) && dial <= (g.turn === 1 ? 20 : 3)
|
||||
? dial : null,
|
||||
stormForecast: p.faction === 'fremen' && g.advanced && g.stormCardKnown &&
|
||||
Number.isSafeInteger(g.stormCard) && g.stormCard! >= 1 && g.stormCard! <= 6
|
||||
? g.stormCard : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function knowledgeFactAnswer(
|
||||
fact: KnowledgeFact,
|
||||
knowledge?: TruthKnowledge,
|
||||
): 'yes' | 'no' | 'unknown' {
|
||||
const error = knowledgeFactInputError(fact);
|
||||
if (error) throw new Error(error);
|
||||
if (!knowledge) return 'unknown';
|
||||
if (fact.kind === 'prediction' && fact.field === 'faction')
|
||||
return knowledge.prediction === null ? 'unknown'
|
||||
: knowledge.prediction.faction === fact.faction ? 'yes' : 'no';
|
||||
const actual = fact.kind === 'prediction' ? knowledge.prediction?.turn
|
||||
: fact.kind === 'stormDial' ? knowledge.stormDial : knowledge.stormForecast;
|
||||
if (actual === null || actual === undefined) return 'unknown';
|
||||
return (fact.compare === 'eq' ? actual === fact.value
|
||||
: fact.compare === 'gte' ? actual >= fact.value : actual <= fact.value) ? 'yes' : 'no';
|
||||
}
|
||||
|
||||
export function knowledgeFactText(fact: KnowledgeFact): string {
|
||||
if (fact.kind === 'prediction' && fact.field === 'faction')
|
||||
return `your recorded Bene Gesserit prediction names ${FACTIONS.find((f) => f.id === fact.faction)!.name}`;
|
||||
const comparison = fact.compare === 'eq' ? 'exactly' : fact.compare === 'gte' ? 'at least' : 'at most';
|
||||
return fact.kind === 'prediction'
|
||||
? `the turn in your recorded Bene Gesserit prediction is ${comparison} ${fact.value}`
|
||||
: fact.kind === 'stormDial'
|
||||
? `your submitted storm dial for this turn is ${comparison} ${fact.value}`
|
||||
: `the storm card currently known to you as Fremen shows ${comparison} ${fact.value} sectors`;
|
||||
}
|
||||
+11
-2
@@ -26,8 +26,10 @@ import { treacheryDeck } from './cards';
|
||||
import { CHEAP_HERO_TRAITOR } from './traitors';
|
||||
import { DUKE_VIDAL_ID } from './duke-vidal';
|
||||
import { canUseAsTruthtranceRole } from './shrine';
|
||||
import { isKnowledgeFact, parseKnowledgeFact, knowledgeFactAnswer, knowledgeFactText, truthKnowledgeOf, type KnowledgeFact, type TruthKnowledge } from './truthtrance-knowledge';
|
||||
|
||||
export type TruthFact =
|
||||
| KnowledgeFact
|
||||
| CardCountFact
|
||||
| HandInventoryFact
|
||||
| { kind: 'hand'; name: string }
|
||||
@@ -89,6 +91,10 @@ function parseFact(
|
||||
'Truthtrance supports at most 16 clauses and four levels of grouping.',
|
||||
);
|
||||
const v = record(value);
|
||||
if (isKnowledgeFact(v)) {
|
||||
try { return parseKnowledgeFact(v); }
|
||||
catch (error) { throw new TruthError(error instanceof Error ? error.message : String(error)); }
|
||||
}
|
||||
if (v.kind === 'handCount' || v.kind === 'handInventory') {
|
||||
try {
|
||||
return v.kind === 'handCount'
|
||||
@@ -306,7 +312,9 @@ export function queuedTruthCardMatches(
|
||||
export function truthFactAnswer(
|
||||
p: Pick<Player, 'hand' | 'traitors' | 'traitorChoices' | 'spice'>,
|
||||
fact: TruthFact,
|
||||
knowledge?: TruthKnowledge,
|
||||
): TruthAnswer {
|
||||
if (isKnowledgeFact(fact)) return knowledgeFactAnswer(fact, knowledge);
|
||||
if (fact.kind === 'handInventory')
|
||||
return handInventoryFactMatches(p.hand, fact) ? 'yes' : 'no';
|
||||
if (fact.kind === 'handCount')
|
||||
@@ -327,7 +335,7 @@ export function truthFactAnswer(
|
||||
: p.spice <= fact.value;
|
||||
return matches ? 'yes' : 'no';
|
||||
}
|
||||
const answers = fact.terms.map((term) => truthFactAnswer(p, term));
|
||||
const answers = fact.terms.map((term) => truthFactAnswer(p, term, knowledge));
|
||||
if (fact.kind === 'and')
|
||||
return answers.includes('no')
|
||||
? 'no'
|
||||
@@ -350,6 +358,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 (isKnowledgeFact(f)) return knowledgeFactText(f);
|
||||
if (f.kind === 'handInventory') return handInventoryFactText(f);
|
||||
if (f.kind === 'handCount') return cardCountFactText(f);
|
||||
if (f.kind === 'hand') return `you hold ${f.name}`;
|
||||
@@ -525,7 +534,7 @@ export function resolveTruthAction(
|
||||
);
|
||||
if (q.kind === 'fact')
|
||||
check(
|
||||
a.answer === truthFactAnswer(p, q.fact),
|
||||
a.answer === truthFactAnswer(p, q.fact, truthKnowledgeOf(g, p)),
|
||||
'Answer this fact question truthfully.',
|
||||
);
|
||||
if (q.kind === 'shipment') {
|
||||
|
||||
Reference in new issue
Block a user