Files
sudoku-tools/tests/solver/advancedLogical.test.ts
T

312 lines
7.8 KiB
TypeScript

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<number>(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<readonly [number, readonly number[]]>,
options: Pick<
AdvancedLogicalContext,
"uniquenessProven" | "uniquenessPatternsSafe"
> = {},
): AdvancedLogicalContext {
const masks = new Array<number>(size * size).fill(0);
for (const [candidateCell, values] of candidates) {
masks[candidateCell] = mask(values);
}
return {
size,
values: new Array<number>(size * size).fill(0),
masks,
regions: empty.regions,
peers: compiled.peers,
units: compiled.units,
...options,
};
}
function expectElimination(
result: ReturnType<typeof findJellyfish>,
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] }],
});
});
});