import { describe, expect, it } from "vitest"; import { classicRegions, compilePuzzle, normalizePuzzle, } from "../../src/domain"; import { findAic, findAdvancedLogicalStep, findFinnedFish, findJellyfish, findSimpleColouring, findSkyscraper, findTwoStringKite, findUniqueRectangle, findWWing, findXChain, findXYChain, solveLogically, type AdvancedLogicalContext, } from "../../src/solver"; const size = 9; const empty = normalizePuzzle({ version: 1, size, givens: new Array(size * size).fill(0), regions: classicRegions(size), constraints: [], }); const compiled = compilePuzzle(empty); function cell(row: number, column: number): number { return row * size + column; } function mask(values: readonly number[]): number { return values.reduce((result, value) => result | (1 << value), 0); } function context( candidates: ReadonlyArray, options: Pick< AdvancedLogicalContext, "uniquenessProven" | "uniquenessPatternsSafe" > = {}, ): AdvancedLogicalContext { const masks = new Array(size * size).fill(0); for (const [candidateCell, values] of candidates) { masks[candidateCell] = mask(values); } return { size, values: new Array(size * size).fill(0), masks, regions: empty.regions, peers: compiled.peers, units: compiled.units, ...options, }; } function expectElimination( result: ReturnType, technique: string, target: number, value: number, ): void { expect(result?.technique).toBe(technique); expect(result?.placements).toEqual([]); expect(result?.eliminations).toContainEqual({ cell: target, values: [value], }); expect(result?.explanation.length).toBeGreaterThan(12); } describe("advanced logical techniques", () => { it("finds a row-based Jellyfish", () => { const target = cell(4, 0); const result = findJellyfish( context([ [cell(0, 0), [5]], [cell(0, 1), [5]], [cell(1, 1), [5]], [cell(1, 2), [5]], [cell(2, 2), [5]], [cell(2, 3), [5]], [cell(3, 0), [5]], [cell(3, 3), [5]], [target, [5, 8]], ]), ); expectElimination(result, "jellyfish", target, 5); expect( findAdvancedLogicalStep( context([ [cell(0, 0), [5]], [cell(0, 1), [5]], [cell(1, 1), [5]], [cell(1, 2), [5]], [cell(2, 2), [5]], [cell(2, 3), [5]], [cell(3, 0), [5]], [cell(3, 3), [5]], [target, [5, 8]], ]), ), ).toEqual(result); }); it("finds Finned X-Wings and Finned Swordfish", () => { const xWingTarget = cell(2, 0); const xWing = findFinnedFish( context([ [cell(0, 0), [5]], [cell(0, 1), [5]], [cell(0, 2), [5]], [cell(1, 0), [5]], [cell(1, 1), [5]], [xWingTarget, [5, 8]], ]), ); expectElimination(xWing, "finned-x-wing", xWingTarget, 5); const swordfishTarget = cell(1, 0); const swordfish = findFinnedFish( context([ [cell(0, 0), [6]], [cell(0, 1), [6]], [cell(0, 2), [6]], [cell(3, 3), [6]], [cell(3, 6), [6]], [cell(4, 3), [6]], [cell(4, 6), [6]], [swordfishTarget, [6, 8]], ]), ); expectElimination(swordfish, "finned-swordfish", swordfishTarget, 6); }); it("finds a Skyscraper", () => { const target = cell(2, 5); const result = findSkyscraper( context([ [cell(0, 0), [7]], [cell(0, 3), [7]], [cell(1, 0), [7]], [cell(1, 4), [7]], [target, [2, 7]], ]), ); expectElimination(result, "skyscraper", target, 7); }); it("finds a Two-String Kite", () => { const target = cell(7, 6); const result = findTwoStringKite( context([ [cell(0, 0), [6]], [cell(0, 6), [6]], [cell(1, 1), [6]], [cell(7, 1), [6]], [target, [3, 6]], ]), ); expectElimination(result, "two-string-kite", target, 6); }); it("applies a simple-colouring colour trap", () => { const target = cell(1, 1); const result = findSimpleColouring( context([ [cell(0, 0), [4]], [cell(0, 4), [4]], [cell(4, 4), [4]], [cell(4, 1), [4]], [target, [4, 8]], [cell(2, 2), [4, 7]], [cell(7, 1), [4, 9]], ]), ); expectElimination(result, "simple-colouring", target, 4); }); it("finds a W-Wing", () => { const target = cell(0, 4); const result = findWWing( context([ [cell(0, 0), [1, 2]], [cell(4, 4), [1, 2]], [cell(0, 3), [1, 3, 4]], [cell(4, 3), [1, 3, 4]], [target, [2, 4]], ]), ); expectElimination(result, "w-wing", target, 2); }); it("finds an X-Chain", () => { const target = cell(1, 1); const result = findXChain( context([ [cell(0, 0), [5]], [cell(0, 4), [5]], [cell(3, 4), [5]], [cell(3, 1), [5]], [target, [5, 8]], [cell(2, 2), [5, 7]], [cell(7, 1), [5, 9]], ]), ); expectElimination(result, "x-chain", target, 5); }); it("finds an XY-Chain", () => { const target = cell(4, 0); const result = findXYChain( context([ [cell(0, 0), [1, 2]], [cell(0, 4), [2, 3]], [cell(4, 4), [1, 3]], [target, [1, 4, 5]], ]), ); expectElimination(result, "xy-chain", target, 1); }); it("finds a mixed Alternating Inference Chain", () => { const target = cell(1, 1); const result = findAic( context([ [cell(0, 0), [1, 2]], [cell(0, 4), [2, 4, 5]], [cell(4, 4), [2, 3, 4]], [cell(4, 8), [1, 3, 4]], [cell(4, 1), [1, 5, 6]], [target, [1, 7, 8]], ]), ); expectElimination(result, "aic", target, 1); }); it("never uses a Unique Rectangle without both safety gates", () => { const roof = cell(1, 3); const candidates = [ [cell(0, 0), [1, 2]], [cell(0, 3), [1, 2]], [cell(1, 0), [1, 2]], [roof, [1, 2, 3]], ] as const; expect(findUniqueRectangle(context(candidates))).toBeUndefined(); expect( findUniqueRectangle(context(candidates, { uniquenessProven: true })), ).toBeUndefined(); expect( findUniqueRectangle( context(candidates, { uniquenessProven: true, uniquenessPatternsSafe: true, }), ), ).toMatchObject({ technique: "unique-rectangle", eliminations: [{ cell: roof, values: [1, 2] }], }); }); it("integrates the explicit uniqueness proof gate into solveLogically", () => { const digits = Array.from({ length: size }, (_unused, index) => index + 1); const candidates = Array.from({ length: size * size }, () => [...digits]); const withoutPair = digits.filter((value) => value !== 1 && value !== 2); const first = cell(0, 0); const second = cell(0, 3); const third = cell(1, 0); const roof = cell(1, 3); for (let current = 0; current < size * size; current += 1) { const row = Math.floor(current / size); const column = current % size; if (row === 0 || column === 0 || empty.regions[current] === 0) { candidates[current] = [...withoutPair]; } } candidates[first] = [1, 2]; candidates[second] = [1, 2]; candidates[third] = [1, 2]; candidates[roof] = [1, 2, 3]; const ordinary = solveLogically(empty, { candidates, maxSteps: 1 }); expect(ordinary.steps[0]?.technique).not.toBe("unique-rectangle"); const provenUnique = solveLogically(empty, { candidates, maxSteps: 1, uniquenessProven: true, }); expect(provenUnique.steps[0]).toMatchObject({ technique: "unique-rectangle", eliminations: [{ cell: roof, values: [1, 2] }], }); }); });