'use client'; import type { ReactElement } from 'react'; 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' ? { ...node, 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. 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)}
; }