312 lines
7.8 KiB
TypeScript
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] }],
|
|
});
|
|
});
|
|
});
|