import { useState } from "react"; import type { PuzzleDefinition, VariantConstraint } from "../domain/types"; import { removeKillerCagesAtCells, replaceOverlappingKillerCages, selectionTouchesKillerCage, } from "./constraintEditing"; interface ConstraintEditorProps { puzzle: PuzzleDefinition; selection: readonly number[]; onChange: (puzzle: PuzzleDefinition) => void; onNewGrid: (size: number) => void; onCheck: () => void; onGenerate: () => void; busy: boolean; } function describeConstraint(constraint: VariantConstraint, size: number) { const cell = (index: number) => `r${String(Math.floor(index / size) + 1)}c${String((index % size) + 1)}`; switch (constraint.type) { case "diagonal": return `${constraint.direction} diagonal`; case "anti-knight": return "anti-knight"; case "anti-king": return "anti-king"; case "non-consecutive": return "non-consecutive"; case "killer-cage": return `${String(constraint.sum)} cage · ${String(constraint.cells.length)} cells`; case "thermo": case "renban": case "palindrome": return `${constraint.type} · ${String(constraint.cells.length)} cells`; case "arrow": return `arrow · ${String(constraint.bulb.length)} bulb / ${String(constraint.line.length)} line`; case "kropki": return `${constraint.kind} dot · ${cell(constraint.a)}–${cell(constraint.b)}`; case "xv": return `sum ${String(constraint.total)} pair · ${cell(constraint.a)}–${cell(constraint.b)}`; case "inequality": return `${cell(constraint.lesser)} < ${cell(constraint.greater)}`; } } export function ConstraintEditor({ puzzle, selection, onChange, onNewGrid, onCheck, onGenerate, busy, }: ConstraintEditorProps) { const [cageSum, setCageSum] = useState(10); const [region, setRegion] = useState(1); const constraints = puzzle.constraints ?? []; const append = (constraint: VariantConstraint) => onChange({ ...puzzle, constraints: [...constraints, constraint] }); const need = (count: number) => selection.length === count; const atLeast = (count: number) => selection.length >= count; const cageCellCount = Math.max(1, selection.length); const minimumCageSum = (cageCellCount * (cageCellCount + 1)) / 2; const maximumCageSum = (cageCellCount * (2 * puzzle.size - cageCellCount + 1)) / 2; const validCage = selection.length >= 1 && selection.length <= puzzle.size && Number.isInteger(cageSum) && cageSum >= minimumCageSum && cageSum <= maximumCageSum; const selectedCageExists = selectionTouchesKillerCage(constraints, selection); const toggleGlobal = ( type: "anti-knight" | "anti-king" | "non-consecutive", ) => { const exists = constraints.some((constraint) => constraint.type === type); onChange({ ...puzzle, constraints: exists ? constraints.filter((constraint) => constraint.type !== type) : [...constraints, { type }], }); }; return (
Puzzle definition
Selected cells
Selection order defines lines and pair direction. Shift-click or drag to build a selection.
Adding replaces any cage touching the selection. Removing clears every cage touching a selected cell.
Global rules
Constraints