Expose full Truthtrance battle-plan and shipment editors

This commit is contained in:
Quintine committed 2026-10-26 09:02:16 +11:00
1 parent 8e2f2c6a95
commit 4ee51917bc
21 files changed
+1958 -92

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
import type { PlanClaim } from './battle-promises';
// Existing structured battle-question limits, shared with authoring controls.
export const BATTLE_CLAIM_MAX_NODES = 16;
export const BATTLE_CLAIM_MAX_DEPTH = 4;
export const BATTLE_CLAIM_MAX_TERMS = 8;
export const BATTLE_CLAIM_INPUT_ERROR =
'Choose a valid battle-plan condition (at most 16 clauses and four grouping levels).';
export function battleClaimExpressionSize(claim: PlanClaim): { nodes: number; depth: number } {
if (claim.kind !== 'and' && claim.kind !== 'or') return { nodes: 1, depth: 0 };
const children = claim.terms.map(battleClaimExpressionSize);
return { nodes: 1 + children.reduce((sum, child) => sum + child.nodes, 0),
depth: 1 + Math.max(0, ...children.map(child => child.depth)) };
}
/** Structure only; parsePlanClaim still validates every actual leaf. */
export function battleClaimExpressionError(claim: PlanClaim): string | null {
const size = battleClaimExpressionSize(claim);
if (size.nodes > BATTLE_CLAIM_MAX_NODES || size.depth > BATTLE_CLAIM_MAX_DEPTH)
return BATTLE_CLAIM_INPUT_ERROR;
const groupError = (node: PlanClaim): string | null => {
if (node.kind !== 'and' && node.kind !== 'or') return null;
if (node.terms.length < 2 || node.terms.length > BATTLE_CLAIM_MAX_TERMS)
return BATTLE_CLAIM_INPUT_ERROR;
for (const term of node.terms) {
const error = groupError(term);
if (error) return error;
}
return null;
};
return groupError(claim);
}
+8 -4
View File
@@ -2,6 +2,10 @@ import { TRUTH_CARD_NAMES } from './truthtrance-card-names';
import type { Plan } from './engine';
import type { Card } from './cards';
import { CHEAP_HERO_TRAITOR } from './traitors';
import {
BATTLE_CLAIM_MAX_NODES, BATTLE_CLAIM_MAX_DEPTH, BATTLE_CLAIM_MAX_TERMS,
BATTLE_CLAIM_INPUT_ERROR,
} from './battle-claim-expression';
export type PlanClaim =
| { kind: 'dial' | 'support'; compare: 'eq' | 'gte' | 'lte'; value: number }
@@ -21,24 +25,24 @@ export function parsePlanClaim(
value: unknown,
leaders: string[],
depth = 0,
budget = { remaining: 16 },
budget = { remaining: BATTLE_CLAIM_MAX_NODES },
): PlanClaim {
const fail = (): never => {
throw new PlanClaimError(
'Choose a valid battle-plan condition (at most 16 clauses and four grouping levels).',
BATTLE_CLAIM_INPUT_ERROR,
);
};
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
depth > 4 ||
depth > BATTLE_CLAIM_MAX_DEPTH ||
budget.remaining-- <= 0
)
return fail();
const v = value as Record<string, unknown>;
if (v.kind === 'and' || v.kind === 'or') {
if (!Array.isArray(v.terms) || v.terms.length < 2 || v.terms.length > 8)
if (!Array.isArray(v.terms) || v.terms.length < 2 || v.terms.length > BATTLE_CLAIM_MAX_TERMS)
return fail();
return {
kind: v.kind,
+32 -2
View File
@@ -3220,6 +3220,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Bind this turn’s reserve shipment to a destination and physical minimum, including earlier Basic phases and players whose shipment turn is still ahead.',
steps: [
'Declare Truthtrance, select an eligible player and choose Bind a shipment from reserves. Bare Basic and Advanced rules with selected native faction decks allow the question during Storm through Revival as well as before that player’s shipment. During Shipment and Movement, base Basic and Advanced, including Guild, support an unused active or waiting turn. Finish pending decisions first. Choose a printed destination and minimum of one to twenty forces, or combine conditions using AND or OR; each describes the same shipment. Answering never grants an early shipment. Optional configurations keep their separate boundaries.',
'The pure shipment builder supports up to sixteen shipment conditions, nested AND/OR groups and four grouping levels. Add, wrap, change group meaning or remove a condition; a singleton group collapses without losing its remaining statement. These app input limits do not add printed rules. Every condition describes the same eventual reserve shipment; a current fact is unnecessary.',
'Yes requires the complete statement to match while possible: all AND conditions or at least one OR condition. No requires the whole statement to be false: at least one AND condition false, or every OR condition false. A smaller count, different destination, transport of forces already on the board or no shipment may satisfy No. Forces already in the destination and ground movement do not satisfy the question. Fremen reinforcements and Guild transport from southern reserves do count.',
'The target privately sees feasible answers based on the current board, owned resources and all earlier promises. Before Movement, include remaining owned Charity and normal Revival, the current winning invoice and unused outgoing Bidding refunds; incoming Bidding aid expires. Auction payment and shipping discounts need distinct physical Karamas. At shipment time, available Ghola, Karama and current pledged aid retain their normal rules. Unknown purchases, unresolved Storm outcomes, rival gifts and future income are not assumed.',
'Accepted whole answers are public and constrain the event together. A new answer reopens your Charity or Revival readiness when needed. Claim or revive before marking Ready if that is your last legal route; bids, gifts and paid revivals cannot spend it away. Honor a required shipment before ground movement or finishing. An unresolved foreign bid and its income responses finish before release. A completed opposing effect that makes compliance impossible releases it publicly.',
@@ -3241,7 +3242,7 @@ export const RULE_TOPICS: RuleTopic[] = [
area: 'Player controls',
status: 'Implemented',
detail:
'Single shipment questions and complete bounded nested mixed authoring, grouped public preview, target-private feasible answers, original-question promise text and optional private next steps.',
'Full pure shipment AND/OR authoring now exposes sixteen shipment leaves and nested groups without adding a current fact. Add/wrap/remove/collapse, whole-input validation and exact public submission retain target-private feasibility and original-question promise text. The separate Mixed builder remains.',
},
{
area: 'AI',
@@ -3265,6 +3266,8 @@ export const RULE_TOPICS: RuleTopic[] = [
'tests/shipment-promises-bots.test.ts',
'tests/shipment-promises-recovery.test.ts',
'tests/compound-shipment-promises.test.ts',
'game/shipment-expression-input.ts', 'components/shipment-expression-fields.tsx', 'components/truth-shipment-question.tsx',
'tests/truth-shipment-expression-controls.test.ts', 'tests/truth-shipment-expression-engine.test.ts', 'tests/fixture-truth-shipment-expression.ts',
'tests/compound-shipment-bots.test.ts',
'tests/compound-shipment-controls.test.tsx',
'tests/compound-shipment-recovery.test.ts',
@@ -3293,6 +3296,32 @@ export const RULE_TOPICS: RuleTopic[] = [
},
],
},
{
id: 'truthtrance-plan-expression',
title: 'Truthtrance: grouped battle-plan questions',
category: 'Cards',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Build a whole AND/OR battle-plan claim with multiple conditions and nested groups.',
steps: [
'Choose Bind a battle-plan choice during a live battle, then another combatant. Edit dial, support, weapon/defense slots, leader or hero, and Kwisatz conditions. Group with AND or OR, add siblings or nested groups, and review the complete public claim.',
'AND requires every branch; OR at least one. No requires the whole claim to be false, not every condition separately. Removing a condition preserves its siblings, and a singleton group collapses to its remaining condition.',
'The builder shares the existing app limits: eight children per group, sixteen total items including groups and four grouping levels. These are input limits. Invalid fields anywhere block Ask publicly, without inspecting the opponent’s private cards or plan.',
'The server supplies feasible answers and compliant preparations/plans. A definite answer discards the original Truthtrance once and constrains the actual plan while possible. Existing Voice, prescience, sealing, reveal and preparation rules remain; the question does not grant a new plan or timing permission.',
],
example: 'Yes to dial exactly zero AND an empty weapon slot AND an empty defense slot requires all three. No permits a legal plan that makes at least one of them false.',
related: ['card-truthtrance', 'battle', 'truthtrance-fact-expression', 'truthtrance-shipment'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Existing parser, whole-claim feasibility, binding and sealing remain authoritative, with their unchanged input limits shared by the editor.' },
{ area: 'Player controls', status: 'Partial', detail: 'Full bounded add/wrap/remove/collapse/group switching, whole-leaf validation, readable preview and exact guarded public submission are integrated.' },
{ area: 'AI', status: 'Partial', detail: 'All four minimal profiles keep their entitled feasible-answer and compliant-plan paths. Strategic question selection and calibration remain deferred.' },
{ area: 'Documentation', status: 'Partial', detail: 'Explains whole Yes/No meaning, preserved timing/plan permissions and software limits separately from printed rules.' },
{ area: 'Verification', status: 'Partial', detail: 'Controlled authoring and genuine native battle question/answer/plan paths cover the form. Full combinations, browser/mobile/deployed and final assurance remain due.', evidence: [
'game/battle-promises.ts', 'game/battle-claim-expression.ts', 'components/battle-claim-expression-fields.tsx', 'components/truth-battle-plan-question.tsx',
'tests/truth-battle-plan-controls.test.ts', 'tests/fixture-truth-plan-expression.ts', 'tests/truth-plan-expression-engine.test.ts', 'docs/TRUTHTRANCE_NONBATTLE_READINESS_20260907.md',
] },
],
},
{
id: 'card-truthtrance',
title: 'Truthtrance',
@@ -3310,7 +3339,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'Freeform questions also accept public Yes, No, or I don’t know answers. Their truthfulness relies on the players. AI answers structured facts and battle-plan questions; it cannot yet interpret freeform questions.',
'An answer may bind actions or decisions during the current turn only. The answering player must do everything in their power to comply; if compliance later becomes impossible, the answer is no longer binding. Freeform promises are recorded in the table’s turn history. Arbitrary freeform enforcement and broader action commitments remain unfinished. Structured reserve-shipment promises include earlier bare Basic phases and unused active or waiting phase5 turns in base Basic/Advanced, including Guild; see the shipment promise guide. Ground-movement minima include earlier Basic phases with declared native families, owned Charity/ordinary revival preparation and exact normal/native auction funding, plus active or unused waiting phase5 turns before or after shipment. Yes binds a qualifying event while possible, and No remains binding through later Hajr/Ornithopter until actual turn end. See the separate ground-movement guide and its remaining preparation/timing boundaries.',
'After both plans reveal, a separate current-Traitor question binds an actual unsubmitted native voter to Reveal or decline. The declaration fulfills it before later Karama/betrayal prevention. Cunning cannot voluntarily return a matching card needed by a live Yes; completed declarations retain their native later custody rights. See the current-Traitor guide for scope.',
'Battle-plan questions can bind dial values or ranges, named weapon/defense slots, a leader or either Cheap Hero, spice support and Kwisatz use. AND/OR combinations preserve legal alternatives. Only answers with a legal completion now or through available Ghola preparation are offered; a sealed plan fixes its answer. Previous battle commitments, Voice and prescience also apply. Private preparation guidance can play a required Ghola, then a compliant-plan example can fill the form for review before sealing.',
'Battle-plan questions can bind dial values or ranges, named weapon/defense slots, a leader or either Cheap Hero, spice support and Kwisatz use. The complete grouped builder supports eight children per group, sixteen total conditions including groups and depth four. Add/wrap/remove/collapse retains the whole claim and validates every field. AND/OR combinations preserve legal alternatives. Only answers with a legal completion now or through available Ghola preparation are offered; a sealed plan fixes its answer. Previous battle commitments, Voice and prescience also apply. Private preparation guidance can play a required Ghola, then a compliant-plan example can fill the form for review before sealing.',
'Named conditions include Reinforcements and Harass slot occupancy without making either a weapon/defense category. In clean classic Homeworlds, a source-aware legal completion can choose the native fee needed for a promised dial; review and filling retain that exact allocation. Active preview, answers/history and server records use the actual combat location name, including native Worlds, rather than treating every battle as an Arrakis territory.',
'If a completed opposing power makes a battle answer impossible, it is publicly released. A pending Voice response resolves before that release. A player cannot voluntarily spend a promised battle card or give away necessary support spice to evade the answer. Feasibility includes available Ghola cards, eligible leader/Kwisatz or force revivals, and possible revival income. Each actual card can be spent only once. Other future card/power sequences and the full timing audit remain unfinished.',
'Holding both copies lets you declare one or both. Each question resolves separately, and each card is discarded only after its definite answer. Full timing and card-face audits remain pending.',
@@ -3323,6 +3352,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'truthtrance-card-count',
'truthtrance-hand-inventory',
'truthtrance-fact-expression',
'truthtrance-plan-expression',
'truthtrance-shipment',
'truthtrance-movement',
'richese-no-field',
+36
View File
@@ -0,0 +1,36 @@
import {
parseShipmentExpression,
ShipmentClaimError,
type ShipmentExpression,
} from './shipment-promises';
/** Shipment limits count leaves, rather than counting the containing groups. */
export function shipmentExpressionInputSize(value: ShipmentExpression): { leaves: number; depth: number } {
if (!('op' in value)) return { leaves: 1, depth: 0 };
const children = value.terms.map(shipmentExpressionInputSize);
return {
leaves: children.reduce((sum, child) => sum + child.leaves, 0),
depth: 1 + Math.max(0, ...children.map(child => child.depth)),
};
}
/** Use the existing parser's structure and bounds while unfinished leaf fields
* remain editable. Frozen current facts are never valid pure-shipment input. */
export function shipmentExpressionInputError(value: ShipmentExpression): string | null {
const structure = (node: unknown): unknown => {
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
if ('op' in node) return {
...node,
...('terms' in node && Array.isArray(node.terms) ? { terms: node.terms.map(structure) } : {}),
};
if ('constant' in node) return node;
return { ...node, territory: 'arrakeen', minimum: 1 };
};
try {
parseShipmentExpression(structure(value), { allowConstants: false });
return null;
} catch (error) {
if (error instanceof ShipmentClaimError) return error.message;
throw error;
}
}