diff --git a/components/game-table.tsx b/components/game-table.tsx
index 6f233b63..3e941f54 100644
--- a/components/game-table.tsx
+++ b/components/game-table.tsx
@@ -1,4 +1,5 @@
'use client';
+import { MixedTraitorPromises } from './mixed-traitor-promises';
import { MixedBattlePromises } from './mixed-battle-promises';
import { TraitorDecisionPromises } from './traitor-decision-promises';
import { MovementPromises } from './movement-promises';
@@ -394,7 +395,9 @@ export function GameTable({
)
);
const canDeclineTraitor = traitorBattle?.traitorCallChoices?.includes(false) ?? true;
- const noTraitorReason = traitorBattle?.ownTraitorPromises?.some(promise => !promise.answer && !promise.fulfilled && !promise.released)
+ const noTraitorReason = traitorBattle?.ownTraitorPromises?.some(promise => !promise.answer && !promise.fulfilled && !promise.released) ||
+ (!canRevealTraitor && canDeclineTraitor && g.mixedTraitorPromises.some(promise =>
+ promise.battle === traitorBattle?.event && !promise.fulfilled && !promise.released))
? 'Your accepted Truthtrance answer requires declining this Traitor declaration.' : traitorPlan?.kwisatz
? 'Kwisatz Haderach prevents this leader from turning traitor.'
: 'You do not hold the opposing leader as a traitor.';
@@ -2060,6 +2063,7 @@ export function GameTable({
{g.status === 'playing' && }
{g.status === 'playing' && }
{g.status === 'playing' && }
+ {g.status === 'playing' && }
{g.guildBetrayalReaction ? (
) : g.richeseBetrayalReaction ? (
diff --git a/components/mixed-traitor-fields.tsx b/components/mixed-traitor-fields.tsx
new file mode 100644
index 00000000..f468240c
--- /dev/null
+++ b/components/mixed-traitor-fields.tsx
@@ -0,0 +1,53 @@
+'use client';
+
+import type { GameView } from '@/game/engine';
+import type { MixedActionExpression } from '@/game/mixed-action-question';
+import {
+ MIXED_TRAITOR_MAX_DEPTH, MIXED_TRAITOR_MAX_LEAVES, parseTraitorActionClaim, type TraitorActionClaim,
+} from '@/game/mixed-traitor-question';
+import { MixedActionFields, mixedActionInputError, type MixedActionConfig } from './mixed-action-fields';
+
+/** Question authors choose an original native declaration, never its private
+ * physical eligibility or the branch needed to honor the aggregate answer. */
+const config: MixedActionConfig = {
+ prefix: 'mixed-traitor',
+ legend: 'Mixed current-fact / this battle’s Traitor declaration',
+ actionLabel: 'Traitor declaration condition',
+ actionTiming: 'Traitor declaration · this revealed battle',
+ actionHelp: <>Traitor conditions concern the respondent’s one original Reveal or decline declaration in this revealed battle,
+ including a native Harkonnen declaration for an ally. Current facts are snapshotted at the answer.
+ An unknown current fact uses the conservative declaration commitment that guarantees the whole answer;
+ if no such answer is possible, the answer is I don’t know. A constant whole answer needs no declaration commitment.
+ The original declaration fulfills a remaining commitment before a later Karama or betrayal response can prevent its effect.
+ Native voter eligibility and physical Traitor rules still apply.>,
+ fallbackError: 'Choose a current fact and a Traitor declaration condition.',
+ maxLeaves: MIXED_TRAITOR_MAX_LEAVES,
+ maxDepth: MIXED_TRAITOR_MAX_DEPTH,
+ newAction: () => ({ call: true }),
+ parseAction: value => parseTraitorActionClaim(value),
+ renderAction: ({ value, id, disabled, onChange }) => <>
+
+
+ >,
+};
+
+export function mixedTraitorInputError(value: unknown, game: GameView, target: string): string | null {
+ return mixedActionInputError(value, game, target, config);
+}
+
+export function MixedTraitorFields(props: {
+ value: MixedActionExpression;
+ game: GameView;
+ target: string;
+ disabled: boolean;
+ onChange: (expression: MixedActionExpression) => void;
+}) {
+ return MixedActionFields({ ...props, config });
+}
diff --git a/components/mixed-traitor-promises.tsx b/components/mixed-traitor-promises.tsx
new file mode 100644
index 00000000..5760b46c
--- /dev/null
+++ b/components/mixed-traitor-promises.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import type { GameView } from '@/game/engine';
+import { truthQuestionText } from '@/game/truthtrance';
+import { CHEAP_HERO_TRAITOR } from '@/game/traitors';
+
+/** Keep only the own original question and whole answer visible. The native
+ * battle controls own declaration choices; no residual belongs in this panel. */
+export function MixedTraitorPromises({ game }: { game: GameView }) {
+ const own = game.mixedTraitorPromises.filter(promise => promise.player === game.me);
+ if (!own.length) return null;
+ const leaderName = (id: string) => id === CHEAP_HERO_TRAITOR ? 'Cheap Hero / Heroine'
+ : game.allLeaders.find(leader => leader.id === id)?.name ?? id;
+ return
+
Your mixed Traitor promises
+
{own.map(promise =>
+ Turn {promise.turn}, {game.combatLocations.find(location => location.id === promise.territory)?.name ?? promise.territory}: You answered {promise.answer ? 'Yes' : 'No'} to {truthQuestionText({ kind: 'mixedTraitorCall',
+ target: promise.player, battle: promise.battle, territory: promise.territory, mixed: promise.mixed }, leaderName,
+ id => game.combatLocations.find(location => location.id === id)?.name ?? id,
+ id => game.players.find(player => player.id === id)?.name ?? id)}
+ {promise.released ? ' Released: this whole commitment is no longer possible.'
+ : promise.fulfilled ? ' Fulfilled: this whole commitment is complete.' : ' This whole answer is binding for this battle’s Traitor declaration.'}
+
)}
+
Current facts are snapshotted at the answer. They create no promise to keep cards, spice or forces afterward.
+ Only the aggregate answer is shared. Individual fact answers and any reduced declaration commitment remain private.
+ AND requires every condition; OR requires at least one. No negates the whole expression, not each condition separately.
+
A constant whole answer is complete immediately without a declaration commitment.
+ A remaining commitment completes at your original native Reveal or decline declaration.
+ This concerns the declaration: a later Karama or betrayal response can still prevent its effect.
+ Use the ordinary battle controls for the currently legal declaration choices.
+ {' '}Mixed Traitor rules.
Yes binds Reveal for this original Traitor decision; No binds decline. This concerns your declaration, so a later Karama or betrayal response can still prevent its effect.
Answer the whole mixed claim. Current facts are snapshotted at the answer; No makes the whole expression false, not each condition separately.
+ A constant whole answer needs no declaration commitment. A remaining commitment concerns your original Reveal or decline declaration,
+ so a later Karama or betrayal response can still prevent its effect.
+ :
Yes binds Reveal for this original Traitor decision; No binds decline. This concerns your declaration, so a later Karama or betrayal response can still prevent its effect.