@amanwebdev/sudoku-generator
Version:
Sudoku generator
361 lines (360 loc) • 12 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateSudoku = generateSudoku;
exports.toPDF = toPDF;
exports.toImage = toImage;
const jspdf_1 = require("jspdf");
const canvas_1 = require("canvas");
function generateSudoku(size, difficulty) {
const grid = createEmptyGrid(size);
const solution = createEmptyGrid(size);
fillGrid(grid, size);
copyGrid(grid, solution);
removeNumbers(grid, difficulty, size);
return {
size,
grid,
solution,
difficulty,
};
}
function createEmptyGrid(size) {
return Array(size)
.fill(0)
.map(() => Array(size).fill(0));
}
function copyGrid(from, to) {
for (let i = 0; i < from.length; i++) {
for (let j = 0; j < from[i].length; j++) {
to[i][j] = from[i][j];
}
}
}
function fillGrid(grid, size) {
const emptyCell = findEmptyCell(grid, size);
if (!emptyCell) {
return true;
}
const [row, col] = emptyCell;
const numbers = shuffle(getPossibleNumbers(size));
for (const num of numbers) {
if (isValid(grid, row, col, num, size)) {
grid[row][col] = num;
if (fillGrid(grid, size)) {
return true;
}
grid[row][col] = 0;
}
}
return false;
}
function findEmptyCell(grid, size) {
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (grid[i][j] === 0) {
return [i, j];
}
}
}
return null;
}
function getPossibleNumbers(size) {
const numbers = [];
for (let i = 1; i <= size; i++) {
numbers.push(i);
}
return numbers;
}
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function isValid(grid, row, col, num, size) {
return (!isInRow(grid, row, num, size) &&
!isInCol(grid, col, num, size) &&
!isInBox(grid, row, col, num, size));
}
function isInRow(grid, row, num, size) {
for (let i = 0; i < size; i++) {
if (grid[row][i] === num) {
return true;
}
}
return false;
}
function isInCol(grid, col, num, size) {
for (let i = 0; i < size; i++) {
if (grid[i][col] === num) {
return true;
}
}
return false;
}
function isInBox(grid, row, col, num, size) {
let boxSizeRow;
let boxSizeCol;
if (size === 4) {
boxSizeRow = 2;
boxSizeCol = 2;
}
else if (size === 6) {
boxSizeRow = 2;
boxSizeCol = 3;
}
else {
boxSizeRow = 3;
boxSizeCol = 3;
}
const startRow = row - (row % boxSizeRow);
const startCol = col - (col % boxSizeCol);
for (let i = 0; i < boxSizeRow; i++) {
for (let j = 0; j < boxSizeCol; j++) {
if (grid[startRow + i][startCol + j] === num) {
return true;
}
}
}
return false;
}
function removeNumbers(grid, difficulty, size) {
const attempts = getAttempts(difficulty, size);
for (let i = 0; i < attempts; i++) {
let row = Math.floor(Math.random() * size);
let col = Math.floor(Math.random() * size);
while (grid[row][col] === 0) {
row = Math.floor(Math.random() * size);
col = Math.floor(Math.random() * size);
}
const backup = grid[row][col];
grid[row][col] = 0;
const gridCopy = createEmptyGrid(size);
copyGrid(grid, gridCopy);
if (!hasUniqueSolution(gridCopy, size)) {
grid[row][col] = backup;
}
}
}
function getAttempts(difficulty, size) {
const totalCells = size * size;
let percentage = 0;
switch (difficulty) {
case "easy":
percentage = 0.4;
break;
case "medium":
percentage = 0.55;
break;
case "hard":
percentage = 0.7;
break;
}
return Math.floor(totalCells * percentage);
}
function hasUniqueSolution(grid, size) {
let count = 0;
solve(grid, size, () => {
count++;
});
return count === 1;
}
function solve(grid, size, onSolution) {
const emptyCell = findEmptyCell(grid, size);
if (!emptyCell) {
onSolution();
return;
}
const [row, col] = emptyCell;
const numbers = getPossibleNumbers(size);
for (const num of numbers) {
if (isValid(grid, row, col, num, size)) {
grid[row][col] = num;
solve(grid, size, onSolution);
grid[row][col] = 0;
}
}
}
function getThemeColors(theme) {
if (theme === "light" || !theme) {
return {
background: "#ffffff",
gridColor: "#000000",
textColor: "#000000",
boxLineColor: "#000000",
};
}
else if (theme === "dark") {
return {
background: "#2d2d2d",
gridColor: "#ffffff",
textColor: "#ffffff",
boxLineColor: "#ffffff",
};
}
else {
return theme;
}
}
function toPDF(grid_1) {
return __awaiter(this, arguments, void 0, function* (grid, options = {}, solution) {
const { theme = "light", title = "Sudoku Puzzle", author = "Sudoku Generator", subject = "Sudoku Puzzle", keywords = "sudoku, puzzle, game", showSolution = false, } = options;
const colors = getThemeColors(theme);
const size = grid.length;
const doc = new jspdf_1.jsPDF({
orientation: "portrait",
unit: "mm",
format: "a4",
});
doc.setProperties({
title,
author,
subject,
keywords,
});
if (colors.background !== "#ffffff") {
doc.setFillColor(colors.background);
doc.rect(0, 0, 210, 297, "F");
}
doc.setTextColor(colors.textColor);
doc.setFont("helvetica", "bold");
doc.setFontSize(24);
doc.text(title, 105, 30, { align: "center" });
const cellSize = size === 9 ? 20 : size === 6 ? 25 : 30;
const totalGridSize = size * cellSize;
const startX = (210 - totalGridSize) / 2;
const startY = 60;
doc.setDrawColor(colors.gridColor);
for (let i = 0; i <= size; i++) {
const isBoxLine = getBoxLinePositions(size).includes(i);
const lineWidth = isBoxLine ? 2.268 : 0.85;
doc.setLineWidth(lineWidth);
const x = startX + i * cellSize;
doc.line(x, startY, x, startY + totalGridSize);
const y = startY + i * cellSize;
doc.line(startX, y, startX + totalGridSize, y);
}
doc.setTextColor(colors.textColor);
doc.setFont("helvetica", "bold");
doc.setFontSize(size === 9 ? 14 : size === 6 ? 16 : 20);
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
const puzzleValue = grid[row][col];
const solutionValue = solution ? solution[row][col] : 0;
let displayValue = 0;
let isGivenDigit = false;
if (showSolution && solution) {
displayValue = solutionValue;
isGivenDigit = puzzleValue !== 0;
}
else {
displayValue = puzzleValue;
isGivenDigit = true;
}
if (displayValue !== 0) {
const x = startX + col * cellSize + cellSize / 2;
const y = startY + row * cellSize + cellSize / 2 + (size === 9 ? 3 : size === 6 ? 4 : 5);
if (showSolution && !isGivenDigit) {
doc.setFont("helvetica", "normal");
doc.setTextColor(colors.textColor === "#000000" ? "#666666" : "#cccccc");
}
else {
doc.setFont("helvetica", "bold");
doc.setTextColor(colors.textColor);
}
doc.text(displayValue.toString(), x, y, { align: "center" });
}
}
}
return new Uint8Array(doc.output("arraybuffer"));
});
}
function getBoxLinePositions(size) {
if (size === 4) {
return [0, 2, 4];
}
else if (size === 6) {
return [0, 2, 4, 6];
}
else {
return [0, 3, 6, 9];
}
}
function toImage(grid, options = {}, solution) {
const { theme = "light", cellSize = 60, showSolution = false, format = "png", quality = 0.95, } = options;
const colors = getThemeColors(theme);
const size = grid.length;
const totalGridSize = size * cellSize;
const padding = cellSize;
const canvasWidth = totalGridSize + padding * 2;
const canvasHeight = totalGridSize + padding * 2;
const canvas = (0, canvas_1.createCanvas)(canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
ctx.fillStyle = colors.background;
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
ctx.strokeStyle = colors.gridColor;
ctx.lineWidth = 1;
for (let i = 0; i <= size; i++) {
const isBoxLine = getBoxLinePositions(size).includes(i);
ctx.lineWidth = isBoxLine ? 3 : 1;
const x = padding + i * cellSize;
ctx.beginPath();
ctx.moveTo(x, padding);
ctx.lineTo(x, padding + totalGridSize);
ctx.stroke();
const y = padding + i * cellSize;
ctx.beginPath();
ctx.moveTo(padding, y);
ctx.lineTo(padding + totalGridSize, y);
ctx.stroke();
}
const fontSize = cellSize * 0.5;
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
const puzzleValue = grid[row][col];
const solutionValue = solution ? solution[row][col] : 0;
let displayValue = 0;
let isGivenDigit = false;
if (showSolution && solution) {
displayValue = solutionValue;
isGivenDigit = puzzleValue !== 0;
}
else {
displayValue = puzzleValue;
isGivenDigit = true;
}
if (displayValue !== 0) {
const x = padding + col * cellSize + cellSize / 2;
const y = padding + row * cellSize + cellSize / 2;
if (showSolution && !isGivenDigit) {
ctx.fillStyle = colors.textColor === "#000000" ? "#666666" : "#cccccc";
ctx.font = `${fontSize}px Arial, sans-serif`;
}
else {
ctx.fillStyle = colors.textColor;
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
}
ctx.fillText(displayValue.toString(), x, y);
}
}
}
if (format === "jpeg") {
return canvas.toBuffer("image/jpeg", { quality });
}
else {
return canvas.toBuffer("image/png");
}
}