Support Truthtrance on Basic Ixian auction knowledge

This commit is contained in:
Quintine committed 2026-10-28 00:30:59 +11:00
1 parent 910e1f9b40
commit c3f37a574d
11 files changed
+248 -13

No files matched your search

+5 -4
View File
@@ -3268,11 +3268,12 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Cards',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Ask about a recorded prediction, submitted storm dial, Fremen forecast or Atreides Spice Blow or auction peek, with a single fact or an AND/OR combination.',
summary: 'Ask about a recorded prediction, submitted storm dial, Fremen forecast, Atreides peek or Basic Ixian auction group, 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.',
'Ask whether the ordinary auction card currently inspected by Atreides has a named Treachery Card face. If the inspection was canceled or the lot is no longer available, the answer is I don’t know. Special auctions and other factions’ occupied Homeworld inspections are not yet included.',
'In Basic Bidding, ask Ixians whether their recorded inspected auction group contains a named card. This refers to the recorded group, including cards later sold, rather than what remains for sale. If the inspection is unavailable or belongs to another player, 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.',
@@ -3281,11 +3282,11 @@ export const RULE_TOPICS: RuleTopic[] = [
],
related: ['card-truthtrance', 'truthtrance-spice', 'privacy'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Stored prediction, entitled storm knowledge, native Atreides Spice and ordinary auction peeks use shared three-valued fact evaluation; special auction and occupied inspection remain separate.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Six fact choices, numeric comparisons, public faction and card names, bounded inputs and compound question previews.', evidence: ['components/truthtrance-knowledge.tsx', 'components/mixed-shipment-fields.tsx'] },
{ area: 'Implementation', status: 'Partial', detail: 'Stored prediction, entitled storm knowledge, native Atreides peeks and the Basic Ixian recorded auction group use shared three-valued fact evaluation; special auction and occupied inspection remain separate.', evidence: ['game/truthtrance-knowledge.ts', 'game/truthtrance.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Fact choices, numeric comparisons, public faction and 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, native Atreides peeks 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-auction-peek-runtime.test.ts', 'tests/truthtrance-knowledge-recovery.test.ts'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused rules, UI, native Atreides peeks and Ixian auction knowledge 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-ix-pool-knowledge.test.ts', 'tests/truthtrance-ix-pool-runtime.test.ts', 'tests/truthtrance-spice-peek-runtime.test.ts', 'tests/truthtrance-auction-peek-runtime.test.ts', 'tests/truthtrance-knowledge-recovery.test.ts'] },
],
},
{
+21 -4
View File
@@ -15,6 +15,7 @@ export type KnowledgeFact =
| { kind: 'prediction'; field: 'turn'; compare: Comparison; value: number }
| { kind: 'spicePeek'; value: string }
| { kind: 'auctionPeek'; name: string }
| { kind: 'ixPoolPeek'; name: string }
| { kind: 'stormDial' | 'stormForecast'; compare: Comparison; value: number };
/** Only values already chosen or actually known by this respondent. */
@@ -24,17 +25,19 @@ export type TruthKnowledge = {
stormForecast: number | null;
spicePeek: string | null;
auctionPeek: string | null;
/** The authorized respondent's recorded Ixian group, not remaining lots. */
ixPoolPeek?: readonly 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' || kind === 'spicePeek' || kind === 'auctionPeek';
return kind === 'prediction' || kind === 'stormDial' || kind === 'stormForecast' || kind === 'spicePeek' || kind === 'auctionPeek' || kind === 'ixPoolPeek';
}
export function knowledgeFactInputError(value: unknown): string | null {
if (!isKnowledgeFact(value)) return 'Choose a stored prediction, storm dial or known forecast.';
if (value.kind === 'auctionPeek')
if (value.kind === 'auctionPeek' || value.kind === 'ixPoolPeek')
return typeof value.name === 'string' && TRUTH_CARD_NAMES.includes(value.name)
? null : 'Choose a known Treachery Card name.';
if (value.kind === 'spicePeek')
@@ -57,7 +60,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 === 'auctionPeek') return { kind: value.kind, name: value.name };
if (value.kind === 'auctionPeek' || value.kind === 'ixPoolPeek') return { kind: value.kind, name: value.name };
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 };
@@ -67,7 +70,7 @@ export function parseKnowledgeFact(value: unknown): KnowledgeFact {
}
export function truthKnowledgeOf(
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown' | 'spicePeekKnown' | 'spiceDeck' | 'auction' | 'richeseAuction' | 'ixAuction'>,
g: Pick<Game, 'phase' | 'turn' | 'advanced' | 'stormDials' | 'stormDialers' | 'stormCard' | 'stormCardKnown' | 'spicePeekKnown' | 'spiceDeck' | 'auction' | 'richeseAuction' | 'ixAuction' | 'ixAuctionKnown' | 'homeworldOccupationPreview'>,
p: Pick<Player, 'id' | 'faction' | 'prediction'>,
): TruthKnowledge {
const dial = g.stormDials[p.id];
@@ -77,6 +80,14 @@ export function truthKnowledgeOf(
// The private lot is read only after the same inspection entitlement used by
// the ordinary auction view has resolved successfully for this faction.
const lotName = lot?.peekKnown ? lot.cards[lot.index]?.name : undefined;
// Basic native Ixians may answer from their own successful inspection. The
// record describes a known group, not the currently unsold auction cards.
const ixKnown = g.phase === 3 && !g.advanced && p.faction === 'ixians'
? g.ixAuctionKnown : null;
const ixPool = ixKnown && ixKnown.turn === g.turn &&
(ixKnown.controller === undefined || ixKnown.controller === p.id) &&
(!g.homeworldOccupationPreview || ixKnown.controller === p.id)
? ixKnown.cards.map(card => card.name) : null;
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)
@@ -92,6 +103,7 @@ export function truthKnowledgeOf(
? g.stormCard : null,
spicePeek: peekFace,
auctionPeek: typeof lotName === 'string' && lotName.length > 0 ? lotName : null,
ixPoolPeek: ixPool,
};
}
@@ -105,6 +117,9 @@ export function knowledgeFactAnswer(
if (fact.kind === 'auctionPeek')
return knowledge.auctionPeek == null ? 'unknown'
: knowledge.auctionPeek === fact.name ? 'yes' : 'no';
if (fact.kind === 'ixPoolPeek')
return knowledge.ixPoolPeek == null ? 'unknown'
: knowledge.ixPoolPeek.includes(fact.name) ? 'yes' : 'no';
if (fact.kind === 'spicePeek')
return knowledge.spicePeek === null || knowledge.spicePeek === undefined ? 'unknown'
: knowledge.spicePeek === fact.value ? 'yes' : 'no';
@@ -121,6 +136,8 @@ export function knowledgeFactAnswer(
export function knowledgeFactText(fact: KnowledgeFact): string {
if (fact.kind === 'auctionPeek')
return `the current auction card you inspected as Atreides is ${fact.name}`;
if (fact.kind === 'ixPoolPeek')
return `your recorded Ixian auction group includes ${fact.name}`;
if (fact.kind === 'spicePeek') {
const face = fact.value === 'worm' ? 'a sandworm' : fact.value === 'sandtrout' ? 'Sandtrout'
: `a Spice Blow at ${territory(fact.value).name}`;