feat: add gameplay assists and variant generator

This commit is contained in:
2026-08-30 17:47:10 +02:00
parent 659640b231
commit 4a9869baa0
29 changed files with 2899 additions and 110 deletions
+64 -15
View File
@@ -1,5 +1,10 @@
import { useState } from "react";
import type { PuzzleDefinition, VariantConstraint } from "../domain/types";
import {
removeKillerCagesAtCells,
replaceOverlappingKillerCages,
selectionTouchesKillerCage,
} from "./constraintEditing";
interface ConstraintEditorProps {
puzzle: PuzzleDefinition;
@@ -34,7 +39,7 @@ function describeConstraint(constraint: VariantConstraint, size: number) {
case "kropki":
return `${constraint.kind} dot · ${cell(constraint.a)}${cell(constraint.b)}`;
case "xv":
return `${constraint.total === 5 ? "V" : "X"} · ${cell(constraint.a)}${cell(constraint.b)}`;
return `sum ${String(constraint.total)} pair · ${cell(constraint.a)}${cell(constraint.b)}`;
case "inequality":
return `${cell(constraint.lesser)} < ${cell(constraint.greater)}`;
}
@@ -57,6 +62,17 @@ export function ConstraintEditor({
onChange({ ...puzzle, constraints: [...constraints, constraint] });
const need = (count: number) => selection.length === count;
const atLeast = (count: number) => selection.length >= count;
const cageCellCount = Math.max(1, selection.length);
const minimumCageSum = (cageCellCount * (cageCellCount + 1)) / 2;
const maximumCageSum =
(cageCellCount * (2 * puzzle.size - cageCellCount + 1)) / 2;
const validCage =
selection.length >= 1 &&
selection.length <= puzzle.size &&
Number.isInteger(cageSum) &&
cageSum >= minimumCageSum &&
cageSum <= maximumCageSum;
const selectedCageExists = selectionTouchesKillerCage(constraints, selection);
const toggleGlobal = (
type: "anti-knight" | "anti-king" | "non-consecutive",
@@ -143,22 +159,46 @@ export function ConstraintEditor({
Cage sum
<input
type="number"
min="1"
max={puzzle.size * puzzle.size}
min={minimumCageSum}
max={maximumCageSum}
value={cageSum}
onChange={(event) => setCageSum(Number(event.target.value))}
/>
</label>
<button
type="button"
disabled={!atLeast(1) || !Number.isInteger(cageSum)}
disabled={!validCage}
onClick={() =>
append({ type: "killer-cage", cells: selection, sum: cageSum })
onChange({
...puzzle,
constraints: replaceOverlappingKillerCages(constraints, {
type: "killer-cage",
cells: selection,
sum: cageSum,
}),
})
}
>
Add cage
Add / replace cage
</button>
<button
type="button"
className="danger"
disabled={!selectedCageExists}
onClick={() =>
onChange({
...puzzle,
constraints: removeKillerCagesAtCells(constraints, selection),
})
}
>
Remove selected cage
</button>
</div>
<p className="muted">
Adding replaces any cage touching the selection. Removing clears every
cage touching a selected cell.
</p>
<div className="button-grid">
<button
type="button"
@@ -234,7 +274,7 @@ export function ConstraintEditor({
})
}
>
V pair
Sum 5 (XV)
</button>
<button
type="button"
@@ -248,7 +288,7 @@ export function ConstraintEditor({
})
}
>
X pair
Sum 10 (XV)
</button>
<button
type="button"
@@ -261,7 +301,20 @@ export function ConstraintEditor({
})
}
>
First &lt; second
1st &lt; 2nd (inequality)
</button>
<button
type="button"
disabled={!need(2)}
onClick={() =>
append({
type: "inequality",
lesser: selection[1]!,
greater: selection[0]!,
})
}
>
1st &gt; 2nd (inequality)
</button>
</div>
<div className="inline-fields">
@@ -374,12 +427,8 @@ export function ConstraintEditor({
<button type="button" onClick={onCheck} disabled={busy}>
Check definition &amp; uniqueness
</button>
<button
type="button"
onClick={onGenerate}
disabled={busy || puzzle.size > 9}
>
Generate classic
<button type="button" onClick={onGenerate} disabled={busy}>
Open generator
</button>
</section>
</div>