feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
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] }],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user