feat: complete advanced Sudoku workbench
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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<number>(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<number>(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<number>(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<number>(16).fill(0));
|
||||
previous.centerMarks.fill(0b1111);
|
||||
previous.cornerMarks.fill(0b1111);
|
||||
const placed = createSession(new Array<number>(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<number>(16).fill(0));
|
||||
erased.centerMarks[1] = 0b0001;
|
||||
const previousFilled = createSession(new Array<number>(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<number>(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<number>(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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user