Add verified current Face Dancer questions to Truthtrance
Ask about a currently held unrevealed printed identity through shared facts and AND/OR authoring. Keep ordinary Traitor meaning separate, candidate identities public and only the aggregate answer published. Existing answer, physical disposal and continuation flow need no new AI strategy. Verified: 12 affected offline files86/86, types/lint and production build; twelve actual field/question/gameplay/GameTable SSR journeys and sixteen legal profile answers. Native Basic/Advanced sources and actual Mentat replacement prove a current Yes creates no future reveal or retention promise. Existing mixed-shipment admission stays unchanged. No saved-room, D1, reset, restart or deployment operation; broader Truthtrance/mode assurance remains.
This commit is contained in:
1 parent
eff668b202
commit
6ea691255a
13 files changed
+1021
-3
No files matched your search
@@ -8,6 +8,7 @@ import { TRUTH_CARD_NAMES } from '@/game/truthtrance-card-names';
|
||||
import { SHIPMENT_EXPRESSION_MAX_DEPTH, SHIPMENT_EXPRESSION_MAX_LEAVES } from '@/game/shipment-promises';
|
||||
import { isKnowledgeFact, knowledgeFactInputError } from '@/game/truthtrance-knowledge';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import { DUKE_VIDAL_ID } from '@/game/duke-vidal';
|
||||
import { Input } from './ui/input';
|
||||
import { Button } from './ui/button';
|
||||
import { CardCountFields } from './truthtrance-card-count';
|
||||
@@ -16,6 +17,8 @@ import { ForceCountFields, forceCountInputError } from './truthtrance-force-coun
|
||||
import { ShipmentClauseFields, invalidShipmentClause } from './shipment-claim-fields';
|
||||
import { NoFieldValueFields } from './truthtrance-no-field-value';
|
||||
import { parseNoFieldValueFact } from '@/game/truthtrance-no-field-value';
|
||||
import { FaceDancerFactFields } from './truthtrance-face-dancer';
|
||||
import { parseFaceDancerFact } from '@/game/truthtrance-face-dancer';
|
||||
|
||||
export function invalidTruthFact(fact: TruthFact, game: GameView, target: string): boolean {
|
||||
if (fact.kind === 'and' || fact.kind === 'or') {
|
||||
@@ -28,6 +31,15 @@ export function invalidTruthFact(fact: TruthFact, game: GameView, target: string
|
||||
try { parseNoFieldValueFact(fact); return false; }
|
||||
catch { return true; }
|
||||
}
|
||||
if (fact.kind === 'faceDancer') {
|
||||
try {
|
||||
parseFaceDancerFact(fact, [
|
||||
...game.allLeaders.filter((leader) => leader.id !== DUKE_VIDAL_ID && leader.id !== CHEAP_HERO_TRAITOR).map((leader) => leader.id),
|
||||
CHEAP_HERO_TRAITOR,
|
||||
]);
|
||||
return false;
|
||||
} catch { return true; }
|
||||
}
|
||||
if (isKnowledgeFact(fact)) return !!knowledgeFactInputError(fact);
|
||||
if (fact.kind === 'forceCount') {
|
||||
return !!forceCountInputError(fact, game, game.players.find(player => player.id === target)!);
|
||||
@@ -48,6 +60,7 @@ export function TruthFactFields({ value: fact, game, target, onChange }: {
|
||||
handCount: { kind: 'handCount', name: 'Shield', compare: 'gte', value: 2 },
|
||||
handInventory: { kind: 'handInventory', category: 'all', compare: 'gte', value: 2 },
|
||||
traitor: { kind: 'traitor', leader: game.allLeaders[0].id },
|
||||
faceDancer: { kind: 'faceDancer', leader: game.allLeaders.find((leader) => leader.id !== DUKE_VIDAL_ID)?.id ?? CHEAP_HERO_TRAITOR },
|
||||
predictionFaction: { kind: 'prediction', field: 'faction', faction: 'atreides' },
|
||||
predictionTurn: { kind: 'prediction', field: 'turn', compare: 'gte', value: 1 },
|
||||
stormDial: { kind: 'stormDial', compare: 'gte', value: 0 },
|
||||
@@ -65,6 +78,7 @@ export function TruthFactFields({ value: fact, game, target, onChange }: {
|
||||
<option value="handCount">Current number of a named card</option>
|
||||
<option value="handInventory">Current hand size or primary card role</option>
|
||||
<option value="traitor">Selected a traitor</option>
|
||||
<option value="faceDancer">Currently unrevealed Face Dancer</option>
|
||||
<option value="predictionFaction">Stored faction prediction</option>
|
||||
<option value="predictionTurn">Stored turn prediction</option>
|
||||
<option value="stormDial">Known storm dial</option>
|
||||
@@ -80,7 +94,8 @@ export function TruthFactFields({ value: fact, game, target, onChange }: {
|
||||
{TRUTH_CARD_NAMES.map(name => <option key={name}>{name}</option>)}
|
||||
</select>
|
||||
</label> : fact.kind === 'handCount' || fact.kind === 'handInventory' ?
|
||||
<CardCountFields value={fact} onChange={onChange} /> : fact.kind === 'noFieldValue' ?
|
||||
<CardCountFields value={fact} onChange={onChange} /> : fact.kind === 'faceDancer' ?
|
||||
<FaceDancerFactFields value={fact} leaders={game.allLeaders} onChange={onChange} /> : fact.kind === 'noFieldValue' ?
|
||||
<NoFieldValueFields value={fact} onChange={onChange} /> : fact.kind === 'forceCount' ?
|
||||
<ForceCountFields value={fact} game={game} player={game.players.find(player => player.id === target)!} onChange={onChange} /> :
|
||||
fact.kind === 'prediction' || fact.kind === 'stormDial' || fact.kind === 'stormForecast' ?
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import { DUKE_VIDAL_ID } from '@/game/duke-vidal';
|
||||
import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
|
||||
import { parseFaceDancerFact, type FaceDancerFact } from '@/game/truthtrance-face-dancer';
|
||||
|
||||
export function FaceDancerFactFields({
|
||||
value,
|
||||
leaders,
|
||||
onChange,
|
||||
}: {
|
||||
value: FaceDancerFact;
|
||||
leaders: readonly { id: string; name: string }[];
|
||||
onChange: (fact: FaceDancerFact) => void;
|
||||
}) {
|
||||
const identities = [
|
||||
...leaders.filter((leader) => leader.id !== DUKE_VIDAL_ID && leader.id !== CHEAP_HERO_TRAITOR),
|
||||
{ id: CHEAP_HERO_TRAITOR, name: 'Cheap Hero / Heroine' },
|
||||
];
|
||||
const allowed = identities.map((identity) => identity.id);
|
||||
let error: string | null = null;
|
||||
try { parseFaceDancerFact(value, allowed); }
|
||||
catch (cause) { error = cause instanceof Error ? cause.message : 'Choose a printed Face Dancer identity.'; }
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
Face Dancer identity
|
||||
<select
|
||||
aria-label="Face Dancer identity"
|
||||
value={value.leader}
|
||||
aria-invalid={!!error}
|
||||
onChange={(event) => {
|
||||
try {
|
||||
onChange(parseFaceDancerFact({ kind: 'faceDancer', leader: event.currentTarget.value }, allowed));
|
||||
} catch { return; }
|
||||
}}
|
||||
>
|
||||
{identities.map((identity) => (
|
||||
<option key={identity.id} value={identity.id}>{identity.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<p className="fine">
|
||||
This asks whether the respondent currently holds an unrevealed Face
|
||||
Dancer for this printed identity. A revealed, absent or returned card
|
||||
means No.
|
||||
</p>
|
||||
<p className="fine">
|
||||
This is a current fact at the answer, not a promise to reveal a Face
|
||||
Dancer later. Ordinary traitor questions still ask only about ordinary
|
||||
Traitor Cards.
|
||||
</p>
|
||||
<p className="fine">
|
||||
Only the aggregate Truthtrance answer is published. This does not
|
||||
physically reveal a Face Dancer or identify which part of an AND/OR
|
||||
claim is true.
|
||||
</p>
|
||||
{error && <p className="notice" role="alert">{error}</p>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user