import { describe, expect, it } from "vitest"; import { classicRegions, type PuzzleDefinition } from "../../src/domain"; import type { LogicalStep } from "../../src/solver"; import { applyLogicalStepToSession, autoRemoveNotesAfterPlacements, fillLegalCenterCandidates, pruneInvalidNotes, } from "../../src/state/candidateMaintenance"; import { createSession, maskValues } from "../../src/state/session"; function puzzle4(): PuzzleDefinition { return { version: 1, size: 4, givens: new Array(16).fill(0), regions: classicRegions(4), constraints: [], }; } describe("candidate maintenance", () => { it("fills legal center candidates without changing corner notes or metadata", () => { const session = createSession(new Array(16).fill(0)); session.values[0] = 1; session.cornerMarks[1] = 0b1111; session.centerMarks[10] = 0b0010; session.colors[3] = 4; session.elapsedSeconds = 73; session.paused = true; const result = fillLegalCenterCandidates(session, puzzle4()); expect(result).not.toBe(session); expect(result.centerMarks).not.toBe(session.centerMarks); expect(result.centerMarks[0]).toBe(0); expect(maskValues(result.centerMarks[1]!, 4)).toEqual([2, 3, 4]); expect(maskValues(result.centerMarks[10]!, 4)).toEqual([1, 2, 3, 4]); expect(result.cornerMarks[1]).toBe(0b1111); expect(result.colors[3]).toBe(4); expect(result.elapsedSeconds).toBe(73); expect(result.paused).toBe(true); expect(session.centerMarks[10]).toBe(0b0010); }); it("only removes illegal center and corner notes", () => { const session = createSession(new Array(16).fill(0)); session.values[0] = 1; session.centerMarks.fill(0b1111); session.cornerMarks.fill(0b1111 | (1 << 8)); const result = pruneInvalidNotes(session, puzzle4()); expect(result.centerMarks[0]).toBe(0); expect(result.cornerMarks[0]).toBe(0); expect(maskValues(result.centerMarks[1]!, 4)).toEqual([2, 3, 4]); expect(maskValues(result.cornerMarks[4]!, 4)).toEqual([2, 3, 4]); expect(maskValues(result.centerMarks[10]!, 4)).toEqual([1, 2, 3, 4]); expect(result.cornerMarks[10]).toBe(0b1111); expect(session.cornerMarks[1]).toBe(0b1111 | (1 << 8)); }); it("auto-removes notes after placements but does not invent notes on erase", () => { const previous = createSession(new Array(16).fill(0)); previous.centerMarks.fill(0b1111); previous.cornerMarks.fill(0b1111); const placed = createSession(new Array(16).fill(0)); placed.centerMarks.fill(0b1111); placed.cornerMarks.fill(0b1111); placed.values[0] = 1; const maintained = autoRemoveNotesAfterPlacements( previous, placed, puzzle4(), ); expect(maskValues(maintained.centerMarks[1]!, 4)).toEqual([2, 3, 4]); expect(maskValues(maintained.cornerMarks[4]!, 4)).toEqual([2, 3, 4]); expect(maintained.centerMarks[10]).toBe(0b1111); const erased = createSession(new Array(16).fill(0)); erased.centerMarks[1] = 0b0001; const previousFilled = createSession(new Array(16).fill(0)); previousFilled.values[0] = 1; const afterErase = autoRemoveNotesAfterPlacements( previousFilled, erased, puzzle4(), ); expect(afterErase.centerMarks[1]).toBe(0b0001); expect(afterErase).not.toBe(erased); }); it("applies a logical step to tracked candidates as one immutable transition", () => { const base = createSession(new Array(16).fill(0)); base.cornerMarks.fill(0b1111); base.colors[10] = 3; base.elapsedSeconds = 99; const session = fillLegalCenterCandidates(base, puzzle4()); const step: LogicalStep = { technique: "naked-single", placements: [{ cell: 0, value: 1 }], eliminations: [{ cell: 10, values: [2] }], focusCells: [0, 10], explanation: "Test step", }; const result = applyLogicalStepToSession(session, step, puzzle4()); expect(result.values[0]).toBe(1); expect(result.centerMarks[0]).toBe(0); expect(result.cornerMarks[0]).toBe(0); expect(maskValues(result.centerMarks[1]!, 4)).toEqual([2, 3, 4]); expect(maskValues(result.centerMarks[10]!, 4)).toEqual([1, 3, 4]); expect(result.cornerMarks[1]).toBe(0b1111); expect(result.colors[10]).toBe(3); expect(result.elapsedSeconds).toBe(99); expect(session.values[0]).toBe(0); expect(session.centerMarks[10]).toBe(0b1111); }); it("leaves absent tracked candidates absent and rejects stale steps", () => { const session = createSession(new Array(16).fill(0)); const elimination: LogicalStep = { technique: "pointing", placements: [], eliminations: [{ cell: 1, values: [1] }], focusCells: [1], explanation: "Test elimination", }; expect( applyLogicalStepToSession(session, elimination, puzzle4()).centerMarks[1], ).toBe(0); session.values[0] = 2; const placement: LogicalStep = { ...elimination, placements: [{ cell: 0, value: 1 }], eliminations: [], }; expect(() => applyLogicalStepToSession(session, placement, puzzle4()), ).toThrow(/filled cell/u); }); });