starting new structure
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export * from './sudoku';
|
||||
export * from './validate-board';
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Create a playable Sudoku puzzle by removing random digits
|
||||
* from a fully solved board string.
|
||||
*
|
||||
* Difficulty only determines how many cells are removed.
|
||||
* - "easy": removes ~40 cells
|
||||
* - "medium": removes ~50 cells
|
||||
* - "hard": removes ~60 cells
|
||||
*
|
||||
* The returned string uses '.' for blanks.
|
||||
*/
|
||||
export function makePuzzle(
|
||||
solution: string,
|
||||
difficulty: "easy" | "medium" | "hard" = "medium"
|
||||
): string {
|
||||
if (solution.length !== 81 || !/^[1-9]+$/.test(solution)) {
|
||||
throw new Error("Invalid solved board: must be 81 digits 1–9");
|
||||
}
|
||||
|
||||
// --- removal count based on difficulty ---
|
||||
const removeCount = {
|
||||
easy: 40,
|
||||
medium: 50,
|
||||
hard: 60,
|
||||
}[difficulty];
|
||||
|
||||
// --- create a mutable copy ---
|
||||
const board = solution.split("");
|
||||
|
||||
// --- build a shuffled list of indices 0–80 ---
|
||||
const indices = Array.from({ length: 81 }, (_, i) => i);
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
|
||||
// --- remove the desired number of cells ---
|
||||
for (let i = 0; i < removeCount; i++) {
|
||||
const idx = indices[i];
|
||||
board[idx] = ".";
|
||||
}
|
||||
|
||||
return board.join("");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { generateSolvedBoard } from "./sudoku";
|
||||
import { assertValidFullBoard } from "./validate-board";
|
||||
|
||||
|
||||
const valids = [
|
||||
"534678912672195348198342567859761423426853791713924856961537284287419635345286179",
|
||||
"456789123123456789789123456567891234234567891891234567678912345345678912912345678",
|
||||
"234567891567891234891234567345678912678912345912345678456789123789123456123456789",
|
||||
"987654321654321987321987654876543219543219876219876543765432198432198765198765432"
|
||||
];
|
||||
|
||||
const invalids = [
|
||||
"534678912672195348198342567859761423426853791713924856961537284287419635345286171",
|
||||
"534678911672195348198342567859761423426853791713924856961537284287419635345286179",
|
||||
"534678912572195348198342567859761423426853791713924856961537284287419635345286179",
|
||||
"534678912672195348198342567859761423426863791713924856961537284287419635345286179",
|
||||
];
|
||||
|
||||
describe("Testing the Asserter with static boards", () => {
|
||||
test("valid boards", () => {
|
||||
for (let board of valids) {
|
||||
expect(() => assertValidFullBoard(board)).not.toThrow();
|
||||
}
|
||||
})
|
||||
|
||||
test("invalid boards", () => {
|
||||
for (let board of invalids) {
|
||||
expect(() => assertValidFullBoard(board)).toThrow()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
describe("Testing the generateSolvedBoard", () => {
|
||||
test("generates 10 valid boards", () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const generated = generateSolvedBoard();
|
||||
assertValidFullBoard(generated);
|
||||
};
|
||||
});
|
||||
test("generates 100 valid boards", () => {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const generated = generateSolvedBoard();
|
||||
assertValidFullBoard(generated);
|
||||
};
|
||||
});
|
||||
|
||||
test("generates 1000 valid boards", () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const generated = generateSolvedBoard();
|
||||
assertValidFullBoard(generated);
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Generate a fully valid Sudoku solution using a base pattern and structural shuffles.
|
||||
* Returns a string of 81 digits (row-major order).
|
||||
*/
|
||||
export function generateSolvedBoard(): string {
|
||||
const size = 9;
|
||||
const box = 3;
|
||||
|
||||
// --- 1️⃣ Base pattern function ---
|
||||
const pattern = (r: number, c: number) =>
|
||||
(box * (r % box) + Math.floor(r / box) + c) % size;
|
||||
|
||||
// --- 2️⃣ Create base board ---
|
||||
const baseBoard: number[][] = Array.from({ length: size }, (_, r) =>
|
||||
Array.from({ length: size }, (_, c) => pattern(r, c) + 1)
|
||||
);
|
||||
|
||||
// --- 3️⃣ Helper to shuffle an array in place ---
|
||||
const shuffle = <T>(arr: T[]): T[] => {
|
||||
const copy = [...arr];
|
||||
for (let i = copy.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[copy[i], copy[j]] = [copy[j], copy[i]];
|
||||
}
|
||||
return copy;
|
||||
};
|
||||
|
||||
// --- 4️⃣ Build shuffled row/col indices preserving 3×3 structure ---
|
||||
const rows = shuffle([0, 1, 2])
|
||||
.concat(shuffle([3, 4, 5]))
|
||||
.concat(shuffle([6, 7, 8]));
|
||||
const cols = shuffle([0, 1, 2])
|
||||
.concat(shuffle([3, 4, 5]))
|
||||
.concat(shuffle([6, 7, 8]));
|
||||
|
||||
// --- 5️⃣ Shuffle entire bands and stacks (groups of 3) ---
|
||||
const rowBands = shuffle([0, 1, 2]);
|
||||
const colStacks = shuffle([0, 1, 2]);
|
||||
|
||||
const shuffledRows = rowBands.flatMap(band => rows.slice(band * 3, band * 3 + 3));
|
||||
const shuffledCols = colStacks.flatMap(stack => cols.slice(stack * 3, stack * 3 + 3));
|
||||
|
||||
// --- 6️⃣ Digit permutation (1–9) ---
|
||||
const digits = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
|
||||
// --- 7️⃣ Build the final shuffled board ---
|
||||
const board: number[][] = shuffledRows.map(r =>
|
||||
shuffledCols.map(c => digits[baseBoard[r][c] - 1])
|
||||
);
|
||||
|
||||
// --- 8️⃣ Flatten to 81-char string ---
|
||||
return board.flat().join('');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Validate a fully filled Sudoku board (81-char string)
|
||||
* Returns true if:
|
||||
* - 81 characters long
|
||||
* - all characters are digits 1–9
|
||||
* - all rows, columns, and 3×3 boxes sum to 45
|
||||
*/
|
||||
export function validateFullBoard(board: string): boolean {
|
||||
// --- Basic sanity ---
|
||||
if (board.length !== 81) return false;
|
||||
if (!/^[1-9]+$/.test(board)) return false;
|
||||
|
||||
const getCell = (r: number, c: number) => Number(board[r * 9 + c]);
|
||||
|
||||
// --- Validate rows ---
|
||||
for (let r = 0; r < 9; r++) {
|
||||
const sum = Array.from({ length: 9 }, (_, c) => getCell(r, c))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
if (sum !== 45) return false;
|
||||
}
|
||||
|
||||
// --- Validate columns ---
|
||||
for (let c = 0; c < 9; c++) {
|
||||
const sum = Array.from({ length: 9 }, (_, r) => getCell(r, c))
|
||||
.reduce((a, b) => a + b, 0);
|
||||
if (sum !== 45) return false;
|
||||
}
|
||||
|
||||
// --- Validate 3×3 boxes ---
|
||||
for (let br = 0; br < 3; br++) {
|
||||
for (let bc = 0; bc < 3; bc++) {
|
||||
const sum = Array.from({ length: 9 }, (_, i) => {
|
||||
const r = br * 3 + Math.floor(i / 3);
|
||||
const c = bc * 3 + (i % 3);
|
||||
return getCell(r, c);
|
||||
}).reduce((a, b) => a + b, 0);
|
||||
if (sum !== 45) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* 🔍 Assert that a Sudoku board string represents a valid, fully filled grid.
|
||||
*
|
||||
* Format:
|
||||
* - 81 characters
|
||||
* - Only digits 1–9
|
||||
* - Each row, column, and 3×3 box contains unique digits that sum to 45
|
||||
*
|
||||
* Throws descriptive errors on failure.
|
||||
*/
|
||||
|
||||
export function assertValidFullBoard(board: string): void {
|
||||
// --- Basic integrity checks ---
|
||||
console.assert(board.length === 81, `❌ Board length must be 81, got ${board.length}`);
|
||||
if (board.length !== 81) throw new Error(`Invalid board length: ${board.length}`);
|
||||
|
||||
// All characters must be digits 1–9
|
||||
console.assert(/^[1-9]+$/.test(board), "❌ Board must contain only digits 1–9 (no 0s or dots)");
|
||||
if (!/^[1-9]+$/.test(board)) throw new Error("Board contains invalid characters");
|
||||
|
||||
const get = (r: number, c: number) => Number(board[r * 9 + c]);
|
||||
|
||||
// --- Helper to assert uniqueness and sum ---
|
||||
const assertGroup = (label: string, cells: number[]) => {
|
||||
const sum = cells.reduce((a, b) => a + b, 0);
|
||||
const unique = new Set(cells);
|
||||
console.assert(sum === 45, `❌ ${label} sum=${sum} (should be 45)`);
|
||||
console.assert(unique.size === 9, `❌ ${label} has duplicates: [${cells.join(",")}]`);
|
||||
|
||||
if (sum !== 45) throw new Error(`${label} does not sum to 45`);
|
||||
if (unique.size !== 9) throw new Error(`${label} contains duplicates`);
|
||||
};
|
||||
|
||||
// --- Validate rows ---
|
||||
for (let r = 0; r < 9; r++) {
|
||||
const row = Array.from({ length: 9 }, (_, c) => get(r, c));
|
||||
assertGroup(`Row ${r + 1}`, row);
|
||||
}
|
||||
|
||||
// --- Validate columns ---
|
||||
for (let c = 0; c < 9; c++) {
|
||||
const col = Array.from({ length: 9 }, (_, r) => get(r, c));
|
||||
assertGroup(`Column ${c + 1}`, col);
|
||||
}
|
||||
|
||||
// --- Validate 3×3 boxes ---
|
||||
for (let br = 0; br < 3; br++) {
|
||||
for (let bc = 0; bc < 3; bc++) {
|
||||
const box: number[] = [];
|
||||
for (let r = 0; r < 3; r++) {
|
||||
for (let c = 0; c < 3; c++) {
|
||||
box.push(get(br * 3 + r, bc * 3 + c));
|
||||
}
|
||||
}
|
||||
const idx = br * 3 + bc + 1;
|
||||
assertGroup(`Box ${idx}`, box);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user