'use client'; import type { ReactElement } from 'react'; import { parseAllianceExpression, ALLIANCE_EXPRESSION_MAX_DEPTH, ALLIANCE_EXPRESSION_MAX_LEAVES, type AllianceExpression, } from '@/game/alliance-expression-promises'; import { Button } from './ui/button'; type Partner = { id: string; name: string }; const newLeaf = (): AllianceExpression => ({ kind: 'partner', partner: '' }); export function allianceExpressionInputError(value: unknown, partners: readonly Partner[]): string | null { try { const expression = parseAllianceExpression(value); const visit = (node: AllianceExpression): void => { if (node.kind === 'partner') { if (!partners.some(partner => partner.id === node.partner)) throw new Error('Choose a current named other alliance partner.'); } else node.terms.forEach(visit); }; visit(expression); return null; } catch (error) { return error instanceof Error ? error.message : 'Choose a valid final-alliance expression.'; } } // Detached placeholders validate only structure, leaving incomplete public // partner choices visible and repairable in the original controlled tree. const structuralDraft = (node: AllianceExpression): AllianceExpression => node.kind === 'partner' ? { ...node, partner: 'partner' } : { ...node, terms: node.terms.map(structuralDraft) }; const inputSize = (node: AllianceExpression): { leaves: number; depth: number } => { if (node.kind === 'partner') 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)) }; }; /** Public partner choices never predict another player's consent. The server * owns feasibility and final actual membership at the original Nexus close. */ export function AllianceExpressionFields({ value, partners, disabled, onChange }: { value: AllianceExpression; partners: readonly Partner[]; disabled: boolean; onChange: (expression: AllianceExpression) => void; }) { const size = inputSize(value), error = allianceExpressionInputError(value, partners); const structuralError = (next: AllianceExpression) => { try { parseAllianceExpression(structuralDraft(next)); return false; } catch { return true; } }; const commit = (next: AllianceExpression) => { if (!disabled && !structuralError(next)) onChange(next); }; const render = (node: AllianceExpression, path: string, replace: (next: AllianceExpression) => AllianceExpression, remove?: () => AllianceExpression): ReactElement => { const group = node.kind !== 'partner', id = `truth-alliance-expression-${path}`; const allowed = (next: AllianceExpression) => !disabled && !structuralError(replace(next)); const change = (next: AllianceExpression) => commit(replace(next)); const wrap = (kind: 'and' | 'or'): AllianceExpression => ({ kind, terms: [node, newLeaf()] }); return
; }; return ; }