'use client'; import type { ReactElement } from 'react'; import { TERRITORIES } from '@/game/board'; import { parseMovementExpression, MOVEMENT_EXPRESSION_MAX_DEPTH, MOVEMENT_EXPRESSION_MAX_LEAVES, type MovementExpression, } from '@/game/movement-expression-promises'; import { Button } from './ui/button'; import { Input } from './ui/input'; type TerritoryChoice = { id: string; name: string }; export function movementExpressionInputError(value: unknown): string | null { try { parseMovementExpression(value); return null; } catch (error) { return error instanceof Error ? error.message : 'Choose a valid ground-movement expression.'; } } // Structural edits preserve repairable destination and numeric drafts. The // detached valid placeholders are used only to check tree shape and limits. const structuralDraft = (node: MovementExpression): MovementExpression => node.kind === 'move' ? { kind: 'move', territory: 'carthag', minimum: 1 } : { ...node, terms: node.terms.map(structuralDraft) }; const inputSize = (node: MovementExpression): { leaves: number; depth: number } => { if (node.kind === 'move') return { leaves: 1, depth: 0 }; const sizes = node.terms.map(inputSize); return { leaves: sizes.reduce((sum, child) => sum + child.leaves, 0), depth: 1 + Math.max(0, ...sizes.map(child => child.depth)) }; }; /** Hook-free controlled tree. The server decides future route feasibility. */ export function MovementExpressionFields({ value, territories, disabled, onChange }: { value: MovementExpression; territories: readonly TerritoryChoice[]; disabled: boolean; onChange: (expression: MovementExpression) => void; }) { const newLeaf = (): MovementExpression => ({ kind: 'move', territory: territories[0]?.id ?? 'carthag', minimum: 1 }); const size = inputSize(value), error = movementExpressionInputError(value); const commit = (next: MovementExpression) => { if (!disabled && !movementExpressionInputError(structuralDraft(next))) onChange(next); }; const render = (node: MovementExpression, path: string, replace: (next: MovementExpression) => MovementExpression, remove?: () => MovementExpression): ReactElement => { const id = `truth-movement-expression-${path}`; const allowed = (next: MovementExpression) => !disabled && !movementExpressionInputError(structuralDraft(replace(next))); const change = (next: MovementExpression) => commit(replace(next)); const wrap = (kind: 'and' | 'or'): MovementExpression => ({ kind, terms: [node, newLeaf()] }); return
{node.kind === 'move' ? `Movement condition ${path}` : `Movement group ${path} · ${node.kind.toUpperCase()}`} {node.kind === 'move' ? <> change({ ...node, minimum: event.currentTarget.valueAsNumber })} /> : <> {node.terms.map((term, index) => render(term, `${path}.${index + 1}`, next => replace({ ...node, terms: node.terms.map((old, i) => i === index ? next : old) }), () => { const terms = node.terms.filter((_, i) => i !== index); return replace(terms.length === 1 ? terms[0] : { ...node, terms }); }))}
{(['and', 'or'] as const).map(kind => { const next: MovementExpression = { ...node, terms: [...node.terms, { kind, terms: [newLeaf(), newLeaf()] }] }; return ; })}
}
{(['and', 'or'] as const).map(kind => )} {remove && }
; }; return
Ground-movement expression

AND requires every condition; OR requires at least one. No makes the whole expression false, not necessarily every condition. Legal alternatives remain available; the question does not pin a fixed branch. Each condition needs one future actual ground move after the answer, before your Movement turn ends this round. Name an exact sector when needed. Smaller moves never add together; a shipment can prepare a move but does not fulfill a condition.

App input limits: {MOVEMENT_EXPRESSION_MAX_LEAVES} conditions and {MOVEMENT_EXPRESSION_MAX_DEPTH} levels of grouping. Groups do not count as conditions. These are input limits, not additional printed rules.

{size.leaves} / {MOVEMENT_EXPRESSION_MAX_LEAVES} conditions · {size.depth} / {MOVEMENT_EXPRESSION_MAX_DEPTH} levels of grouping

{error &&

{error}

} {render(value, '1', next => next)}
; }