import fc from "fast-check"; import { describe, expect, it } from "vitest"; import { analyzeSumLab, sumCombinationKey } from "../../src/helpers/sumLab"; function mask(...digits: number[]): number { return digits.reduce((value, digit) => value | (2 ** (digit - 1)), 0); } describe("generalized sum lab", () => { it("enumerates deterministic, sorted combinations", () => { const result = analyzeSumLab({ cellCount: 2, target: 10 }); expect(result.combinations.map(({ digits }) => digits)).toEqual([ [1, 9], [2, 8], [3, 7], [4, 6], ]); expect(result.truncated).toBe(false); }); it("supports a digit range, repeats, required and excluded digits", () => { const result = analyzeSumLab({ cellCount: 3, target: 15, minimumDigit: 3, maximumDigit: 7, allowRepeats: true, requiredDigits: [3], excludedDigits: [4], }); expect(result.combinations.map(({ digits }) => digits)).toEqual([ [3, 5, 7], [3, 6, 6], ]); }); it("uses per-cell candidate masks and derives positional possibilities", () => { const result = analyzeSumLab({ cellCount: 2, target: 10, candidateMasks: [mask(1, 2), mask(8, 9)], }); expect(result.combinations.map(({ digits }) => digits)).toEqual([ [1, 9], [2, 8], ]); expect(result.assignments).toEqual([ [1, 9], [2, 8], ]); expect(result.possibleByCell).toEqual([ [1, 2], [8, 9], ]); }); it("removes toggled combinations from every live deduction", () => { const eliminatedKeys = new Set([sumCombinationKey([1, 9])]); const result = analyzeSumLab({ cellCount: 2, target: 10, eliminatedKeys, }); expect(result.combinations).toHaveLength(4); expect(result.eliminatedCombinations.map(({ digits }) => digits)).toEqual([ [1, 9], ]); expect(result.activeCombinations.map(({ digits }) => digits)).toEqual([ [2, 8], [3, 7], [4, 6], ]); expect(result.possibleDigits).not.toContain(1); expect(result.possibleDigits).not.toContain(9); }); it("reports which independent safety bound stopped an analysis", () => { const result = analyzeSumLab({ cellCount: 8, target: 36, allowRepeats: true, maxSearchNodes: 1, }); expect(result.truncated).toBe(true); expect(result.truncationReason).toBe("search"); expect(result.exploredNodes).toBe(2); }); it("validates positional candidates and contradictory digit filters", () => { expect(() => analyzeSumLab({ cellCount: 2, target: 3, candidateMasks: [mask(1)] }), ).toThrow(/exactly 2/u); expect( analyzeSumLab({ cellCount: 2, target: 3, requiredDigits: [1], excludedDigits: [1], }).combinations, ).toEqual([]); }); it("preserves combination invariants over generated inputs", () => { fc.assert( fc.property( fc.integer({ min: 3, max: 12 }), fc.integer({ min: 1, max: 6 }), fc.integer({ min: 0, max: 72 }), fc.boolean(), (maximumDigit, cellCount, target, allowRepeats) => { if (target > maximumDigit * cellCount) return; const result = analyzeSumLab({ cellCount, target, maximumDigit, allowRepeats, }); const keys = new Set(); for (const combination of result.combinations) { expect(combination.digits).toHaveLength(cellCount); expect( combination.digits.reduce((total, digit) => total + digit, 0), ).toBe(target); expect(combination.digits).toEqual( [...combination.digits].sort((left, right) => left - right), ); if (!allowRepeats) { expect(new Set(combination.digits)).toHaveProperty( "size", cellCount, ); } keys.add(combination.key); } expect(keys.size).toBe(result.combinations.length); }, ), { numRuns: 100 }, ); }); });