feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classicRegions, type PuzzleDefinition } from "../../src/domain";
|
||||
import {
|
||||
PuzzleValidationError,
|
||||
classicRegions,
|
||||
type PuzzleDefinition,
|
||||
} from "../../src/domain";
|
||||
import { killerDigitCombinations, solveLogically } from "../../src/solver";
|
||||
|
||||
const easy: PuzzleDefinition = {
|
||||
@@ -15,6 +19,14 @@ const easy: PuzzleDefinition = {
|
||||
constraints: [],
|
||||
};
|
||||
|
||||
const emptyFour: PuzzleDefinition = {
|
||||
version: 1,
|
||||
size: 4,
|
||||
givens: Array<number>(16).fill(0),
|
||||
regions: classicRegions(4),
|
||||
constraints: [],
|
||||
};
|
||||
|
||||
describe("logical solver", () => {
|
||||
it("solves a standard puzzle and emits inspectable steps", () => {
|
||||
const result = solveLogically(easy);
|
||||
@@ -60,6 +72,103 @@ describe("logical solver", () => {
|
||||
expect(result.candidates[0]).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it("resumes after an elimination-only step from exposed candidates", () => {
|
||||
const candidates = Array.from({ length: 16 }, () => [1, 2, 3, 4]);
|
||||
candidates[0] = [1, 2];
|
||||
candidates[1] = [1, 2];
|
||||
candidates[4] = [1, 3];
|
||||
candidates[5] = [1, 3];
|
||||
|
||||
const first = solveLogically(emptyFour, {
|
||||
candidates,
|
||||
maxSteps: 1,
|
||||
});
|
||||
expect(first.steps[0]).toMatchObject({
|
||||
technique: "naked-pair",
|
||||
focusCells: [0, 1],
|
||||
placements: [],
|
||||
});
|
||||
expect(first.candidates[2]).toEqual([3, 4]);
|
||||
expect(first.candidates[3]).toEqual([3, 4]);
|
||||
|
||||
const next = solveLogically(emptyFour, {
|
||||
candidates: first.candidates,
|
||||
maxSteps: 1,
|
||||
});
|
||||
expect(next.steps[0]?.placements).toEqual([]);
|
||||
expect(next.steps[0]?.eliminations.length).toBeGreaterThan(0);
|
||||
expect(next.steps[0]?.eliminations).not.toEqual(
|
||||
first.steps[0]?.eliminations,
|
||||
);
|
||||
// The first elimination remains applied while the solver advances to a
|
||||
// different logical effect. This is the key resume contract for guided
|
||||
// hints that do not place a digit.
|
||||
expect(next.candidates[2]).toEqual([3, 4]);
|
||||
expect(next.candidates[3]).toEqual([3, 4]);
|
||||
});
|
||||
|
||||
it("intersects supplied restrictions with candidates legal on the board", () => {
|
||||
const candidates = Array.from({ length: 81 }, () =>
|
||||
Array.from({ length: 9 }, (_, index) => index + 1),
|
||||
);
|
||||
|
||||
const result = solveLogically(easy, { candidates, maxSteps: 1 });
|
||||
expect(result.candidates[2]).not.toContain(3);
|
||||
expect(result.candidates[2]).not.toContain(5);
|
||||
expect(result.candidates[2]).not.toContain(6);
|
||||
expect(result.candidates[2]).not.toContain(7);
|
||||
expect(result.candidates[2]).not.toContain(8);
|
||||
expect(result.candidates[2]).not.toContain(9);
|
||||
});
|
||||
|
||||
it("reports a valid empty restriction as an invalid logical state", () => {
|
||||
const candidates = Array.from({ length: 16 }, () => [1, 2, 3, 4]);
|
||||
candidates[0] = [];
|
||||
|
||||
const result = solveLogically(emptyFour, { candidates });
|
||||
expect(result.status).toBe("invalid");
|
||||
expect(result.steps).toEqual([]);
|
||||
expect(result.candidates[0]).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects malformed candidate restrictions with precise issues", () => {
|
||||
expect(() =>
|
||||
solveLogically(emptyFour, {
|
||||
candidates: Array.from({ length: 15 }, () => [1, 2, 3, 4]),
|
||||
}),
|
||||
).toThrow(PuzzleValidationError);
|
||||
|
||||
try {
|
||||
solveLogically(emptyFour, {
|
||||
candidates: [
|
||||
[1, 1],
|
||||
[0, 5],
|
||||
...Array.from({ length: 14 }, () => [1, 2, 3, 4]),
|
||||
],
|
||||
});
|
||||
throw new Error("Expected malformed candidates to be rejected");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(PuzzleValidationError);
|
||||
expect((error as PuzzleValidationError).issues).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: "candidates[0][1]" }),
|
||||
expect.objectContaining({ path: "candidates[1][0]" }),
|
||||
expect.objectContaining({ path: "candidates[1][1]" }),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
expect(() =>
|
||||
solveLogically(emptyFour, {
|
||||
candidates: [
|
||||
[1, 2, 3, 4],
|
||||
null,
|
||||
...Array.from({ length: 14 }, () => [1, 2, 3, 4]),
|
||||
] as unknown as readonly (readonly number[])[],
|
||||
}),
|
||||
).toThrow(PuzzleValidationError);
|
||||
});
|
||||
|
||||
it("returns bounded killer combinations", () => {
|
||||
expect(
|
||||
killerDigitCombinations({ size: 9, count: 2, sum: 10 }).combinations,
|
||||
|
||||
Reference in New Issue
Block a user