Combine current facts with native Traitor decisions
This commit is contained in:
1 parent
812f77e348
commit
13398aa76e
22 files changed
+1512
-17
No files matched your search
@@ -1374,6 +1374,10 @@ function policyActions(g: GameView): Action[] {
|
||||
const answer = g.truthBidAnswers?.[0];
|
||||
return answer ? [{ type: 'truthAnswer', answer }] : [];
|
||||
}
|
||||
if (w.question.kind === 'mixedTraitorCall') {
|
||||
const answer = g.truthMixedTraitorAnswers?.[0];
|
||||
return answer ? [{ type: 'truthAnswer', answer }] : [];
|
||||
}
|
||||
if (w.question.kind === 'traitorCall') {
|
||||
const answers = g.truthTraitorAnswers ?? [];
|
||||
const answer = answers.find(answer => answer === 'yes') ?? answers[0];
|
||||
|
||||
+66
-4
@@ -1,3 +1,4 @@
|
||||
import { mixedTraitorRequirement, type MixedTraitorClaim } from './mixed-traitor-question';
|
||||
import type { MixedBattleActionClaim, MixedBattleClaim } from './mixed-battle-question';
|
||||
import { AllianceExpressionPromiseError, allianceExpressionOutcomes, findAllianceExpressionPlans,
|
||||
allianceExpressionPromisesMatch, evaluateAllianceExpression, validateAllianceExpressionPromises,
|
||||
@@ -2161,6 +2162,10 @@ export type Game = {
|
||||
};
|
||||
truthtrance?: TruthWindow | null;
|
||||
truthHistory?: TruthRecord[];
|
||||
mixedTraitorQuestions?: (MixedTraitorClaim & {
|
||||
event: string; turn: number; player: string; asker: string; answer: boolean;
|
||||
binding: boolean; fulfilled: boolean; released: boolean;
|
||||
})[];
|
||||
mixedBattleQuestions?: (Omit<MixedBattleClaim, 'battle'> & {
|
||||
event: string; battle: string; turn: number; player: string; asker: string; answer: boolean;
|
||||
binding: boolean; fulfilled: boolean; released: boolean;
|
||||
@@ -6937,6 +6942,7 @@ function truthtranceDiscardSignature(g: Game, next: TruthDiscardContinuation) {
|
||||
battle: g.battle,
|
||||
shipmentPromises: g.shipmentPromises,
|
||||
movementPromises: g.movementPromises,
|
||||
...(next.record.question.kind === 'mixedTraitorCall' ? { mixedTraitorQuestions: g.mixedTraitorQuestions } : {}),
|
||||
...(next.record.question.kind === 'mixedBattlePlan' ? { mixedBattleQuestions: g.mixedBattleQuestions } : {}),
|
||||
...(['allianceExpression', 'mixedAlliance'].includes(next.record.question.kind) ? {
|
||||
allianceExpressionPromises: g.allianceExpressionPromises, alliancePromiseSequence: g.alliancePromiseSequence,
|
||||
@@ -28198,11 +28204,58 @@ function traitorTruthAnswers(g: Game, p: Player): TruthAnswer[] {
|
||||
return context ? traitorDecisionRule(() => traitorDecisionChoices(context, g.battle?.traitorDecisionPromises ?? []))
|
||||
.map(call => call ? 'yes' as const : 'no' as const) : [];
|
||||
}
|
||||
function bindTraitorTruth(g: Game, p: Player, answer: boolean, asker: string) {
|
||||
function bindTraitorTruth(g: Game, p: Player, answer: boolean, asker: string, event?: string) {
|
||||
const context = traitorTruthContext(g, p.id);
|
||||
requireRule(context, 'Retain the current unsubmitted Traitor decision.');
|
||||
g.battle!.traitorDecisionPromises = traitorDecisionRule(() =>
|
||||
bindTraitorDecisionPromise(context, g.battle!.traitorDecisionPromises ?? [], asker, answer));
|
||||
bindTraitorDecisionPromise(context, g.battle!.traitorDecisionPromises ?? [], asker, answer, event));
|
||||
}
|
||||
function compileMixedTraitor(g: Game, p: Player, claim: MixedTraitorClaim, answer: boolean) {
|
||||
return mixedTraitorRequirement(compileMixedActionExpression(claim.mixed,
|
||||
fact => truthFactAnswer(p, fact, truthKnowledgeOf(g, p)), answer), answer);
|
||||
}
|
||||
function mixedTraitorTruthAnswers(g: Game, p: Player, claim: MixedTraitorClaim): TruthAnswer[] {
|
||||
const context = traitorTruthContext(g, p.id);
|
||||
if (!context || context.submitted !== undefined || context.battle !== claim.battle ||
|
||||
g.battle?.territory !== claim.territory) return [];
|
||||
const native = traitorTruthAnswers(g, p), answers: TruthAnswer[] = [];
|
||||
for (const answer of [true, false]) {
|
||||
const requirement = compileMixedTraitor(g, p, claim, answer);
|
||||
if (requirement.possible && (requirement.call === null || native.includes(requirement.call ? 'yes' : 'no')))
|
||||
answers.push(answer ? 'yes' : 'no');
|
||||
}
|
||||
return answers.length ? answers : ['unknown'];
|
||||
}
|
||||
function bindMixedTraitorTruth(g: Game, p: Player, claim: MixedTraitorClaim, answer: boolean, asker: string) {
|
||||
requireRule(mixedTraitorTruthAnswers(g, p, claim).includes(answer ? 'yes' : 'no'),
|
||||
'Choose a whole mixed Traitor answer compatible with the current native declaration.');
|
||||
const requirement = compileMixedTraitor(g, p, claim, answer);
|
||||
requireRule(requirement.possible, 'Retain the guaranteed whole mixed Traitor answer.');
|
||||
const event = crypto.randomUUID(), binding = requirement.call !== null;
|
||||
if (requirement.call !== null) bindTraitorTruth(g, p, requirement.call, asker, event);
|
||||
(g.mixedTraitorQuestions ??= []).push({ battle: claim.battle, territory: claim.territory,
|
||||
mixed: structuredClone(claim.mixed), event, turn: g.turn,
|
||||
player: p.id, asker, answer, binding, fulfilled: !binding, released: false });
|
||||
}
|
||||
function reconcileMixedTraitorQuestions(g: Game) {
|
||||
const b = g.battle;
|
||||
for (const question of g.mixedTraitorQuestions ?? []) {
|
||||
if (!question.binding || question.fulfilled || question.released) continue;
|
||||
if (!b || b.event !== question.battle) { question.released = true; continue; }
|
||||
const bound = b.traitorDecisionPromises?.find(promise => promise.event === question.event);
|
||||
if (bound?.released) question.released = true;
|
||||
else if (bound?.fulfilled) question.fulfilled = true;
|
||||
}
|
||||
}
|
||||
function isMixedTraitorEvent(g: Game, event: string | undefined): boolean {
|
||||
return typeof event === 'string' && !!g.mixedTraitorQuestions?.some(question => question.event === event);
|
||||
}
|
||||
function projectedMixedTraitorPromises(g: Game, player: string) {
|
||||
return (g.mixedTraitorQuestions ?? []).filter(question => question.player === player).map(question => ({
|
||||
event: question.event, player: question.player, asker: question.asker, turn: question.turn,
|
||||
battle: question.battle, territory: question.territory, mixed: structuredClone(question.mixed), answer: question.answer,
|
||||
fulfilled: question.fulfilled, released: question.released,
|
||||
}));
|
||||
}
|
||||
function traitorTruthIntegrity(g: Game) {
|
||||
const b = g.battle, promises = b?.traitorDecisionPromises;
|
||||
@@ -28219,13 +28272,14 @@ function traitorTruthIntegrity(g: Game) {
|
||||
}
|
||||
}
|
||||
const question = g.truthtrance?.question;
|
||||
if (question?.kind === 'traitorCall') {
|
||||
if (question?.kind === 'traitorCall' || question?.kind === 'mixedTraitorCall') {
|
||||
const context = traitorTruthContext(g, question.target);
|
||||
requireRule(context && context.submitted === undefined && context.battle === question.battle,
|
||||
'This Truthtrance requires the original unsubmitted native Traitor voter.');
|
||||
}
|
||||
}
|
||||
function reconcileTraitorTruth(g: Game, actor?: string, action?: Action) {
|
||||
reconcileMixedTraitorQuestions(g);
|
||||
const b = g.battle;
|
||||
if (!b?.traitorDecisionPromises?.length) return;
|
||||
for (const player of new Set(b.traitorDecisionPromises.map(promise => promise.player))) {
|
||||
@@ -28240,6 +28294,7 @@ function reconcileTraitorTruth(g: Game, actor?: string, action?: Action) {
|
||||
}
|
||||
b.traitorDecisionPromises = next;
|
||||
}
|
||||
reconcileMixedTraitorQuestions(g);
|
||||
}
|
||||
function traitorReturnPromiseBlock(g: Game, p: Player, returned: readonly string[]): string | null {
|
||||
const promises = g.battle?.traitorDecisionPromises;
|
||||
@@ -36841,6 +36896,8 @@ function applyActionInner(
|
||||
bindMixedBid: bindMixedBidTruth,
|
||||
bidAnswers: bidTruthAnswers,
|
||||
bindBid: bindBidTruth,
|
||||
mixedTraitorAnswers: mixedTraitorTruthAnswers,
|
||||
bindMixedTraitor: bindMixedTraitorTruth,
|
||||
traitorAnswers: traitorTruthAnswers,
|
||||
bindTraitor: bindTraitorTruth,
|
||||
mixedBattleAnswers: mixedBattleTruthAnswers,
|
||||
@@ -40291,6 +40348,7 @@ function applyActionInner(
|
||||
const context = traitorTruthContext(g, id);
|
||||
if (context && b.traitorDecisionPromises)
|
||||
b.traitorDecisionPromises = traitorDecisionRule(() => settleTraitorDecisionPromises(context, b.traitorDecisionPromises!, action.call as boolean));
|
||||
reconcileMixedTraitorQuestions(g);
|
||||
if (g.nexusCards?.cards || b.ecazOccupy) ensureTraitorDeclarations(g);
|
||||
if (action.call) {
|
||||
const blocked = traitorCallBlock(g, b, id);
|
||||
@@ -40629,6 +40687,10 @@ export function viewGame(state: Game, id: string) {
|
||||
...(g.advanced&&g.decision?.kind==='advisor'&&g.decision.player===id
|
||||
? {spiritualAdvisorChoices:spiritualAdvisorChoices(g,id)} : {}),
|
||||
truthMovementTargets: g.players.filter(p => movementPromiseTargetAvailable(g, p.id)).map(p => p.id),
|
||||
truthMixedTraitorTargets: g.players.filter(p => { const context = traitorTruthContext(g, p.id); return context && context.submitted === undefined; }).map(p => p.id),
|
||||
truthMixedTraitorAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.kind === 'mixedTraitorCall' &&
|
||||
g.truthtrance.question.target === id ? mixedTraitorTruthAnswers(g, me, g.truthtrance.question) : null,
|
||||
mixedTraitorPromises: projectedMixedTraitorPromises(g, id),
|
||||
truthTraitorTargets: g.players.filter(p => { const context = traitorTruthContext(g, p.id); return context && context.submitted === undefined; }).map(p => p.id),
|
||||
truthTraitorAnswers: g.truthtrance?.stage === 'answer' && g.truthtrance.question?.kind === 'traitorCall' &&
|
||||
g.truthtrance.question.target === id ? traitorTruthAnswers(g, me) : null,
|
||||
@@ -41882,7 +41944,7 @@ export function viewGame(state: Game, id: string) {
|
||||
traitorSubmitted: Object.keys(b.traitorCalls),
|
||||
traitorCallChoices: (() => { const context = traitorTruthContext(g, id); return context
|
||||
? traitorDecisionRule(() => traitorDecisionChoices(context, b.traitorDecisionPromises ?? [])) : null; })(),
|
||||
ownTraitorPromises: (b.traitorDecisionPromises ?? []).filter(promise => promise.player === id),
|
||||
ownTraitorPromises: (b.traitorDecisionPromises ?? []).filter(promise => promise.player === id && !isMixedTraitorEvent(g, promise.event)),
|
||||
traitorVoters: traitorVoters(g, b),
|
||||
preparation: b.preparation && g.homeworldOccupationPreview && b.preparation.kind === 'voice'
|
||||
? { ...b.preparation, blocked: homeworldRule(() => quoteOccupiedHomeworldVoice(g,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { CompiledMixedAction, MixedActionExpression } from './mixed-action-question';
|
||||
import { TraitorDecisionError } from './traitor-decision-promises';
|
||||
|
||||
export const MIXED_TRAITOR_MAX_LEAVES = 16;
|
||||
export const MIXED_TRAITOR_MAX_DEPTH = 4;
|
||||
export type TraitorActionClaim = { call: boolean };
|
||||
export type MixedTraitorClaim = {
|
||||
battle: string;
|
||||
territory: string;
|
||||
mixed: MixedActionExpression<TraitorActionClaim>;
|
||||
};
|
||||
|
||||
/** One original native declaration; source and frozen metadata stay outside. */
|
||||
export function parseTraitorActionClaim(value: unknown): TraitorActionClaim {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
||||
Object.keys(value).join(',') !== 'call' || typeof (value as Record<string, unknown>).call !== 'boolean')
|
||||
throw new TraitorDecisionError('Supply one Reveal or Decline condition without battle metadata or groups.');
|
||||
return { call: (value as TraitorActionClaim).call };
|
||||
}
|
||||
|
||||
type TraitorOutcomes = { decline: boolean; reveal: boolean };
|
||||
function declarationOutcomes(node: CompiledMixedAction<TraitorActionClaim>): TraitorOutcomes {
|
||||
if (node.kind === 'constant') return { decline: node.value, reveal: node.value };
|
||||
if (node.kind === 'action') {
|
||||
const { call } = parseTraitorActionClaim(node.action);
|
||||
return { decline: !call, reveal: call };
|
||||
}
|
||||
// Evaluate every child before combining, including correlated opposite calls.
|
||||
const terms = node.terms.map(declarationOutcomes);
|
||||
return node.kind === 'and'
|
||||
? { decline: terms.every(term => term.decline), reveal: terms.every(term => term.reveal) }
|
||||
: { decline: terms.some(term => term.decline), reveal: terms.some(term => term.reveal) };
|
||||
}
|
||||
|
||||
/** Exact logical residual over both declarations. The caller intersects a forced
|
||||
* choice with native eligibility; current inability to Reveal must not erase a
|
||||
* forced Decline that remains binding after a later matching Traitor draw. */
|
||||
export function mixedTraitorRequirement(
|
||||
node: CompiledMixedAction<TraitorActionClaim>,
|
||||
answer: boolean,
|
||||
): { possible: false } | { possible: true; call: boolean | null } {
|
||||
if (typeof answer !== 'boolean') throw new TraitorDecisionError('Choose a definite mixed Traitor answer.');
|
||||
const outcomes = declarationOutcomes(node);
|
||||
const decline = outcomes.decline === answer, reveal = outcomes.reveal === answer;
|
||||
if (!decline && !reveal) return { possible: false };
|
||||
return { possible: true, call: decline && reveal ? null : reveal };
|
||||
}
|
||||
@@ -3185,6 +3185,31 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
{ 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-mixed-traitor',
|
||||
title: 'Truthtrance: current facts and this Traitor declaration',
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Combine answer-time facts with the original current revealed-battle Reveal or Decline decision, preserving matching eligibility and declaration receipts.',
|
||||
steps: [
|
||||
'Choose Combine current facts and this Traitor declaration. Add full AND/OR groups with both current facts and Reveal/Decline conditions. All declaration leaves concern the same original unsubmitted native voter in this revealed battle, including a native allied Harkonnen voter.',
|
||||
'Current facts freeze at the actual answer. Later spice payments, matching-card draws or returns do not create a new factual answer. Unknown facts cannot be relied on being true for Yes or false for No; No negates the complete expression.',
|
||||
'The two possible declaration values are checked together. A whole answer may force Reveal, force Decline, or already be guaranteed for either choice. A constant or tautology needs no native restriction. No to a promise to decline can require Reveal.',
|
||||
'A forced value uses the original matching physical Traitor, Kwisatz/leader protection, voter/beneficiary and prior-promise rules. Current inability to Reveal does not erase a Decline commitment: it stays binding if a later native draw makes Reveal possible.',
|
||||
'The actual original declaration fulfills a binding answer before later Karama or Nexus betrayal changes its effect. A prevented effect never retracts the receipt. Submitted true and false votes are never reopened.',
|
||||
'During Harkonnen Cunning, a live required Reveal protects the matching identity from a voluntary return. Decline may return that identity. After a fulfilled declaration, the original return rules apply and the receipt remains.',
|
||||
'Full mixed editing, raw preview/Ask, private aggregate answer choices and persistent own status connect to the original compatible declaration buttons and four minimal profiles. Reduced choices and individual frozen facts are not projected as extra answers. Unrevealed future-leader questions, broader preparations/compositions and final deployed acceptance remain unfinished.',
|
||||
],
|
||||
related: ['card-truthtrance', 'truthtrance-traitor-decision', 'truthtrance-mixed-battle', 'implementation-checklist'],
|
||||
checklist: [
|
||||
{ area: 'Implementation', status: 'Partial', detail: 'Exact logical reduction of one current decision binds only a forced original native declaration. Actual receipt/release updates own raw aggregate without exposing compiled choice or frozen facts.', evidence: ['game/mixed-traitor-question.ts', 'game/traitor-decision-promises.ts', 'game/truthtrance.ts', 'game/engine.ts', 'tests/mixed-traitor-question.test.ts'] },
|
||||
{ area: 'Player controls', status: 'Partial', detail: 'Full shared editor and exact source-aware aggregate answer callbacks use public context. Original private legal-choice buttons and own persistent status stay connected.', evidence: ['components/mixed-traitor-fields.tsx', 'components/mixed-traitor-promises.tsx', 'components/traitor-decision-promises.tsx', 'components/truthtrance.tsx', 'components/game-table.tsx', 'tests/truth-mixed-traitor-controls.test.tsx'] },
|
||||
{ area: 'AI', status: 'Partial', detail: 'Four minimal profiles answer and declare through original compatible choices; full strategy/calibration remain deferred.', evidence: ['game/bots.ts', 'tests/truth-mixed-traitor-runtime.test.ts'] },
|
||||
{ area: 'Documentation', status: 'Implemented', detail: 'Snapshot/unknown, opposite-call logic, native eligibility/protection, declaration-versus-effect and remaining boundaries are explicit.', evidence: ['docs/TRUTHTRANCE_MIXED_TRAITOR.md', 'docs/RULE_DECISIONS.md'] },
|
||||
{ area: 'Verification', status: 'Partial', detail: 'Focused pure logical/event, original Basic/allied/Cunning and controlled editing/answer cases; wider faction/module/composition and final deployed acceptance remain open.', evidence: ['tests/mixed-traitor-question.test.ts', 'tests/traitor-decision-promises.test.ts', 'tests/fixture-truth-mixed-traitor.ts', 'tests/truth-mixed-traitor-runtime.test.ts', 'tests/truth-mixed-traitor-harkonnen.test.ts', 'tests/truth-mixed-traitor-controls.test.tsx'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'truthtrance-traitor-decision',
|
||||
title: 'Truthtrance: this battle’s Traitor declaration',
|
||||
|
||||
@@ -13,6 +13,8 @@ export type TraitorDecisionContext = {
|
||||
export type TraitorDecisionPromise = Omit<TraitorDecisionContext, 'canReveal' | 'submitted'> & {
|
||||
asker: string;
|
||||
answer: boolean;
|
||||
/** Link to the owner's mixed-question record; pure declaration receipts omit it. */
|
||||
event?: string;
|
||||
fulfilled?: boolean;
|
||||
released?: boolean;
|
||||
};
|
||||
@@ -40,11 +42,12 @@ export function validateTraitorDecisionPromise(value: unknown): value is Traitor
|
||||
if (!record(value)) return false;
|
||||
const required = ['answer', 'asker', 'battle', 'beneficiary', 'leader', 'player', 'target', 'turn'];
|
||||
if (!required.every(key => Object.hasOwn(value, key)) ||
|
||||
Object.keys(value).some(key => !required.includes(key) && key !== 'fulfilled' && key !== 'released')) return false;
|
||||
Object.keys(value).some(key => !required.includes(key) && key !== 'event' && key !== 'fulfilled' && key !== 'released')) return false;
|
||||
return turnNumber(value.turn) && identifier(value.battle) && identifier(value.player) &&
|
||||
identifier(value.beneficiary) && identifier(value.target) &&
|
||||
(value.leader === null || identifier(value.leader)) && identifier(value.asker) &&
|
||||
typeof value.answer === 'boolean' && optionalBoolean(value.fulfilled) && optionalBoolean(value.released);
|
||||
typeof value.answer === 'boolean' && (!Object.hasOwn(value, 'event') || identifier(value.event)) &&
|
||||
optionalBoolean(value.fulfilled) && optionalBoolean(value.released);
|
||||
}
|
||||
|
||||
function validateContext(context: TraitorDecisionContext) {
|
||||
@@ -85,13 +88,16 @@ export function bindTraitorDecisionPromise(
|
||||
promises: readonly TraitorDecisionPromise[],
|
||||
asker: string,
|
||||
answer: boolean,
|
||||
event?: string,
|
||||
): TraitorDecisionPromise[] {
|
||||
requireDecision(identifier(asker) && typeof answer === 'boolean', 'Choose a definite Traitor decision answer and its asker.');
|
||||
requireDecision(identifier(asker) && typeof answer === 'boolean' && (event === undefined || identifier(event)),
|
||||
'Choose a definite Traitor decision answer, its asker and an optional public event.');
|
||||
requireDecision(traitorDecisionChoices(context, promises).includes(answer),
|
||||
'This answer has no compatible current Traitor declaration.');
|
||||
return [...promises.map(promise => ({ ...promise })), {
|
||||
turn: context.turn, battle: context.battle, player: context.player,
|
||||
beneficiary: context.beneficiary, target: context.target, leader: context.leader, asker, answer,
|
||||
...(event === undefined ? {} : { event }),
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { parseTraitorActionClaim, type MixedTraitorClaim } from './mixed-traitor-question';
|
||||
import { parseBattleActionClaim, MIXED_BATTLE_MAX_NODES, MIXED_BATTLE_MAX_TERMS, MIXED_BATTLE_MAX_DEPTH,
|
||||
MIXED_BATTLE_MAX_LEAVES, type MixedBattleClaim } from './mixed-battle-question';
|
||||
import { parseAllianceExpressionClaim, allianceExpressionText, type AllianceExpressionClaim } from './alliance-expression-promises';
|
||||
@@ -77,6 +78,7 @@ export type TruthFact =
|
||||
| { kind: 'and' | 'or'; terms: TruthFact[] };
|
||||
export type MixedMovementClaim = { turn: number; mixed: MixedActionExpression<MovementClaim> };
|
||||
export type TruthQuestion =
|
||||
| ({ kind: 'mixedTraitorCall'; target: string } & MixedTraitorClaim)
|
||||
| ({ kind: 'mixedBattlePlan'; target: string } & MixedBattleClaim)
|
||||
| ({ kind: 'allianceExpression'; target: string } & AllianceExpressionClaim)
|
||||
| ({ kind: 'mixedAlliance'; target: string } & MixedAllianceClaim)
|
||||
@@ -368,6 +370,17 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
'Choose the current ordinary bid lot in a supported table and finish pending decisions.');
|
||||
return { kind: 'bid', target: v.target, ...claim };
|
||||
}
|
||||
if (v.kind === 'mixedTraitorCall') {
|
||||
check(Object.keys(v).sort().join(',') === 'battle,kind,mixed,target,territory' &&
|
||||
g.status === 'playing' && g.phase === 6 && g.battle?.revealed && typeof v.battle === 'string' &&
|
||||
v.battle.length > 0 && v.battle === g.battle.event && v.territory === g.battle.territory &&
|
||||
g.battle.traitorCalls[v.target as string] === undefined,
|
||||
'Choose the current revealed battle and an original unsubmitted Traitor decision.');
|
||||
const respondent = g.players.find(player => player.id === v.target)!;
|
||||
return { kind: 'mixedTraitorCall', target: v.target as string, battle: v.battle,
|
||||
territory: v.territory as string,
|
||||
mixed: parseMixedActionExpression(v.mixed, fact => parseFact(g, fact, respondent), parseTraitorActionClaim) };
|
||||
}
|
||||
if (v.kind === 'traitorCall') {
|
||||
check(Object.keys(v).sort().join(',') === 'battle,kind,target,territory' &&
|
||||
g.status === 'playing' && g.phase === 6 && g.battle?.revealed &&
|
||||
@@ -657,6 +670,9 @@ export function truthQuestionText(
|
||||
return `you currently hold ${f.compare === 'eq' ? 'exactly' : f.compare === 'gte' ? 'at least' : 'at most'} ${f.value} spice`;
|
||||
return `(${f.terms.map(clause).join(f.kind === 'and' ? ' AND ' : ' OR ')})`;
|
||||
};
|
||||
if (q.kind === 'mixedTraitorCall') return `Is it true that ${mixedActionExpressionText(q.mixed, clause,
|
||||
action => action.call ? 'you declare the opposing leader a Traitor in this battle'
|
||||
: 'you decline to declare the opposing leader a Traitor in this battle')}?`;
|
||||
if (q.kind === 'mixedBattlePlan') return `Is it true that ${mixedActionExpressionText(q.mixed, clause,
|
||||
action => `${planClaimText(action, leaderName)} in this battle`)}?`;
|
||||
if (q.kind === 'mixedAlliance') return `Is it true that ${mixedActionExpressionText(q.mixed, clause,
|
||||
@@ -723,6 +739,8 @@ export function resolveTruthAction(
|
||||
bindMixedBid?: (g: Game, p: Player, claim: MixedBidClaim, answer: boolean, asker: string) => void;
|
||||
bidAnswers?: (g: Game, p: Player, claim: BidClaim) => TruthAnswer[];
|
||||
bindBid?: (g: Game, p: Player, claim: BidClaim, answer: boolean, asker: string) => void;
|
||||
mixedTraitorAnswers?: (g: Game, p: Player, claim: MixedTraitorClaim) => TruthAnswer[];
|
||||
bindMixedTraitor?: (g: Game, p: Player, claim: MixedTraitorClaim, answer: boolean, asker: string) => void;
|
||||
traitorAnswers?: (g: Game, p: Player) => TruthAnswer[];
|
||||
bindTraitor?: (g: Game, p: Player, answer: boolean, asker: string) => void;
|
||||
mixedBattleAnswers?: (g: Game, p: Player, claim: MixedBattleClaim) => TruthAnswer[];
|
||||
@@ -888,6 +906,9 @@ export function resolveTruthAction(
|
||||
if (w.question.kind === 'bid')
|
||||
check(effects.bidAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
|
||||
'Choose a bidder with a remaining ordinary opportunity in this lot.');
|
||||
if (w.question.kind === 'mixedTraitorCall')
|
||||
check(effects.mixedTraitorAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
|
||||
'Choose a whole mixed Traitor question for an actual current voter.');
|
||||
if (w.question.kind === 'traitorCall')
|
||||
check(effects.traitorAnswers?.(g, g.players.find(other => other.id === w.question!.target)!).length,
|
||||
'Choose an actual unsubmitted native Traitor voter.');
|
||||
@@ -1009,6 +1030,11 @@ export function resolveTruthAction(
|
||||
'Choose an answer consistent with this lot, your legal bids and earlier answers.');
|
||||
if (a.answer !== 'unknown') effects.bindBid(g, p, q, a.answer === 'yes', current.player);
|
||||
}
|
||||
if (q.kind === 'mixedTraitorCall') {
|
||||
check(effects.mixedTraitorAnswers && effects.bindMixedTraitor && effects.mixedTraitorAnswers(g, p, q).includes(a.answer),
|
||||
'Choose an aggregate answer compatible with this mixed question and current native declaration.');
|
||||
if (a.answer !== 'unknown') effects.bindMixedTraitor(g, p, q, a.answer === 'yes', current.player);
|
||||
}
|
||||
if (q.kind === 'traitorCall') {
|
||||
check(effects.traitorAnswers && effects.bindTraitor && effects.traitorAnswers(g, p).includes(a.answer),
|
||||
'Choose an answer compatible with your current legal Traitor declaration and earlier answers.');
|
||||
|
||||
Reference in new issue
Block a user