feat: launch local-first Sudoku workbench
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
export interface KillerCombinationOptions {
|
||||
readonly size: number;
|
||||
readonly count: number;
|
||||
readonly sum: number;
|
||||
readonly noRepeat?: boolean;
|
||||
readonly usedDigits?: readonly number[];
|
||||
readonly maxResults?: number;
|
||||
}
|
||||
|
||||
export interface KillerCombinationResult {
|
||||
readonly combinations: readonly (readonly number[])[];
|
||||
readonly truncated: boolean;
|
||||
}
|
||||
|
||||
export function killerDigitCombinations(
|
||||
options: KillerCombinationOptions,
|
||||
): KillerCombinationResult {
|
||||
const { size, count, sum } = options;
|
||||
if (!Number.isInteger(size) || size < 4 || size > 16) {
|
||||
throw new RangeError("size must be an integer from 4 to 16");
|
||||
}
|
||||
if (!Number.isInteger(count) || count < 1 || count > size) {
|
||||
throw new RangeError("count must be an integer from 1 to size");
|
||||
}
|
||||
if (!Number.isInteger(sum) || sum < count || sum > count * size) {
|
||||
throw new RangeError("sum is outside the possible range");
|
||||
}
|
||||
const maxResults = options.maxResults ?? 10_000;
|
||||
if (!Number.isInteger(maxResults) || maxResults < 1 || maxResults > 100_000) {
|
||||
throw new RangeError("maxResults must be an integer from 1 to 100000");
|
||||
}
|
||||
const noRepeat = options.noRepeat !== false;
|
||||
const usedDigits = new Set(options.usedDigits ?? []);
|
||||
for (const digit of usedDigits) {
|
||||
if (!Number.isInteger(digit) || digit < 1 || digit > size) {
|
||||
throw new RangeError("usedDigits contains an out-of-range digit");
|
||||
}
|
||||
}
|
||||
const combinations: number[][] = [];
|
||||
let truncated = false;
|
||||
const current: number[] = [];
|
||||
|
||||
const visit = (
|
||||
remainingCount: number,
|
||||
remainingSum: number,
|
||||
minimum: number,
|
||||
): void => {
|
||||
if (combinations.length >= maxResults) {
|
||||
truncated = true;
|
||||
return;
|
||||
}
|
||||
if (remainingCount === 0) {
|
||||
if (remainingSum === 0) combinations.push([...current]);
|
||||
return;
|
||||
}
|
||||
const lowestPossible = noRepeat
|
||||
? (remainingCount * (2 * minimum + remainingCount - 1)) / 2
|
||||
: remainingCount * minimum;
|
||||
const highestPossible = noRepeat
|
||||
? (remainingCount * (2 * size - remainingCount + 1)) / 2
|
||||
: remainingCount * size;
|
||||
if (remainingSum < lowestPossible || remainingSum > highestPossible) return;
|
||||
for (let digit = minimum; digit <= size; digit += 1) {
|
||||
if (usedDigits.has(digit)) continue;
|
||||
if (digit > remainingSum) break;
|
||||
current.push(digit);
|
||||
visit(
|
||||
remainingCount - 1,
|
||||
remainingSum - digit,
|
||||
noRepeat ? digit + 1 : digit,
|
||||
);
|
||||
current.pop();
|
||||
if (truncated) return;
|
||||
}
|
||||
};
|
||||
|
||||
visit(count, sum, 1);
|
||||
return { combinations, truncated };
|
||||
}
|
||||
Reference in New Issue
Block a user