'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
{group ? `Alliance group ${path} · ${node.kind.toUpperCase()}` : `Alliance condition ${path}`} {group ? <> {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: AllianceExpression = { ...node, terms: [...node.terms, { kind, terms: [newLeaf(), newLeaf()] }] }; return ; })}
: <> }
{(['and', 'or'] as const).map(kind => )} {remove && }
; }; return
Final alliance expression · Current Nexus

Each condition concerns actual membership when this Nexus closes. Offers alone are not alliances. 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.

Yes and No settle only at the actual Nexus close. Forming an alliance does not fulfill the answer early. You cannot promise the other player’s consent. If future consent is needed, unknown may apply; ask again or save the card.

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

{size.leaves} / {ALLIANCE_EXPRESSION_MAX_LEAVES} conditions · {size.depth} / {ALLIANCE_EXPRESSION_MAX_DEPTH} levels of grouping

{error &&

{error}

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