Files
sudoku-tools/src/components/GeneratorWorkspace.tsx
T

244 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState, type FormEvent } from "react";
import {
GENERATOR_VARIANTS,
type DifficultyAssessment,
type GeneratedVariantPuzzle,
type GenerationDifficultyTarget,
type GeneratorVariant,
type GenerateVariantOptions,
} from "../solver";
const difficultyTargets: readonly GenerationDifficultyTarget[] = [
"beginner",
"easy",
"medium",
"hard",
"expert",
];
function techniqueLabel(value: string | undefined): string {
return value === undefined
? "Direct placements"
: value
.split("-")
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
.join(" ");
}
export function GeneratorWorkspace({
busy,
assessment,
generation,
onGenerate,
onRate,
}: {
busy: boolean;
assessment?: DifficultyAssessment;
generation?: GeneratedVariantPuzzle;
onGenerate: (options: GenerateVariantOptions) => void;
onRate: () => void;
}) {
const [variant, setVariant] = useState<GeneratorVariant>("classic");
const definition = useMemo(
() => GENERATOR_VARIANTS.find((item) => item.id === variant)!,
[variant],
);
const [size, setSize] = useState(9);
const [targetDifficulty, setTargetDifficulty] =
useState<GenerationDifficultyTarget>("medium");
const [symmetry, setSymmetry] = useState<"none" | "rotational">("rotational");
const [constraintCount, setConstraintCount] = useState(8);
const [seed, setSeed] = useState("");
const usesMarkingCount = ![
"classic",
"diagonal",
"killer",
"anti-knight",
"anti-king",
"non-consecutive",
].includes(variant);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
onGenerate({
variant,
size,
targetDifficulty,
symmetry,
...(usesMarkingCount ? { constraintCount } : {}),
seed: seed.trim() || `local-${Date.now().toString(36)}`,
});
};
return (
<div className="generator-workspace stack">
<div>
<p className="eyebrow">Bounded local construction</p>
<h2>Sudoku generator</h2>
<p className="muted">
Build a seedable, uniquely checked puzzle in the worker. The requested
level guides clue removal; the finished puzzle receives an independent
evidence-based rating.
</p>
</div>
<form className="panel-section generator-form" onSubmit={submit}>
<div className="field-grid">
<label>
Variant
<select
value={variant}
onChange={(event) => {
const next = event.target.value as GeneratorVariant;
const nextDefinition = GENERATOR_VARIANTS.find(
(item) => item.id === next,
)!;
setVariant(next);
if (
!(
nextDefinition.supportedSizes as readonly number[]
).includes(size)
) {
setSize(nextDefinition.supportedSizes[0]);
}
}}
>
{GENERATOR_VARIANTS.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
</select>
</label>
<label>
Grid
<select
value={size}
onChange={(event) => setSize(Number(event.target.value))}
>
{definition.supportedSizes.map((supportedSize) => (
<option key={supportedSize} value={supportedSize}>
{supportedSize} × {supportedSize}
</option>
))}
</select>
</label>
<label>
Requested profile
<select
value={targetDifficulty}
onChange={(event) =>
setTargetDifficulty(
event.target.value as GenerationDifficultyTarget,
)
}
>
{difficultyTargets.map((target) => (
<option key={target} value={target}>
{target[0]!.toUpperCase() + target.slice(1)}
</option>
))}
</select>
</label>
<label>
Given symmetry
<select
value={symmetry}
onChange={(event) =>
setSymmetry(event.target.value as "none" | "rotational")
}
>
<option value="rotational">Rotational</option>
<option value="none">None</option>
</select>
</label>
{usesMarkingCount && (
<label>
Requested markings
<input
type="number"
min="1"
max={size * size * 2}
value={constraintCount}
onChange={(event) =>
setConstraintCount(Number(event.target.value))
}
/>
</label>
)}
<label>
Seed
<input
value={seed}
maxLength={128}
placeholder="blank = fresh local seed"
onChange={(event) => setSeed(event.target.value)}
/>
</label>
</div>
<p className="generator-description">{definition.description}</p>
<div className="action-row">
<button className="primary-button" type="submit" disabled={busy}>
{busy ? "Working locally…" : `Generate ${definition.label}`}
</button>
<button type="button" disabled={busy} onClick={onRate}>
Rate current puzzle
</button>
</div>
<p className="muted">
Difficulty is an estimate from reproducible solver evidence, not a
universal promise. Uniqueness is never claimed after a safety limit.
</p>
</form>
{assessment && (
<section
className="analysis-summary difficulty-card"
aria-live="polite"
>
<div className="section-heading">
<div>
<p className="eyebrow">Difficulty assessment</p>
<h3>{assessment.label}</h3>
</div>
<span
className={`status-pill${assessment.uniqueness === "unique" ? " status-solved" : " status-invalid"}`}
>
{assessment.uniqueness}
</span>
</div>
<div className="metric-row">
<span>
Score <strong>{assessment.score ?? "—"}/100</strong>
</span>
<span>
Givens <strong>{assessment.clueCount}</strong>
</span>
<span>
Logical steps <strong>{assessment.logicalSteps}</strong>
</span>
<span>
Hardest{" "}
<strong>{techniqueLabel(assessment.hardestTechnique)}</strong>
</span>
<span>
Search nodes <strong>{assessment.exactNodes}</strong>
</span>
{generation && (
<span>
Markings <strong>{generation.generatedConstraintCount}</strong>
</span>
)}
</div>
<p>{assessment.summary}</p>
{generation && (
<p className="muted">
Seed: <code>{String(generation.seed)}</code>
</p>
)}
</section>
)}
</div>
);
}