import { describe, expect, it } from "vitest"; import { candidatesForCell, classicRegions, compilePuzzle, constraintIsFeasible, normalizePuzzle, validatePuzzle, type PuzzleDefinition, type VariantConstraint, } from "../../src/domain"; import { solveExact } from "../../src/solver"; const solved4 = [1, 2, 3, 4, 3, 4, 1, 2, 4, 3, 2, 1, 2, 1, 4, 3] as const; function puzzle4(overrides: Partial = {}): PuzzleDefinition { return { version: 1, size: 4, givens: new Array(16).fill(0), regions: classicRegions(4), constraints: [], ...overrides, }; } const packTwoConstraints: readonly VariantConstraint[] = [ { type: "between-line", cells: [0, 1, 2] }, { type: "german-whisper", cells: [0, 3, 1] }, { type: "region-sum-line", cells: [0, 1, 2] }, { type: "clone", cells: [0, 1], cloneCells: [6, 7] }, { type: "extra-region", cells: [0, 7, 9, 14] }, ]; describe("Pack 2 constraint validation", () => { it("accepts and clones every production shape", () => { const source = puzzle4({ constraints: packTwoConstraints }); expect(validatePuzzle(source)).toEqual({ valid: true, issues: [] }); const normalized = normalizePuzzle(source); expect(normalized.constraints).toEqual(packTwoConstraints); expect(normalized.constraints).not.toBe(packTwoConstraints); }); it.each([ { type: "between-line", cells: [0, 1] }, { type: "between-line", cells: [0, 1, 0] }, { type: "german-whisper", cells: [0] }, { type: "german-whisper", cells: [0, 1], minimumDifference: 0 }, { type: "german-whisper", cells: [0, 1], minimumDifference: 4 }, { type: "region-sum-line", cells: [0, 1] }, { type: "clone", cells: [0, 1], cloneCells: [8] }, { type: "clone", cells: [0, 1], cloneCells: [8, 8] }, { type: "extra-region", cells: [0, 1, 2] }, { type: "extra-region", cells: [0, 1, 2, 3], negated: true }, ] as const)("rejects malformed constraint $type", (constraint) => { const result = validatePuzzle({ ...puzzle4(), constraints: [constraint] }); expect(result.valid).toBe(false); expect( result.issues.some(({ path }) => path.startsWith("constraints[0]")), ).toBe(true); }); it("uses the puzzle's actual regions when checking region-sum crossings", () => { expect( validatePuzzle( puzzle4({ constraints: [{ type: "region-sum-line", cells: [0, 1] }] }), ).issues, ).toContainEqual({ path: "constraints[0].cells", message: "region-sum line must cross at least one region boundary", }); expect( validatePuzzle( puzzle4({ constraints: [{ type: "region-sum-line", cells: [1, 2] }] }), ).valid, ).toBe(true); }); }); describe("Pack 2 partial and completed semantics", () => { it("enforces between-line interiors and its negated truth", () => { const constraint = { type: "between-line", cells: [0, 1, 2] } as const; const partial = new Array(16).fill(0); partial[0] = 1; partial[2] = 4; expect( candidatesForCell(puzzle4({ constraints: [constraint] }), partial, 1), ).toEqual([2, 3]); expect( constraintIsFeasible( constraint, [2, 0, 3, ...new Array(13).fill(0)], 4, ), ).toBe(false); expect(constraintIsFeasible(constraint, solved4, 4)).toBe(true); expect( constraintIsFeasible({ ...constraint, negated: true }, solved4, 4), ).toBe(false); expect( constraintIsFeasible( { ...constraint, negated: true }, [1, 4, 3, ...new Array(13).fill(0)], 4, ), ).toBe(true); }); it("runs exact dynamic feasibility for default and explicit German whispers", () => { const constraint = { type: "german-whisper", cells: [0, 5, 10], } as const; const partial = new Array(16).fill(0); partial[0] = 2; expect( candidatesForCell(puzzle4({ constraints: [constraint] }), partial, 5), ).toEqual([4]); const impossible = new Array(16).fill(0); impossible[5] = 2; expect( constraintIsFeasible( { ...constraint, minimumDifference: 3 }, impossible, 4, ), ).toBe(false); const complete = [1, 4, 2, ...new Array(13).fill(0)]; const short = { type: "german-whisper", cells: [0, 1, 2] } as const; expect(constraintIsFeasible(short, complete, 4)).toBe(true); expect(constraintIsFeasible({ ...short, negated: true }, complete, 4)).toBe( false, ); complete[1] = 2; expect(constraintIsFeasible({ ...short, negated: true }, complete, 4)).toBe( true, ); }); it("balances contiguous region sums using the supplied region map", () => { const constraint = { type: "region-sum-line", cells: [0, 1, 2], } as const; const partial = new Array(16).fill(0); partial[0] = 1; partial[2] = 3; expect( candidatesForCell(puzzle4({ constraints: [constraint] }), partial, 1), ).toEqual([2]); expect(constraintIsFeasible(constraint, solved4, 4)).toBe(true); expect( constraintIsFeasible({ ...constraint, negated: true }, solved4, 4), ).toBe(false); const unequal = [...solved4]; unequal[2] = 4; expect( constraintIsFeasible({ ...constraint, negated: true }, unequal, 4), ).toBe(true); }); it("enforces ordered clone equality and exact negated state", () => { const constraint = { type: "clone", cells: [0, 1], cloneCells: [6, 7], } as const; const partial = new Array(16).fill(0); partial[0] = 1; partial[1] = 2; partial[6] = 1; expect( candidatesForCell(puzzle4({ constraints: [constraint] }), partial, 7), ).toEqual([2]); expect(constraintIsFeasible(constraint, solved4, 4)).toBe(true); expect( constraintIsFeasible({ ...constraint, negated: true }, solved4, 4), ).toBe(false); const mismatch = [...solved4]; mismatch[7] = 3; expect(constraintIsFeasible(constraint, mismatch, 4)).toBe(false); expect( constraintIsFeasible({ ...constraint, negated: true }, mismatch, 4), ).toBe(true); }); it("compiles extra regions as all-different units and remote peers", () => { const constraint = { type: "extra-region", cells: [0, 7, 9, 14], } as const; const compiled = compilePuzzle( normalizePuzzle(puzzle4({ constraints: [constraint] })), ); expect( compiled.units.filter(({ kind }) => kind === "extra-region"), ).toEqual([{ kind: "extra-region", index: 0, cells: constraint.cells }]); const partial = new Array(16).fill(0); partial[0] = 1; expect(candidatesForCell(compiled, partial, 7)).not.toContain(1); expect(compilePuzzle(normalizePuzzle(puzzle4())).peers[7]?.has(0)).toBe( false, ); }); }); describe("Pack 2 exact solving", () => { it.each( packTwoConstraints.map( (constraint) => [constraint.type, constraint] as const, ), )("solves a puzzle containing %s", (_type, constraint) => { const givens: number[] = [...solved4]; givens[5] = 0; givens[10] = 0; const result = solveExact(puzzle4({ givens, constraints: [constraint] })); expect(result.count).toBe(1); expect(result.truncated).toBe(false); expect(result.solutions[0]).toEqual(solved4); }); });