Support Truthtrance Storm dial comparisons
This commit is contained in:
1 parent
c8889e94e2
commit
31b3c1ab87
12 files changed
+288
-18
No files matched your search
+1
-1
@@ -3280,7 +3280,7 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
'Basic Revival No lasts to phase close; ordered Advanced finishes at the respondent’s actual own turn end. Own Ready must retain a whole completion, including a funded later payment alternative. An owned Karama invoice guide appears only at the actual payment decision, never while another bidder is active.',
|
||||
'A battle-plan condition names an atomic part of your actual plan in this current battle. An open condition settles when both original plans reveal; a plan already sealed fixes its value at the answer. The human guide fills your existing plan for review before sealing. Native leaders, physical cards, Voice, costs and ordered preparation still apply.',
|
||||
'A Traitor condition describes the original eligible voter’s Reveal or Decline declaration in this revealed battle, before any later Karama prevention. A prevented Reveal stays a declared Reveal. No missing battle or declaration is invented as a zero plan or Decline, and an absent matching Traitor never creates one.',
|
||||
'A future storm-dial condition names a current Basic dialer who has not submitted and one exact legal value. Turn 1 permits 0–20; later Basic turns permit 1–3. A Yes binds that exact native choice, while a No excludes it. Existing whole-answer alternatives still apply. The other dial remains sealed, and the normal sum is revealed only after both dialers submit.',
|
||||
'A future storm-dial condition names a current Basic dialer who has not submitted and asks whether their value will be exactly, at least or at most a chosen threshold. Turn 1 permits 0–20; later Basic turns permit 1–3. Yes and No bind the whole answer; multiple conditions share the same native choice. The other dial remains sealed, and the normal sum is revealed only after both dialers submit.',
|
||||
'A native-power condition names your original Voice or Prescience opportunity in this battle. Ask about actual use or explicit Decline, an exact Voice category and require/forbid command, or the requested Prescience field. A requested field is not the inspected answer. The declaration counts before later Karama prevention.',
|
||||
'Native owners can provide their eligible ally’s power. Voice finishes before Prescience; a pending Prescience condition does not advance its turn. Original command and No-Field restrictions remain. All predicates concern the same one native choice; source expiry is absence and never becomes a synthetic Decline. Borrowed Nexus and special powers retain separate scope.',
|
||||
'In a supported Basic battle response, ask whether a named non-owner will spend a physical printed Karama to cancel the current Voice or Prescience. The question may combine this exact response with current facts; other future action families remain unavailable while the response is open. Yes requires that actual card use while possible; No prevents it. A different player may cancel first, releasing an impossible Yes. Passing does not itself surrender a still-open response, but the target cannot close their last opportunity while a required Yes remains.',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
/** A future choice names only the actor's original native dial opportunity.
|
||||
* The accepted value stays in the normal private storm dial until reveal. */
|
||||
export type StormDialSource = { turn: number; player: string };
|
||||
export type StormDialClaim = { kind: 'stormDial'; source: string; value: number };
|
||||
export type StormDialClaim = {
|
||||
kind: 'stormDial'; source: string; value: number;
|
||||
/** Older saved claims omit this field and retain exact-value meaning. */
|
||||
compare?: 'eq' | 'gte' | 'lte';
|
||||
};
|
||||
export type StormDialOpportunity = StormDialSource & { source: string; min: number; max: number };
|
||||
export type StormDialGame = {
|
||||
status: string;
|
||||
@@ -43,15 +47,20 @@ export function parseStormDialSource(source: unknown): StormDialSource | null {
|
||||
return { turn: tuple[1], player: tuple[2] };
|
||||
}
|
||||
|
||||
/** Parse only an exact future native choice within that turn's real dial range. */
|
||||
/** Parse a future native choice within that turn's real dial range. */
|
||||
export function parseStormDialClaim(value: unknown): StormDialClaim {
|
||||
check(object(value) && Object.keys(value).sort().join(',') === 'kind,source,value' && value.kind === 'stormDial',
|
||||
'Choose one original storm-dial opportunity and exact value.');
|
||||
check(object(value) &&
|
||||
(Object.keys(value).sort().join(',') === 'kind,source,value' ||
|
||||
Object.keys(value).sort().join(',') === 'compare,kind,source,value') &&
|
||||
value.kind === 'stormDial' &&
|
||||
(!Object.hasOwn(value, 'compare') || value.compare === 'eq' || value.compare === 'gte' || value.compare === 'lte'),
|
||||
'Choose one original storm-dial opportunity and value comparison.');
|
||||
const source = parseStormDialSource(value.source);
|
||||
check(source && typeof value.value === 'number' && Number.isSafeInteger(value.value) &&
|
||||
value.value >= (source.turn === 1 ? 0 : 1) && value.value <= (source.turn === 1 ? 20 : 3),
|
||||
'Choose a value within the original turn’s native storm-dial range.');
|
||||
return { kind: 'stormDial', source: value.source as string, value: value.value };
|
||||
return { kind: 'stormDial', source: value.source as string, value: value.value,
|
||||
...(Object.hasOwn(value, 'compare') ? { compare: value.compare as 'eq' | 'gte' | 'lte' } : {}) };
|
||||
}
|
||||
|
||||
/** Compare the actual accepted native submission, never an invented default. */
|
||||
@@ -60,7 +69,8 @@ export function stormDialMatches(claim: StormDialClaim, actual: unknown): boolea
|
||||
check(typeof actual === 'number' && Number.isSafeInteger(actual) &&
|
||||
actual >= (source.turn === 1 ? 0 : 1) && actual <= (source.turn === 1 ? 20 : 3),
|
||||
'Retain the actual native storm-dial submission.');
|
||||
return parsed.value === actual;
|
||||
return parsed.compare === 'gte' ? actual >= parsed.value :
|
||||
parsed.compare === 'lte' ? actual <= parsed.value : actual === parsed.value;
|
||||
}
|
||||
|
||||
/** Public eligibility only. Reads whether a value is sealed, never that value.
|
||||
|
||||
+1
-1
@@ -795,7 +795,7 @@ export function truthQuestionText(
|
||||
};
|
||||
if (q.kind === 'turnActions') return `Is it true that ${mixedActionExpressionText(q.mixed, clause,
|
||||
action => {
|
||||
if (action.kind === 'stormDial') return `you submit exactly ${action.value} on your original storm dial this turn`;
|
||||
if (action.kind === 'stormDial') return `you submit ${action.compare === 'gte' ? 'at least' : action.compare === 'lte' ? 'at most' : 'exactly'} ${action.value} on your original storm dial this turn`;
|
||||
if (action.kind === 'windowCardPlay') {
|
||||
if (action.effect === 'karamaBattleCancel') {
|
||||
const response = parseKaramaBattleCancelSource(action.source);
|
||||
|
||||
@@ -225,7 +225,8 @@ function storedFact(value: unknown, depth = 0): TruthFact {
|
||||
}
|
||||
|
||||
function actionKey(action: TurnActionLeaf): string {
|
||||
if (action.kind === 'stormDial') return JSON.stringify([action.kind, action.source, action.value]);
|
||||
if (action.kind === 'stormDial') return JSON.stringify(action.compare && action.compare !== 'eq'
|
||||
? [action.kind, action.source, action.value, action.compare] : [action.kind, action.source, action.value]);
|
||||
if (action.kind === 'windowCardPlay') return JSON.stringify([action.kind, action.effect, action.source]);
|
||||
if (action.kind === 'battlePower') return JSON.stringify([action.kind, action.battle, action.territory, powerKey(action.claim)]);
|
||||
if (action.kind === 'cardPlay') return JSON.stringify([action.kind, action.effect]);
|
||||
@@ -576,7 +577,8 @@ export function liveTurnActionPromises<T extends TurnActionPromise>(promises: re
|
||||
|
||||
function sameAction(left: TurnActionBranchLeaf, right: TurnActionBranchLeaf): boolean {
|
||||
if (left.kind !== right.kind) return false;
|
||||
if (left.kind === 'stormDial') return right.kind === 'stormDial' && left.source === right.source && left.value === right.value;
|
||||
if (left.kind === 'stormDial') return right.kind === 'stormDial' && left.source === right.source &&
|
||||
left.value === right.value && (left.compare ?? 'eq') === (right.compare ?? 'eq');
|
||||
if (left.kind === 'windowCardPlay') return right.kind === 'windowCardPlay' && left.effect === right.effect && left.source === right.source;
|
||||
if (left.kind === 'battlePower') return right.kind === 'battlePower' && left.battle === right.battle &&
|
||||
left.territory === right.territory && left.owner === right.owner && left.beneficiary === right.beneficiary &&
|
||||
@@ -605,7 +607,8 @@ export function turnActionStormDialChoices(leaves: readonly TurnActionBranchLeaf
|
||||
leaf.kind === 'stormDial' && leaf.source === source);
|
||||
const min = decoded.turn === 1 ? 0 : 1, max = decoded.turn === 1 ? 20 : 3;
|
||||
return Array.from({ length: max - min + 1 }, (_, index) => index + min)
|
||||
.filter(value => named.every(leaf => (value === leaf.value) === leaf.answer));
|
||||
.filter(value => named.every(leaf => stormDialMatches({ kind: 'stormDial', source: leaf.source,
|
||||
value: leaf.value, ...(leaf.compare ? { compare: leaf.compare } : {}) }, value) === leaf.answer));
|
||||
}
|
||||
|
||||
/** Called within one record only: payment progress belongs to that interval. */
|
||||
@@ -622,8 +625,6 @@ function mergeBranches(left: TurnActionBranchLeaf[], right: TurnActionBranchLeaf
|
||||
if (leaf.kind === 'stormDial') {
|
||||
const prior = merged.find(other => sameAction(other, leaf));
|
||||
if (prior && prior.answer !== leaf.answer) return null;
|
||||
if (leaf.answer && merged.some(other => other.kind === 'stormDial' && other.source === leaf.source &&
|
||||
other.answer && other.value !== leaf.value)) return null;
|
||||
if (!prior) merged.push({ ...leaf });
|
||||
if (!turnActionStormDialChoices(merged, leaf.source).length) return null;
|
||||
continue;
|
||||
|
||||
Reference in new issue
Block a user