sudoku-pro 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +10 -0
- package/esm.mjs +12 -0
- package/package.json +52 -45
- package/sudoku-pro.js +74 -99
package/cli.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { generateSudoku, printSudoku, waitForHint } = require("./sudoku-pro.js");
|
|
5
|
+
|
|
6
|
+
const difficulty = (process.argv[2] || "m").toLowerCase();
|
|
7
|
+
const { sudoku, solvedSudoku } = generateSudoku(difficulty);
|
|
8
|
+
|
|
9
|
+
printSudoku(sudoku);
|
|
10
|
+
waitForHint(sudoku, solvedSudoku);
|
package/esm.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import cjs from "./sudoku-pro.js";
|
|
2
|
+
|
|
3
|
+
// Re-export as named exports (frontend-friendly)
|
|
4
|
+
export const generateSudoku = cjs.generateSudoku;
|
|
5
|
+
export const printSudoku = cjs.printSudoku;
|
|
6
|
+
export const solveSudoku = cjs.solveSudoku;
|
|
7
|
+
export const printSudokuWithAnswers = cjs.printSudokuWithAnswers;
|
|
8
|
+
export const getHint = cjs.getHint;
|
|
9
|
+
export const waitForHint = cjs.waitForHint;
|
|
10
|
+
|
|
11
|
+
// Optional default export too (nice for interop)
|
|
12
|
+
export default cjs;
|
package/package.json
CHANGED
|
@@ -1,59 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sudoku-pro",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
3
|
+
"version": "1.0.11",
|
|
4
|
+
"description": "Sudoku generator, solver, and CLI tool. Works in terminal and frontend apps with a clean library-first design.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ashish Vashisht",
|
|
7
|
+
|
|
8
|
+
"main": "./sudoku-pro.js",
|
|
9
|
+
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./esm.mjs",
|
|
13
|
+
"require": "./sudoku-pro.js"
|
|
14
|
+
}
|
|
15
15
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"hard": "node sudoku-pro h",
|
|
20
|
-
"very": "node sudoku-pro v"
|
|
16
|
+
|
|
17
|
+
"bin": {
|
|
18
|
+
"sudoku-pro": "./cli.js"
|
|
21
19
|
},
|
|
20
|
+
|
|
22
21
|
"files": [
|
|
23
|
-
"sudoku-pro.js"
|
|
22
|
+
"sudoku-pro.js",
|
|
23
|
+
"esm.mjs",
|
|
24
|
+
"cli.js",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
24
27
|
],
|
|
28
|
+
|
|
29
|
+
"scripts": {
|
|
30
|
+
"easy": "node cli.js e",
|
|
31
|
+
"medium": "node cli.js m",
|
|
32
|
+
"hard": "node cli.js h",
|
|
33
|
+
"very": "node cli.js v",
|
|
34
|
+
"test": "node -e \"const { generateSudoku } = require('./sudoku-pro'); const g = generateSudoku('m'); console.log(g.sudoku.length === 9 ? 'ok' : 'fail')\""
|
|
35
|
+
},
|
|
36
|
+
|
|
25
37
|
"keywords": [
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"Medium Sudoku",
|
|
37
|
-
"Hard Sudoku",
|
|
38
|
-
"Very hard Sudoku",
|
|
39
|
-
"Brain Teaser",
|
|
40
|
-
"Puzzle",
|
|
41
|
-
"Puzzle Game",
|
|
42
|
-
"Game",
|
|
43
|
-
"Board Game",
|
|
44
|
-
"Logic Game",
|
|
45
|
-
"Numbers Game",
|
|
46
|
-
"Recreational Mathematics",
|
|
47
|
-
"Backtracking"
|
|
38
|
+
"sudoku",
|
|
39
|
+
"sudoku-generator",
|
|
40
|
+
"sudoku-solver",
|
|
41
|
+
"sudoku-cli",
|
|
42
|
+
"puzzle",
|
|
43
|
+
"game",
|
|
44
|
+
"logic-game",
|
|
45
|
+
"backtracking",
|
|
46
|
+
"board-game",
|
|
47
|
+
"brain-teaser"
|
|
48
48
|
],
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
"
|
|
49
|
+
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+ssh://git@github.com/concurdev/sudoku-pro.git"
|
|
53
53
|
},
|
|
54
|
+
|
|
54
55
|
"bugs": {
|
|
55
56
|
"url": "https://github.com/concurdev/sudoku-pro/issues"
|
|
56
57
|
},
|
|
58
|
+
|
|
57
59
|
"homepage": "https://github.com/concurdev/sudoku-pro#readme",
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=14"
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
"sideEffects": false
|
|
59
66
|
}
|
package/sudoku-pro.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// NOTE: No top-level require("readline").
|
|
4
|
+
// This file is safe to import in frontend bundlers.
|
|
5
5
|
|
|
6
6
|
function shuffle(array) {
|
|
7
|
-
// Fisher-Yates shuffle algorithm
|
|
8
7
|
for (let i = array.length - 1; i > 0; i--) {
|
|
9
8
|
const j = Math.floor(Math.random() * (i + 1));
|
|
10
9
|
[array[i], array[j]] = [array[j], array[i]];
|
|
@@ -15,7 +14,7 @@ function shuffle(array) {
|
|
|
15
14
|
function generateSudoku(difficulty) {
|
|
16
15
|
const sudoku = Array.from({ length: 9 }, () => Array(9).fill(0));
|
|
17
16
|
|
|
18
|
-
// Fill
|
|
17
|
+
// Fill diagonal blocks
|
|
19
18
|
for (let i = 0; i < 9; i += 3) {
|
|
20
19
|
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
21
20
|
shuffle(nums);
|
|
@@ -27,23 +26,23 @@ function generateSudoku(difficulty) {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
// Solve the puzzle
|
|
30
|
-
const solvedSudoku = JSON.parse(JSON.stringify(sudoku));
|
|
29
|
+
const solvedSudoku = JSON.parse(JSON.stringify(sudoku));
|
|
31
30
|
solveSudoku(solvedSudoku);
|
|
32
31
|
|
|
33
|
-
// Determine the number of cells to remove
|
|
32
|
+
// Determine the number of cells to remove
|
|
34
33
|
let numToRemove;
|
|
35
34
|
switch (difficulty) {
|
|
36
35
|
case "e":
|
|
37
|
-
numToRemove = ensureNumToRemoveInRange(20, 20);
|
|
36
|
+
numToRemove = ensureNumToRemoveInRange(20, 20);
|
|
38
37
|
break;
|
|
39
38
|
case "m":
|
|
40
|
-
numToRemove = ensureNumToRemoveInRange(25, 50);
|
|
39
|
+
numToRemove = ensureNumToRemoveInRange(25, 50);
|
|
41
40
|
break;
|
|
42
41
|
case "h":
|
|
43
|
-
numToRemove = ensureNumToRemoveInRange(30, 60);
|
|
42
|
+
numToRemove = ensureNumToRemoveInRange(30, 60);
|
|
44
43
|
break;
|
|
45
44
|
case "v":
|
|
46
|
-
numToRemove = ensureNumToRemoveInRange(35, 75);
|
|
45
|
+
numToRemove = ensureNumToRemoveInRange(35, 75);
|
|
47
46
|
break;
|
|
48
47
|
default:
|
|
49
48
|
throw new Error(
|
|
@@ -51,17 +50,15 @@ function generateSudoku(difficulty) {
|
|
|
51
50
|
);
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// Create a copy of the solved Sudoku to remove cells from
|
|
55
53
|
const sudokuCopy = JSON.parse(JSON.stringify(solvedSudoku));
|
|
56
54
|
|
|
57
|
-
// Remove some numbers to create the puzzle
|
|
58
55
|
for (let i = 0; i < numToRemove; i++) {
|
|
59
56
|
const row = Math.floor(Math.random() * 9);
|
|
60
57
|
const col = Math.floor(Math.random() * 9);
|
|
61
58
|
if (sudokuCopy[row][col] !== 0) {
|
|
62
59
|
sudokuCopy[row][col] = 0;
|
|
63
60
|
} else {
|
|
64
|
-
i--;
|
|
61
|
+
i--;
|
|
65
62
|
}
|
|
66
63
|
}
|
|
67
64
|
|
|
@@ -72,87 +69,61 @@ function ensureNumToRemoveInRange(lowerBound, upperBound) {
|
|
|
72
69
|
let numToRemove;
|
|
73
70
|
do {
|
|
74
71
|
numToRemove = Math.floor(Math.random() * lowerBound) + upperBound;
|
|
75
|
-
} while (numToRemove > 81);
|
|
72
|
+
} while (numToRemove > 81);
|
|
76
73
|
return numToRemove;
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
function solveSudoku(sudoku) {
|
|
80
77
|
const emptyCell = findEmptyCell(sudoku);
|
|
81
|
-
if (!emptyCell)
|
|
82
|
-
return true; // Puzzle solved successfully
|
|
83
|
-
}
|
|
78
|
+
if (!emptyCell) return true;
|
|
84
79
|
|
|
85
80
|
const [row, col] = emptyCell;
|
|
86
81
|
for (let num = 1; num <= 9; num++) {
|
|
87
82
|
if (isValidMove(sudoku, row, col, num)) {
|
|
88
83
|
sudoku[row][col] = num;
|
|
89
|
-
if (solveSudoku(sudoku))
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
sudoku[row][col] = 0; // Undo the assignment
|
|
84
|
+
if (solveSudoku(sudoku)) return true;
|
|
85
|
+
sudoku[row][col] = 0;
|
|
93
86
|
}
|
|
94
87
|
}
|
|
95
|
-
|
|
96
|
-
return false; // No solution found, backtrack
|
|
88
|
+
return false;
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
function findEmptyCell(sudoku) {
|
|
100
|
-
// Function to find an empty cell in the Sudoku grid
|
|
101
92
|
for (let row = 0; row < 9; row++) {
|
|
102
93
|
for (let col = 0; col < 9; col++) {
|
|
103
|
-
if (sudoku[row][col] === 0)
|
|
104
|
-
return [row, col];
|
|
105
|
-
}
|
|
94
|
+
if (sudoku[row][col] === 0) return [row, col];
|
|
106
95
|
}
|
|
107
96
|
}
|
|
108
|
-
return null;
|
|
97
|
+
return null;
|
|
109
98
|
}
|
|
110
99
|
|
|
111
100
|
function isValidMove(sudoku, row, col, num) {
|
|
112
|
-
// Check if assigning num to the cell at (row, col) is a valid move
|
|
113
|
-
// Check row
|
|
114
101
|
for (let i = 0; i < 9; i++) {
|
|
115
|
-
if (sudoku[row][i] === num)
|
|
116
|
-
return false; // Number already exists in the row
|
|
117
|
-
}
|
|
102
|
+
if (sudoku[row][i] === num) return false;
|
|
118
103
|
}
|
|
119
|
-
|
|
120
|
-
// Check column
|
|
121
104
|
for (let i = 0; i < 9; i++) {
|
|
122
|
-
if (sudoku[i][col] === num)
|
|
123
|
-
return false; // Number already exists in the column
|
|
124
|
-
}
|
|
105
|
+
if (sudoku[i][col] === num) return false;
|
|
125
106
|
}
|
|
126
107
|
|
|
127
|
-
// Check 3x3 subgrid
|
|
128
108
|
const startRow = Math.floor(row / 3) * 3;
|
|
129
109
|
const startCol = Math.floor(col / 3) * 3;
|
|
130
110
|
for (let i = 0; i < 3; i++) {
|
|
131
111
|
for (let j = 0; j < 3; j++) {
|
|
132
|
-
if (sudoku[startRow + i][startCol + j] === num)
|
|
133
|
-
return false; // Number already exists in the subgrid
|
|
134
|
-
}
|
|
112
|
+
if (sudoku[startRow + i][startCol + j] === num) return false;
|
|
135
113
|
}
|
|
136
114
|
}
|
|
137
|
-
|
|
138
|
-
return true; // Valid move
|
|
115
|
+
return true;
|
|
139
116
|
}
|
|
140
117
|
|
|
141
118
|
function printSudoku(sudoku) {
|
|
142
119
|
console.log("Sudoku Puzzle:");
|
|
143
120
|
console.log("-------------------------");
|
|
144
121
|
sudoku.forEach((row, rowIndex) => {
|
|
145
|
-
if (rowIndex % 3 === 0 && rowIndex !== 0)
|
|
146
|
-
console.log("------|-------|-------");
|
|
147
|
-
}
|
|
122
|
+
if (rowIndex % 3 === 0 && rowIndex !== 0) console.log("------|-------|-------");
|
|
148
123
|
row.forEach((cell, cellIndex) => {
|
|
149
|
-
if (cellIndex % 3 === 0 && cellIndex !== 0)
|
|
150
|
-
process.stdout.write("| ");
|
|
151
|
-
}
|
|
124
|
+
if (cellIndex % 3 === 0 && cellIndex !== 0) process.stdout.write("| ");
|
|
152
125
|
process.stdout.write(String(cell || ".") + " ");
|
|
153
|
-
if (cellIndex === 8)
|
|
154
|
-
console.log();
|
|
155
|
-
}
|
|
126
|
+
if (cellIndex === 8) console.log();
|
|
156
127
|
});
|
|
157
128
|
});
|
|
158
129
|
console.log("-------------------------");
|
|
@@ -162,78 +133,82 @@ function printSudokuWithAnswers(sudoku) {
|
|
|
162
133
|
console.log("Sudoku Puzzle with Answers:");
|
|
163
134
|
console.log("-------------------------");
|
|
164
135
|
sudoku.forEach((row, rowIndex) => {
|
|
165
|
-
if (rowIndex % 3 === 0 && rowIndex !== 0)
|
|
166
|
-
console.log("------|-------|-------");
|
|
167
|
-
}
|
|
136
|
+
if (rowIndex % 3 === 0 && rowIndex !== 0) console.log("------|-------|-------");
|
|
168
137
|
row.forEach((cell, cellIndex) => {
|
|
169
|
-
if (cellIndex % 3 === 0 && cellIndex !== 0)
|
|
170
|
-
process.stdout.write("| ");
|
|
171
|
-
}
|
|
138
|
+
if (cellIndex % 3 === 0 && cellIndex !== 0) process.stdout.write("| ");
|
|
172
139
|
process.stdout.write(String(cell) + " ");
|
|
173
|
-
if (cellIndex === 8)
|
|
174
|
-
console.log();
|
|
175
|
-
}
|
|
140
|
+
if (cellIndex === 8) console.log();
|
|
176
141
|
});
|
|
177
142
|
});
|
|
178
143
|
console.log("-------------------------");
|
|
179
144
|
}
|
|
180
145
|
|
|
181
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Browser-friendly hint:
|
|
148
|
+
* - same logic (fill a random empty cell)
|
|
149
|
+
* - DOES NOT console.log by default
|
|
150
|
+
* - Returns {row,col,value} or null
|
|
151
|
+
*
|
|
152
|
+
* If you want old behavior (printing), pass { log: true }.
|
|
153
|
+
*/
|
|
154
|
+
function getHint(sudoku, solvedSudoku, opts = {}) {
|
|
182
155
|
const emptyCells = [];
|
|
183
156
|
for (let i = 0; i < 9; i++) {
|
|
184
157
|
for (let j = 0; j < 9; j++) {
|
|
185
|
-
if (sudoku[i][j] === 0)
|
|
186
|
-
emptyCells.push([i, j]);
|
|
187
|
-
}
|
|
158
|
+
if (sudoku[i][j] === 0) emptyCells.push([i, j]);
|
|
188
159
|
}
|
|
189
160
|
}
|
|
190
161
|
if (emptyCells.length === 0) {
|
|
191
|
-
console.log("No empty cells left to provide a hint.");
|
|
192
|
-
return;
|
|
162
|
+
if (opts.log) console.log("No empty cells left to provide a hint.");
|
|
163
|
+
return null;
|
|
193
164
|
}
|
|
165
|
+
|
|
194
166
|
const randomIndex = Math.floor(Math.random() * emptyCells.length);
|
|
195
167
|
const [row, col] = emptyCells[randomIndex];
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}`
|
|
201
|
-
|
|
168
|
+
const value = solvedSudoku[row][col];
|
|
169
|
+
sudoku[row][col] = value;
|
|
170
|
+
|
|
171
|
+
if (opts.log) {
|
|
172
|
+
console.log(`Hint: The number at row ${row + 1}, column ${col + 1} is ${value}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { row, col, value };
|
|
202
176
|
}
|
|
203
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Terminal-only function. Safe for bundlers because it loads readline dynamically.
|
|
180
|
+
* If you call it in the browser, it throws a clean error.
|
|
181
|
+
*/
|
|
204
182
|
function waitForHint(sudoku, solvedSudoku) {
|
|
183
|
+
if (typeof process === "undefined" || !process.stdin) {
|
|
184
|
+
throw new Error("waitForHint() is terminal-only (Node.js). Use getHint() in the browser.");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Lazy require so browser builds don't crash
|
|
188
|
+
// eslint-disable-next-line global-require
|
|
189
|
+
const readline = require("readline");
|
|
190
|
+
|
|
205
191
|
const rl = readline.createInterface({
|
|
206
192
|
input: process.stdin,
|
|
207
193
|
output: process.stdout,
|
|
208
194
|
});
|
|
209
195
|
|
|
210
|
-
rl.question(
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
console.log(
|
|
221
|
-
"Invalid input. Please press 'h' for a hint or 'c' for the complete solution."
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
rl.close();
|
|
225
|
-
waitForHint(sudoku, solvedSudoku); // Ask again for hint or complete solution
|
|
196
|
+
rl.question("Press 'h' for a hint or 'c' for the complete solution: ", (answer) => {
|
|
197
|
+
const a = answer.trim().toLowerCase();
|
|
198
|
+
if (a === "h") {
|
|
199
|
+
getHint(sudoku, solvedSudoku, { log: true });
|
|
200
|
+
printSudoku(sudoku);
|
|
201
|
+
} else if (a === "c") {
|
|
202
|
+
printSudokuWithAnswers(solvedSudoku);
|
|
203
|
+
process.exit(0);
|
|
204
|
+
} else {
|
|
205
|
+
console.log("Invalid input. Please press 'h' for a hint or 'c' for the complete solution.");
|
|
226
206
|
}
|
|
227
|
-
|
|
207
|
+
rl.close();
|
|
208
|
+
waitForHint(sudoku, solvedSudoku);
|
|
209
|
+
});
|
|
228
210
|
}
|
|
229
211
|
|
|
230
|
-
const difficulty = args[0]; // Get the difficulty level from command line arguments
|
|
231
|
-
|
|
232
|
-
const { sudoku, solvedSudoku } = generateSudoku(difficulty);
|
|
233
|
-
printSudoku(sudoku);
|
|
234
|
-
|
|
235
|
-
waitForHint(sudoku, solvedSudoku);
|
|
236
|
-
|
|
237
212
|
module.exports = {
|
|
238
213
|
generateSudoku,
|
|
239
214
|
printSudoku,
|