Support Truthtrance on Atreides spice foresight
This commit is contained in:
1 parent
24255905ed
commit
235c2eb03f
11 files changed
+255
-16
No files matched your search
+6
-5
@@ -3242,13 +3242,14 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-knowledge',
|
||||
title: 'Truthtrance: recorded predictions and storm knowledge',
|
||||
title: 'Truthtrance: recorded predictions and private forecasts',
|
||||
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.',
|
||||
summary: 'Ask about a recorded prediction, submitted storm dial, Fremen forecast or Atreides Spice Blow peek, 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.',
|
||||
'Ask whether Atreides’ currently known next Spice Card is a sandworm, Sandtrout, or a blow at a named printed territory. If the peek was canceled or is no longer available, the answer is I don’t know.',
|
||||
'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.',
|
||||
'For a supported Basic dialer who has not submitted, the separate combined turn-action question can ask about their future exact choice; that is a commitment, not a fact about an already sealed dial.',
|
||||
@@ -3257,11 +3258,11 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
],
|
||||
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: 'Implementation', status: 'Partial', detail: 'Stored prediction, entitled storm knowledge and an active Atreides Spice peek use shared three-valued fact evaluation; no future commitment or unentitled deck lookup.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Five fact choices, numeric comparisons, public faction and Spice Card names, bounded inputs and compound question previews.', evidence: ['components/truthtrance-knowledge.tsx', 'components/mixed-shipment-fields.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'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused rules, UI, native Atreides peek and all-profile answers; earlier storm cases include JSON and SQLite restoration. Broader Truthtrance acceptance remains open.', evidence: ['tests/truthtrance-knowledge.test.ts', 'tests/truthtrance-knowledge-controls.test.tsx', 'tests/truthtrance-spice-peek-runtime.test.ts', 'tests/truthtrance-knowledge-recovery.test.ts'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { FACTIONS, type FactionId } from './catalog';
|
||||
import { SPICE_CARDS } from './cards';
|
||||
import { territory } from './board';
|
||||
import type { Game, Player } from './engine';
|
||||
|
||||
type Comparison = 'eq' | 'gte' | 'lte';
|
||||
/** Publicly selectable identities; a territory names a blow, including a
|
||||
* Discovery card at that same printed territory. No deck contents enter here. */
|
||||
export const SPICE_PEEK_VALUES: readonly string[] = [
|
||||
'worm', 'sandtrout', ...SPICE_CARDS.map(([id]) => id),
|
||||
];
|
||||
export type KnowledgeFact =
|
||||
| { kind: 'prediction'; field: 'faction'; faction: FactionId }
|
||||
| { kind: 'prediction'; field: 'turn'; compare: Comparison; value: number }
|
||||
| { kind: 'spicePeek'; value: string }
|
||||
| { kind: 'stormDial' | 'stormForecast'; compare: Comparison; value: number };
|
||||
|
||||
/** Only values already chosen or actually known by this respondent. */
|
||||
@@ -12,16 +20,20 @@ export type TruthKnowledge = {
|
||||
prediction: { faction: FactionId; turn: number } | null;
|
||||
stormDial: number | null;
|
||||
stormForecast: number | null;
|
||||
spicePeek: string | 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';
|
||||
return kind === 'prediction' || kind === 'stormDial' || kind === 'stormForecast' || kind === 'spicePeek';
|
||||
}
|
||||
|
||||
export function knowledgeFactInputError(value: unknown): string | null {
|
||||
if (!isKnowledgeFact(value)) return 'Choose a stored prediction, storm dial or known forecast.';
|
||||
if (value.kind === 'spicePeek')
|
||||
return typeof value.value === 'string' && SPICE_PEEK_VALUES.includes(value.value)
|
||||
? null : 'Choose a printed Spice Blow territory, a sandworm, or Sandtrout.';
|
||||
if (value.kind === 'prediction') {
|
||||
if (value.field === 'faction')
|
||||
return FACTIONS.some((faction) => faction.id === value.faction)
|
||||
@@ -39,6 +51,7 @@ export function knowledgeFactInputError(value: unknown): string | null {
|
||||
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 === 'spicePeek') return { kind: value.kind, value: value.value };
|
||||
if (value.kind === 'prediction' && value.field === 'faction')
|
||||
return { kind: value.kind, field: value.field, faction: value.faction };
|
||||
if (value.kind === 'prediction')
|
||||
@@ -47,10 +60,16 @@ export function parseKnowledgeFact(value: unknown): KnowledgeFact {
|
||||
}
|
||||
|
||||
export function truthKnowledgeOf(
|
||||
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown'>,
|
||||
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown' | 'spicePeekKnown' | 'spiceDeck'>,
|
||||
p: Pick<Player, 'id' | 'faction' | 'prediction'>,
|
||||
): TruthKnowledge {
|
||||
const dial = g.stormDials[p.id];
|
||||
const peek = g.phase === 5 && p.faction === 'atreides' && g.spicePeekKnown
|
||||
? g.spiceDeck[0] : undefined;
|
||||
const peekFace = peek && 'worm' in peek && peek.worm === true ? 'worm'
|
||||
: peek && 'sandtrout' in peek && peek.sandtrout === true ? 'sandtrout'
|
||||
: peek && 'territory' in peek && typeof peek.territory === 'string' && SPICE_PEEK_VALUES.includes(peek.territory)
|
||||
? peek.territory : null;
|
||||
return {
|
||||
prediction: p.faction === 'beneGesserit' && p.prediction
|
||||
? { ...p.prediction } : null,
|
||||
@@ -60,6 +79,7 @@ export function truthKnowledgeOf(
|
||||
stormForecast: p.faction === 'fremen' && g.advanced && g.stormCardKnown &&
|
||||
Number.isSafeInteger(g.stormCard) && g.stormCard! >= 1 && g.stormCard! <= 6
|
||||
? g.stormCard : null,
|
||||
spicePeek: peekFace,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,6 +90,9 @@ export function knowledgeFactAnswer(
|
||||
const error = knowledgeFactInputError(fact);
|
||||
if (error) throw new Error(error);
|
||||
if (!knowledge) return 'unknown';
|
||||
if (fact.kind === 'spicePeek')
|
||||
return knowledge.spicePeek === null || knowledge.spicePeek === undefined ? 'unknown'
|
||||
: knowledge.spicePeek === fact.value ? 'yes' : 'no';
|
||||
if (fact.kind === 'prediction' && fact.field === 'faction')
|
||||
return knowledge.prediction === null ? 'unknown'
|
||||
: knowledge.prediction.faction === fact.faction ? 'yes' : 'no';
|
||||
@@ -81,6 +104,11 @@ export function knowledgeFactAnswer(
|
||||
}
|
||||
|
||||
export function knowledgeFactText(fact: KnowledgeFact): string {
|
||||
if (fact.kind === 'spicePeek') {
|
||||
const face = fact.value === 'worm' ? 'a sandworm' : fact.value === 'sandtrout' ? 'Sandtrout'
|
||||
: `a Spice Blow at ${territory(fact.value).name}`;
|
||||
return `the next Spice Card currently known to you as Atreides is ${face}`;
|
||||
}
|
||||
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';
|
||||
|
||||
Reference in new issue
Block a user