146 lines
5.1 KiB
TypeScript
146 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|