feat: complete advanced Sudoku workbench

This commit is contained in:
2026-08-31 08:20:30 +02:00
parent 8ca9300ab3
commit 0a1bdc1a8c
99 changed files with 20793 additions and 923 deletions
@@ -2,6 +2,11 @@ import { render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { GeneratorWorkspace } from "../../src/components/GeneratorWorkspace";
import { classicRegions } from "../../src/domain";
import type {
GeneratedVariantBatch,
GeneratedVariantPuzzle,
} from "../../src/solver";
describe("Sudoku generator workspace", () => {
it("offers only reliable sizes and submits an explicit local recipe", async () => {
@@ -97,4 +102,158 @@ describe("Sudoku generator workspace", () => {
}),
).toBeInTheDocument();
});
it("submits mixed families, density, minimality and a full profile as a batch", async () => {
const user = userEvent.setup();
const onGenerateBatch = vi.fn();
render(
<GeneratorWorkspace
busy={false}
onGenerate={vi.fn()}
onGenerateBatch={onGenerateBatch}
onRate={vi.fn()}
/>,
);
await user.selectOptions(screen.getByLabelText("Variant"), "thermo");
await user.click(screen.getByRole("checkbox", { name: "Kropki dots" }));
await user.selectOptions(
screen.getByLabelText("Given symmetry"),
"horizontal",
);
await user.selectOptions(
screen.getByLabelText("Constraint density"),
"dense",
);
await user.click(
screen.getByRole("checkbox", { name: "Prove minimal givens" }),
);
await user.selectOptions(
screen.getByLabelText("Practice technique"),
"x-wing",
);
await user.clear(screen.getByLabelText("Minimum occurrences"));
await user.type(screen.getByLabelText("Minimum occurrences"), "2");
await user.type(screen.getByLabelText("Maximum occurrences"), "3");
await user.selectOptions(
screen.getByLabelText("Forbidden technique"),
"swordfish",
);
await user.selectOptions(
screen.getByLabelText("Hardest technique target"),
"x-wing",
);
await user.clear(screen.getByLabelText("Batch size"));
await user.type(screen.getByLabelText("Batch size"), "3");
await user.selectOptions(
screen.getByLabelText("Batch ranking"),
"fewest-givens",
);
await user.type(screen.getByLabelText("Seed"), "mixed-batch");
await user.click(
screen.getByRole("button", { name: "Generate 3-puzzle batch" }),
);
expect(onGenerateBatch).toHaveBeenCalledWith({
variant: "thermo",
variants: ["thermo", "kropki"],
size: 9,
targetDifficulty: "medium",
symmetry: "horizontal",
constraintCount: 8,
constraintDensity: "dense",
minimalGivens: true,
requiredTechnique: "x-wing",
techniqueProfile: {
forbidden: ["swordfish"],
counts: [{ technique: "x-wing", min: 2, max: 3 }],
hardestTechnique: "x-wing",
},
maxTechniqueAttempts: 10,
seed: "mixed-batch",
batchSize: 3,
ranking: "fewest-givens",
});
});
it("renders ranked evidence and lets the caller open a candidate", async () => {
const user = userEvent.setup();
const onSelectGenerated = vi.fn();
const solution = [1, 2, 3, 4, 3, 4, 1, 2, 4, 3, 2, 1, 2, 1, 4, 3];
const generation: GeneratedVariantPuzzle = {
puzzle: {
version: 1,
size: 4,
givens: solution,
solution,
regions: classicRegions(4),
constraints: [],
},
difficulty: {
score: 12,
level: "beginner",
label: "Beginner",
uniqueness: "unique",
clueCount: 16,
emptyCount: 0,
logicalStatus: "solved",
logicalSteps: 0,
techniqueCounts: {},
exactNodes: 1,
exactTruncated: false,
summary: "fixture",
},
variant: "classic",
families: ["classic"],
seed: "ranked:1",
generatedConstraintCount: 0,
generationAttempts: 1,
constraintDensity: "balanced",
minimality: {
status: "not-requested",
checksPerformed: 0,
nodes: 0,
removedClues: 0,
criticalCells: [],
unknownCells: [],
limitReasons: [],
symmetryPreserved: true,
},
};
const batch: GeneratedVariantBatch = {
entries: [generation],
summaries: [
{
rank: 1,
seed: generation.seed,
families: generation.families,
clueCount: 16,
constraintCount: 0,
score: 12,
level: "beginner",
minimalityStatus: "not-requested",
},
],
failures: [],
requested: 1,
completed: 1,
truncated: false,
ranking: "difficulty",
baseSeed: "ranked",
};
render(
<GeneratorWorkspace
busy={false}
batch={batch}
onGenerate={vi.fn()}
onSelectGenerated={onSelectGenerated}
onRate={vi.fn()}
/>,
);
expect(screen.getByText("1 of 1 generated")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Use" }));
expect(onSelectGenerated).toHaveBeenCalledWith(generation);
});
});