feat: launch local-first Sudoku workbench
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import fc from "fast-check";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
analyzeKillerCage,
|
||||
calculateKillerCombinations,
|
||||
} from "../../src/helpers";
|
||||
|
||||
describe("killer cage combinations", () => {
|
||||
it("enumerates the familiar two-cell sum 10 combinations", () => {
|
||||
expect(calculateKillerCombinations({ cellCount: 2, sum: 10 })).toEqual([
|
||||
[1, 9],
|
||||
[2, 8],
|
||||
[3, 7],
|
||||
[4, 6],
|
||||
]);
|
||||
});
|
||||
|
||||
it("supports allowed, required, excluded and repeated digits", () => {
|
||||
expect(
|
||||
calculateKillerCombinations({
|
||||
cellCount: 3,
|
||||
sum: 10,
|
||||
allowedDigits: [1, 2, 3, 4, 5],
|
||||
excludedDigits: [1],
|
||||
requiredDigits: [5],
|
||||
}),
|
||||
).toEqual([[2, 3, 5]]);
|
||||
expect(
|
||||
calculateKillerCombinations({
|
||||
cellCount: 2,
|
||||
sum: 10,
|
||||
allowRepeats: true,
|
||||
}),
|
||||
).toContainEqual([5, 5]);
|
||||
});
|
||||
|
||||
it("filters assignments through per-cell candidates", () => {
|
||||
const result = analyzeKillerCage({
|
||||
cellCount: 2,
|
||||
sum: 10,
|
||||
candidates: [
|
||||
[1, 2],
|
||||
[8, 9],
|
||||
],
|
||||
});
|
||||
expect(result.combinations).toEqual([
|
||||
[1, 9],
|
||||
[2, 8],
|
||||
]);
|
||||
expect(result.assignments).toEqual([
|
||||
[1, 9],
|
||||
[2, 8],
|
||||
]);
|
||||
expect(result.possibleByCell).toEqual([
|
||||
[1, 2],
|
||||
[8, 9],
|
||||
]);
|
||||
expect(result.necessaryDigits).toEqual([]);
|
||||
});
|
||||
|
||||
it("reports digits necessary in every surviving combination", () => {
|
||||
const result = analyzeKillerCage({ cellCount: 3, sum: 6 });
|
||||
expect(result.combinations).toEqual([[1, 2, 3]]);
|
||||
expect(result.necessaryDigits).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("satisfies combination invariants for generated inputs", () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.integer({ min: 4, max: 12 }),
|
||||
fc.integer({ min: 1, max: 5 }),
|
||||
fc.integer({ min: 1, max: 60 }),
|
||||
(size, count, sum) => {
|
||||
if (count > size || sum > size * count) return;
|
||||
const result = calculateKillerCombinations({
|
||||
size,
|
||||
cellCount: count,
|
||||
sum,
|
||||
});
|
||||
const keys = new Set<string>();
|
||||
for (const combination of result) {
|
||||
expect(combination).toHaveLength(count);
|
||||
expect(combination.reduce((total, digit) => total + digit, 0)).toBe(
|
||||
sum,
|
||||
);
|
||||
expect(combination).toEqual([...combination].sort((a, b) => a - b));
|
||||
expect(new Set(combination).size).toBe(count);
|
||||
keys.add(combination.join(","));
|
||||
}
|
||||
expect(keys.size).toBe(result.length);
|
||||
},
|
||||
),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
calculateResidual,
|
||||
fortyFiveRuleResidual,
|
||||
inequalityPairs,
|
||||
kropkiPairs,
|
||||
relationPairs,
|
||||
xvPairs,
|
||||
} from "../../src/helpers";
|
||||
|
||||
describe("relation pair tables", () => {
|
||||
it("builds white and black Kropki tables", () => {
|
||||
expect(kropkiPairs("white", 4)).toEqual([
|
||||
[1, 2],
|
||||
[2, 1],
|
||||
[2, 3],
|
||||
[3, 2],
|
||||
[3, 4],
|
||||
[4, 3],
|
||||
]);
|
||||
expect(kropkiPairs("black", 4)).toEqual([
|
||||
[1, 2],
|
||||
[2, 1],
|
||||
[2, 4],
|
||||
[4, 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds XV and directed inequality tables with candidate filtering", () => {
|
||||
expect(xvPairs(5, 4)).toEqual([
|
||||
[1, 4],
|
||||
[2, 3],
|
||||
[3, 2],
|
||||
[4, 1],
|
||||
]);
|
||||
expect(inequalityPairs("<", 4, [2, 4], [1, 3])).toEqual([[2, 3]]);
|
||||
expect(
|
||||
relationPairs({ type: "difference", difference: 2 }, 4),
|
||||
).toContainEqual([1, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("45-rule helpers", () => {
|
||||
it("calculates a residual and its combinations", () => {
|
||||
const result = calculateResidual({ knownSums: [10, 20], unknownCount: 2 });
|
||||
expect(result.residual).toBe(15);
|
||||
expect(result.analysis?.combinations).toEqual([
|
||||
[6, 9],
|
||||
[7, 8],
|
||||
]);
|
||||
});
|
||||
|
||||
it("subtracts complete cages and values but reports crossing cages", () => {
|
||||
const result = fortyFiveRuleResidual({
|
||||
unitCells: [0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
cages: [
|
||||
{ cells: [0, 1], sum: 3 },
|
||||
{ cells: [8, 17], sum: 10 },
|
||||
],
|
||||
knownValues: { 2: 4 },
|
||||
});
|
||||
expect(result.accounted).toBe(7);
|
||||
expect(result.residual).toBe(38);
|
||||
expect(result.residualCells).toEqual([3, 4, 5, 6, 7, 8]);
|
||||
expect(result.crossingCages).toEqual([{ cells: [8, 17], sum: 10 }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user