Files
sudoku-tools/src/domain/constraintRegistry.ts
T

483 lines
12 KiB
TypeScript

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<VariantConstraint, { readonly type: Type }>,
) => readonly CellId[];
}
type ConstraintRegistry = {
readonly [Type in ConstraintType]: ConstraintRegistryEntry<Type>;
};
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<Type extends ConstraintType>(
value: ConstraintRegistryEntry<Type>,
): ConstraintRegistryEntry<Type> {
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 extends ConstraintType>(
type: Type,
): ConstraintRegistryEntry<Type> {
return CONSTRAINT_REGISTRY[type] as unknown as ConstraintRegistryEntry<Type>;
}
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<string> {
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))];
}