import { correspondingBoxPositionCells, littleKillerCells, orthogonalNeighbours, outsideLineCells, } from "./geometry"; import type { CellId, VariantConstraint } from "./types"; export type ConstraintType = VariantConstraint["type"]; export type ConstraintCategory = "global" | "region" | "line" | "adjacency" | "outside" | "cell"; export type ConstraintFieldKind = | "discriminator" | "boolean" | "integer" | "enum" | "cell" | "cells" | "integers" | "outside-side"; export interface ConstraintFieldMetadata { readonly key: string; readonly kind: ConstraintFieldKind; readonly required: boolean; } export interface ConstraintRegistryEntry< Type extends ConstraintType = ConstraintType, > { readonly type: Type; readonly label: string; readonly category: ConstraintCategory; readonly negatable: boolean; readonly fields: readonly ConstraintFieldMetadata[]; readonly cells: ( size: number, constraint: Extract, ) => readonly CellId[]; } type ConstraintRegistry = { readonly [Type in ConstraintType]: ConstraintRegistryEntry; }; const typeField: ConstraintFieldMetadata = { key: "type", kind: "discriminator", required: true, }; const negatedField: ConstraintFieldMetadata = { key: "negated", kind: "boolean", required: false, }; const allCells = (size: number): CellId[] => Array.from({ length: size * size }, (_, cell) => cell); const cellField = (key = "cell"): ConstraintFieldMetadata => ({ key, kind: "cell", required: true, }); const cellsField = (key = "cells"): ConstraintFieldMetadata => ({ key, kind: "cells", required: true, }); const integerField = (key: string): ConstraintFieldMetadata => ({ key, kind: "integer", required: true, }); const enumField = (key: string): ConstraintFieldMetadata => ({ key, kind: "enum", required: true, }); const outsideFields = ( valueField: string, ): readonly ConstraintFieldMetadata[] => [ typeField, { key: "side", kind: "outside-side", required: true }, integerField("index"), integerField(valueField), negatedField, ]; function entry( value: ConstraintRegistryEntry, ): ConstraintRegistryEntry { return value; } export const CONSTRAINT_REGISTRY = { diagonal: entry({ type: "diagonal", label: "Diagonal", category: "global", negatable: false, fields: [typeField, enumField("direction")], cells: (size, constraint) => Array.from({ length: size }, (_, index) => constraint.direction === "main" ? index * size + index : index * size + size - index - 1, ), }), "anti-knight": entry({ type: "anti-knight", label: "Anti-knight", category: "global", negatable: false, fields: [typeField], cells: (size) => allCells(size), }), "anti-king": entry({ type: "anti-king", label: "Anti-king", category: "global", negatable: false, fields: [typeField], cells: (size) => allCells(size), }), "non-consecutive": entry({ type: "non-consecutive", label: "Non-consecutive", category: "global", negatable: false, fields: [typeField], cells: (size) => allCells(size), }), "disjoint-groups": entry({ type: "disjoint-groups", label: "Disjoint groups", category: "global", negatable: false, fields: [typeField], cells: (size) => allCells(size), }), "killer-cage": entry({ type: "killer-cage", label: "Killer cage", category: "region", negatable: true, fields: [ typeField, cellsField(), integerField("sum"), { key: "noRepeat", kind: "boolean", required: false }, negatedField, ], cells: (_size, constraint) => constraint.cells, }), thermo: entry({ type: "thermo", label: "Thermometer", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), arrow: entry({ type: "arrow", label: "Arrow", category: "line", negatable: true, fields: [typeField, cellsField("bulb"), cellsField("line"), negatedField], cells: (_size, constraint) => [...constraint.bulb, ...constraint.line], }), kropki: entry({ type: "kropki", label: "Kropki dot", category: "adjacency", negatable: true, fields: [ typeField, cellField("a"), cellField("b"), enumField("kind"), negatedField, ], cells: (_size, constraint) => [constraint.a, constraint.b], }), xv: entry({ type: "xv", label: "XV pair", category: "adjacency", negatable: true, fields: [ typeField, cellField("a"), cellField("b"), integerField("total"), negatedField, ], cells: (_size, constraint) => [constraint.a, constraint.b], }), inequality: entry({ type: "inequality", label: "Inequality", category: "adjacency", negatable: true, fields: [ typeField, cellField("lesser"), cellField("greater"), negatedField, ], cells: (_size, constraint) => [constraint.lesser, constraint.greater], }), renban: entry({ type: "renban", label: "Renban line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), palindrome: entry({ type: "palindrome", label: "Palindrome line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), "x-sum": entry({ type: "x-sum", label: "X-sum", category: "outside", negatable: true, fields: outsideFields("sum"), cells: (size, constraint) => outsideLineCells(size, constraint.side, constraint.index), }), skyscraper: entry({ type: "skyscraper", label: "Skyscraper", category: "outside", negatable: true, fields: outsideFields("count"), cells: (size, constraint) => outsideLineCells(size, constraint.side, constraint.index), }), quadruple: entry({ type: "quadruple", label: "Quadruple", category: "cell", negatable: true, fields: [ typeField, cellsField(), { key: "digits", kind: "integers", required: true }, negatedField, ], cells: (_size, constraint) => constraint.cells, }), maximum: entry({ type: "maximum", label: "Maximum cell", category: "cell", negatable: true, fields: [typeField, cellField(), negatedField], cells: (size, constraint) => [ constraint.cell, ...orthogonalNeighbours(size, constraint.cell), ], }), minimum: entry({ type: "minimum", label: "Minimum cell", category: "cell", negatable: true, fields: [typeField, cellField(), negatedField], cells: (size, constraint) => [ constraint.cell, ...orthogonalNeighbours(size, constraint.cell), ], }), odd: entry({ type: "odd", label: "Odd cell", category: "cell", negatable: true, fields: [typeField, cellField(), negatedField], cells: (_size, constraint) => [constraint.cell], }), even: entry({ type: "even", label: "Even cell", category: "cell", negatable: true, fields: [typeField, cellField(), negatedField], cells: (_size, constraint) => [constraint.cell], }), "little-killer": entry({ type: "little-killer", label: "Little killer", category: "outside", negatable: true, fields: [ typeField, { key: "side", kind: "outside-side", required: true }, integerField("index"), enumField("direction"), integerField("sum"), negatedField, ], cells: (size, constraint) => littleKillerCells( size, constraint.side, constraint.index, constraint.direction, ), }), sandwich: entry({ type: "sandwich", label: "Sandwich sum", category: "outside", negatable: true, fields: outsideFields("sum"), cells: (size, constraint) => outsideLineCells(size, constraint.side, constraint.index), }), "between-line": entry({ type: "between-line", label: "Between line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), "german-whisper": entry({ type: "german-whisper", label: "German whisper", category: "line", negatable: true, fields: [ typeField, cellsField(), { key: "minimumDifference", kind: "integer", required: false }, negatedField, ], cells: (_size, constraint) => constraint.cells, }), "region-sum-line": entry({ type: "region-sum-line", label: "Region-sum line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), clone: entry({ type: "clone", label: "Clone regions", category: "region", negatable: true, fields: [typeField, cellsField(), cellsField("cloneCells"), negatedField], cells: (_size, constraint) => [ ...constraint.cells, ...constraint.cloneCells, ], }), "extra-region": entry({ type: "extra-region", label: "Extra region", category: "region", negatable: false, fields: [typeField, cellsField()], cells: (_size, constraint) => constraint.cells, }), "modular-line": entry({ type: "modular-line", label: "Modular line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), "entropic-line": entry({ type: "entropic-line", label: "Entropic line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), "zipper-line": entry({ type: "zipper-line", label: "Zipper line", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), "double-arrow": entry({ type: "double-arrow", label: "Double arrow", category: "line", negatable: true, fields: [typeField, cellsField(), negatedField], cells: (_size, constraint) => constraint.cells, }), indexer: entry({ type: "indexer", label: "Indexer", category: "cell", negatable: true, fields: [typeField, enumField("kind"), cellField(), negatedField], cells: (size, constraint) => { const row = Math.floor(constraint.cell / size); const column = constraint.cell % size; if (constraint.kind === "row") { return Array.from( { length: size }, (_, targetRow) => targetRow * size + column, ); } if (constraint.kind === "column") { return Array.from( { length: size }, (_, targetColumn) => row * size + targetColumn, ); } return correspondingBoxPositionCells(size, constraint.cell); }, }), fog: entry({ type: "fog", label: "Fog of war", category: "global", negatable: false, fields: [ typeField, cellsField("lights"), { key: "revealRadius", kind: "integer", required: false }, ], cells: (_size, constraint) => constraint.lights, }), } satisfies ConstraintRegistry; export const CONSTRAINT_TYPES = Object.freeze( Object.keys(CONSTRAINT_REGISTRY) as ConstraintType[], ); export function isConstraintType(value: string): value is ConstraintType { return Object.hasOwn(CONSTRAINT_REGISTRY, value); } export function constraintMetadata( type: Type, ): ConstraintRegistryEntry { return CONSTRAINT_REGISTRY[type] as unknown as ConstraintRegistryEntry; } export function constraintLabel(type: string): string { if (isConstraintType(type)) return CONSTRAINT_REGISTRY[type].label; return type .split("-") .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`) .join(" "); } export function constraintAllowedFields( type: ConstraintType, ): ReadonlySet { return new Set(CONSTRAINT_REGISTRY[type].fields.map(({ key }) => key)); } export function constraintCells( size: number, constraint: VariantConstraint, ): readonly CellId[] { const resolver = CONSTRAINT_REGISTRY[constraint.type].cells as ( size: number, constraint: never, ) => readonly CellId[]; return [...new Set(resolver(size, constraint as never))]; }