├── .gitignore ├── .nvmrc ├── README.md ├── followers └── README.md ├── package.json ├── sample_puzzles.js ├── solve.js ├── sudoku_solver.js └── sudoku_solver_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | node_modules/ 3 | *.swp 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v5.6.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Sudoku Solver from Scratch in JavaScript (TDD Style): A Tutorial 2 | 3 | As a JS noob, it can be tough to come up with ideas to practice your skills, but at NOOBjs, we've got you in mind with our noob-first design philosophy, so we have a little project for you. 4 | 5 | We've all played sudoku. Maybe you enjoy it, or maybe you really don't, but you feel offended by its incompleteness in the in-flight magazine. Either way, the puzzle must be solved! Sometimes you don't want to bother, but just copying the solution is not very satisfying. However, it's not really cheating if you write an app that does the heavy lifting, is it? That's what we're setting out to do today - write a sudoku solver from scratch in JavaScript. Oh, and why don't we do it [TDD](http://en.wikipedia.org/wiki/Test-driven_development) style? Don't worry, the solver should be so simple that a noob should be able to follow! No promises... 6 | 7 | The goal of this tutorial is to walk you through writing a sudoku solver in JavaScript, so I will not go into detail regarding the testing tools, but you should be able to get the idea of how everything is working. It won't be the fastest solver out there, but it will get the job done. 8 | 9 | ##Our Algorithm 10 | 11 | The most basic way I could think of to write a sudoku solver is to start at the first empty square, try a number, and check the row, column and nearest 3x3 square for a match. If there is no match, the number is currently valid, so move to the next square and try a new number. If you try numbers 1-9 and find no valid numbers, go back to the previous square and increment it by one until you either exceeded 9 or you find a new possible valid number. Keep following this same plan, sliding forward and backwards through the empty squares until you arrive at a solution. 12 | 13 | This is a [backtracking](http://en.wikipedia.org/wiki/Backtracking) algorithm. The basic idea being that you incrementally build a solution and discard it once you realize that it's not viable. 14 | 15 | For now, let's create a simple Node.js application that tests puzzles with the following format: 16 | 17 | var board = '090000006\n' + 18 | '000960485\n' + 19 | '000581000\n' + 20 | '004000000\n' + 21 | '517200900\n' + 22 | '602000370\n' + 23 | '100804020\n' + 24 | '706000810\n' + 25 | '300090000'; 26 | 27 | As you can see, we have one long string where each row is separated by newline characters, '\n', and all of the empty squares are represented by zeros. 28 | 29 | ##The Functions 30 | 31 | In order to create this sudoku solver, I see the following functions that we need to write: 32 | 33 | 1. `parseBoard`: parse the string into a 2D array and convert strings to integers for easier manipulation 34 | 35 | 2. `saveEmptyPositions`: iterate through the board and save all of the empty positions into an array so we can track which numbers are mutable and keep order to our testing 36 | 37 | 3. `checkRow`, `checkColumn`, `check3x3Square`, `checkValue`: check the column, row and current 3x3 square for a match to the current value tested, which can all be called with `checkValue` 38 | 39 | 4. `solvePuzzle`: take the parsed sudoku board, the array of empty positions, and find the solution 40 | 41 | 5. `solveSudoku`: parse the board, save the empty positions, and pass them to `solvePuzzle` 42 | 43 | ##Getting Started 44 | 45 | Create a new directory to begin the project and navigate to that directory. 46 | 47 | ##Our Testing Suite 48 | 49 | In order to run the tests involved in this project, you will need Node.js installed. For the testing suite, we're going to use [Mocha](http://mochajs.org/) and [Chai](http://chaijs.com/) for assertions, which can both be installed via NPM. 50 | 51 | Mocha must be installed globally: 52 | 53 | $ npm install -g mocha 54 | 55 | Chai can be installed locally: 56 | 57 | $ npm install chai 58 | 59 | ##Our First Test 60 | 61 | Time to begin the fun! Let's set up our file and write our first `describe` statement: 62 | 63 | // sudoku_solver_spec.js 64 | var Chai = require('chai'), 65 | expect = Chai.expect, 66 | solver = require('./sudoku_solver'); 67 | 68 | describe('Sudoku Solver', function() { 69 | // all tests should be inserted here 70 | }); 71 | 72 | So far, we're not actually doing anything. We're just setting the stage. Looking at our roadmap above, we need to first write a function that parses the sudoku board input. Here's the test: 73 | 74 | // sudoku_solver_spec.js 75 | var board = '090000006\n' + 76 | '000960485\n' + 77 | '000581000\n' + 78 | '004000000\n' + 79 | '517200900\n' + 80 | '602000370\n' + 81 | '100804020\n' + 82 | '706000810\n' + 83 | '300090000'; 84 | var parsedBoard; 85 | 86 | describe('#parseBoard()', function() { 87 | it('should parse a sudoku board into a 2D array', function() { 88 | parsedBoard = parseBoard(board); 89 | var expectedBoard = [ 90 | [0,9,0,0,0,0,0,0,6], 91 | [0,0,0,9,6,0,4,8,5], 92 | [0,0,0,5,8,1,0,0,0], 93 | [0,0,4,0,0,0,0,0,0], 94 | [5,1,7,2,0,0,9,0,0], 95 | [6,0,2,0,0,0,3,7,0], 96 | [1,0,0,8,0,4,0,2,0], 97 | [7,0,6,0,0,0,8,1,0], 98 | [3,0,0,0,9,0,0,0,0] 99 | ]; 100 | 101 | expect(parsedBoard.length).to.equal(9); 102 | expect(parsedBoard[0].length).to.equal(9); 103 | expect(parsedBoard).to.eql(expectedBoard); 104 | }); 105 | }); 106 | 107 | Let's write it red: 108 | 109 | // sudoku_solver.js 110 | module.exports.parseBoard = function(board) { 111 | 112 | }; 113 | 114 | Confirm that it fails: 115 | 116 | $ mocha sudoku_solver_spec.js 117 | 118 | Now, let's make it green: 119 | 120 | // sudoku_solver.js 121 | module.exports.parseBoard = function(board) { 122 | // split the board at each new line, and use map 123 | // to split each row into an array of characters 124 | return board.split('\n').map(function(row) { 125 | // use map to convert the characters into integers 126 | return row.split('').map(function(num) { 127 | return +num; 128 | }); 129 | }); 130 | }; 131 | 132 | ##Collect the Empty Positions 133 | 134 | Now that we have the board in a friendlier format, we can move to the next step where we track all of the positions that were left empty in the original board. Once we have this list, we can start iterating through all of the positions to test out integers. 135 | 136 | `saveEmptyPositions` should find all of the zeroes in the board and push the `row, column` coordinates to an array. Let's write the test! First, define `emptyPositions` variable next to the `parsedBoard` declaration so that we can access it later. Let's write our test: 137 | 138 | describe('#saveEmptyPositions()', function() { 139 | it('should save all of the empty positions, 0s, in a parsed board', function() { 140 | emptyPositions = solver.saveEmptyPositions(parsedBoard); 141 | 142 | var expectedPositions = [ 143 | [0,0],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[1,0],[1,1], 144 | [1,2],[1,5],[2,0],[2,1],[2,2],[2,6],[2,7],[2,8],[3,0], 145 | [3,1],[3,3],[3,4],[3,5],[3,6],[3,7],[3,8],[4,4],[4,5], 146 | [4,7],[4,8],[5,1],[5,3],[5,4],[5,5],[5,8],[6,1],[6,2], 147 | [6,4],[6,6],[6,8],[7,1],[7,3],[7,4],[7,5],[7,8],[8,1], 148 | [8,2],[8,3],[8,5],[8,6],[8,7],[8,8] 149 | ]; 150 | 151 | expect(emptyPositions.length).to.equal(51); 152 | expect(emptyPositions).to.eql(expectedPositions); 153 | }); 154 | }); 155 | 156 | Write an empty `saveEmptyPositions` function and run your test again with Mocha (`mocha sudoku_solver_spec.js`). You should have one failing test. Time to write the code to make it pass! 157 | 158 | // sudoku_solver.js 159 | module.exports.saveEmptyPositions = function(board) { 160 | // Create an array to save the positions 161 | var emptyPositions = []; 162 | 163 | // Check every square in the puzzle for a zero 164 | for(var i = 0; i < board.length; i++) { 165 | for(var j = 0; j < board[i].length; j++) { 166 | // If a zero is found, save that position 167 | if(board[i][j] === 0) { 168 | emptyPositions.push([i, j]); 169 | } 170 | } 171 | } 172 | 173 | // Return the positions 174 | return emptyPositions; 175 | }; 176 | 177 | All tests should be green! 178 | 179 | ##Check the Row, Column, and 3x3 Square 180 | 181 | Now, we have the sudoku board parsed into a 2D array, and we have all of the empty positions that we need to fill. Next, we need to write the `checkRow`, `checkColumn`, and `check3x3Square` functions to look for conflicts. When we have those three, we can combine them into the `checkValue` function to confirm the validity of a number in one call. Once we can actually check values we plug into our puzzle, we can move onto the stage of trying numbers systematically. 182 | 183 | Starting with our `checkRow` function, we need to see if there are any conflicts with the value we're trying. Time to write our test! 184 | 185 | describe('#checkRow()', function() { 186 | it('should check that each value in the row does not equal the input', function() { 187 | // No match. Return true. 188 | expect(solver.checkRow(parsedBoard, 0, 2)).to.be.ok; 189 | // Match found. Return false; 190 | expect(solver.checkRow(parsedBoard, 0, 9)).to.not.be.ok; 191 | }); 192 | }); 193 | 194 | In this test, we're expecting 2 to not be in row 0, but we expect 9 to already be in the row. Checking for this is simple. We just go through each value in the row and see if it's already in use: 195 | 196 | module.exports.checkRow = function(board, row, value) { 197 | // Iterate through every value in the row 198 | for(var i = 0; i < board[row].length; i++) { 199 | // If a match is found, return false 200 | if(board[row][i] === value) { 201 | return false; 202 | } 203 | } 204 | // If no match was found, return true 205 | return true; 206 | }; 207 | 208 | Checking the column should work in almost the same manner. Here's our test: 209 | 210 | describe('#checkColumn()', function() { 211 | it('should check that each value in a column does not equal the input', function() { 212 | // No match. Return true 213 | expect(solver.checkColumn(parsedBoard, 0, 9)).to.be.ok; 214 | // Match found. Return false 215 | expect(solver.checkColumn(parsedBoard, 0, 5)).to.not.be.ok; 216 | }); 217 | }); 218 | 219 | Verify that our test fails. Now, we can make it green: 220 | 221 | module.exports.checkColumn = function(board, column, value) { 222 | // Iterate through each value in the column 223 | for(var i = 0; i < board.length; i++) { 224 | // If a match is found, return false 225 | if(board[i][column] === value) { 226 | return false; 227 | } 228 | } 229 | // If no match was found, return true 230 | return true; 231 | }; 232 | 233 | The final values to check will be the 3x3 square that the value lives in. Before we can check every value in the appropriate square, we'll need to find the appropriate square to check. Let's test it: 234 | 235 | describe('#check3x3Square()', function() { 236 | it('should check that each value in a 3x3 square does not match the input', function() { 237 | // No match. Return true 238 | expect(solver.check3x3Square(parsedBoard, 2, 2, 1)).to.be.ok; 239 | expect(solver.check3x3Square(parsedBoard, 7, 7, 9)).to.be.ok; 240 | // Match found. Return false 241 | expect(solver.check3x3Square(parsedBoard, 2, 2, 9)).to.not.be.ok; 242 | expect(solver.check3x3Square(parsedBoard, 7, 7, 1)).to.not.be.ok; 243 | }); 244 | }); 245 | 246 | Once we verify that we have a failing test, we can work on making the test pass. Let's figure out the bounds of the 3x3 square and test each of the values: 247 | 248 | module.exports.check3x3Square = function(board, column, row, value) { 249 | // Save the upper left corner 250 | var columnCorner = 0, 251 | rowCorner = 0, 252 | squareSize = 3; 253 | 254 | // Find the left-most column 255 | while(column >= columnCorner + squareSize) { 256 | columnCorner += squareSize; 257 | } 258 | 259 | // Find the upper-most row 260 | while(row >= rowCorner + squareSize) { 261 | rowCorner += squareSize; 262 | } 263 | 264 | // Iterate through each row 265 | for(var i = rowCorner; i < rowCorner + squareSize; i++) { 266 | // Iterate through each column 267 | for(var j = columnCorner; j < columnCorner + squareSize; j++) { 268 | // Return false is a match is found 269 | if(board[i][j] === value) { 270 | return false; 271 | } 272 | } 273 | } 274 | // If no match was found, return true 275 | return true; 276 | }; 277 | 278 | Now, we have the three functions to test all possible conflicts for a particular value, so let's wrap all of that into one function: 279 | 280 | describe('#checkValue()', function() { 281 | it('should check whether a value is valid for a particular position', function() { 282 | // No match. Return true 283 | expect(solver.checkValue(parsedBoard, 0, 0, 2)).to.be.ok; 284 | expect(solver.checkValue(parsedBoard, 3, 7, 3)).to.be.ok; 285 | // Match found. Return false 286 | expect(solver.checkValue(parsedBoard, 0, 0, 9)).to.not.be.ok; 287 | expect(solver.checkValue(parsedBoard, 3, 7, 1)).to.not.be.ok; 288 | }); 289 | }); 290 | 291 | Once we check that we have a failing test, we can now combine `checkRow`, `checkColumn`, and `check3x3Square` to verify that a value is valid for a given position: 292 | 293 | module.exports.checkValue = function(board, column, row, value) { 294 | if(this.checkRow(board, row, value) && 295 | this.checkColumn(board, column, value) && 296 | this.check3x3Square(board, column, row, value)) { 297 | return true; 298 | } else { 299 | return false; 300 | } 301 | }; 302 | 303 | At this point, we have almost everything that we need to solve a sudoku puzzle. We've parsed the board, saved the empty positions, and can check for any possible conflicts given a value. Next up: solving the puzzle! 304 | 305 | ##Finding a Solution 306 | 307 | Now that we can test values, we need to write a function that can systematically check each possible value until we find a valid value. Essentially, we need to go through each empty position that we saved in the `emptyPositions` array, try numbers 1-9 at each position until we find a valid number, and move to the next position. If no valid numbers are found at the next position, we move back a position to find a new valid number. We do this through all of the positions until we've found a whole board of valid positions, giving us our solution. 308 | 309 | Our `solvePuzzle` function should take a parsed board and an array of empty positions and return the solution (as well as log it). Let's write our failing test: 310 | 311 | var expectedSolution = [[ 8,9,5,7,4,2,1,3,6 ], 312 | [ 2,7,1,9,6,3,4,8,5 ], 313 | [ 4,6,3,5,8,1,7,9,2 ], 314 | [ 9,3,4,6,1,7,2,5,8 ], 315 | [ 5,1,7,2,3,8,9,6,4 ], 316 | [ 6,8,2,4,5,9,3,7,1 ], 317 | [ 1,5,9,8,7,4,6,2,3 ], 318 | [ 7,4,6,3,2,5,8,1,9 ], 319 | [ 3,2,8,1,9,6,5,4,7 ]]; 320 | 321 | describe('#solvePuzzle()', function() { 322 | it('should find a solution to the puzzle passed in', function() { 323 | var solution = solver.solvePuzzle(parsedBoard, emptyPositions); 324 | 325 | expect(solution).to.eql(expectedSolution); 326 | }); 327 | }); 328 | 329 | After we verify that this test will fail, we can begin writing the code to solve our sudoku: 330 | 331 | module.exports.solvePuzzle = function(board, emptyPositions) { 332 | // Variables to track our position in the solver 333 | var limit = 9, 334 | i, row, column, value, found; 335 | for(i = 0; i < emptyPositions.length;) { 336 | row = emptyPositions[i][0]; 337 | column = emptyPositions[i][1]; 338 | // Try the next value 339 | value = board[row][column] + 1; 340 | // Was a valid number found? 341 | found = false; 342 | // Keep trying new values until either the limit 343 | // was reached or a valid value was found 344 | while(!found && value <= limit) { 345 | // If a valid value is found, mark found true, 346 | // set the position to the value, and move to the 347 | // next position 348 | if(this.checkValue(board, column, row, value)) { 349 | found = true; 350 | board[row][column] = value; 351 | i++; 352 | } 353 | // Otherwise, try the next value 354 | else { 355 | value++; 356 | } 357 | } 358 | // If no valid value was found and the limit was 359 | // reached, move back to the previous position 360 | if(!found) { 361 | board[row][column] = 0; 362 | i--; 363 | } 364 | } 365 | 366 | // A solution was found! Log it 367 | board.forEach(function(row) { 368 | console.log(row.join()); 369 | }); 370 | 371 | // return the solution 372 | return board; 373 | }; 374 | 375 | Now, all of our tests should be green again! And the solution should have logged: 376 | 377 | 8,9,5,7,4,2,1,3,6 378 | 2,7,1,9,6,3,4,8,5 379 | 4,6,3,5,8,1,7,9,2 380 | 9,3,4,6,1,7,2,5,8 381 | 5,1,7,2,3,8,9,6,4 382 | 6,8,2,4,5,9,3,7,1 383 | 1,5,9,8,7,4,6,2,3 384 | 7,4,6,3,2,5,8,1,9 385 | 3,2,8,1,9,6,5,4,7 386 | 387 | ##Pull It All Together! 388 | 389 | Finally, we have everything we need! We just need to write `solveSudoku`, which takes the original string of the sudoku board, parses the board, saves the empty positions, and runs our solver. That's it! All of this could have been handled in our `solvePuzzle` function, but I prefer to move as many individual steps out as possible. Our test is going to look just like our previous test, except that it's going to take our sudoku board string as an argument: 390 | 391 | describe('#solveSudoku()', function() { 392 | it('should find a solution to the puzzle string passed in', function() { 393 | var solution = solver.solveSudoku(board); 394 | 395 | expect(solution).to.eql(expectedSolution); 396 | }); 397 | }); 398 | 399 | Once we confirm that our tests fail, we can finish this! 400 | 401 | module.exports.solveSudoku = function(board) { 402 | var parsedBoard = this.parseBoard(board); 403 | var emptyPositions = this.saveEmptyPositions(parsedBoard); 404 | 405 | return this.solvePuzzle(parsedBoard, emptyPositions); 406 | }; 407 | 408 | All should now be green! And that's our fully tested sudoku solver. 409 | 410 | ##What next? 411 | 412 | The last step in TDD is refactoring, which we will save for you to do. As it stands, it solves a 'fiendish' puzzle in a few milliseconds, so we're content with the performance. However, there are a number of improvements that could be made. For example, we are manually checking every value, 1-9, whenever we test a new position no matter what. A possible improvement would be to be a little smarter about how we check values. Also, it currently does not handle the case of puzzle with no solution, nor does it handle any puzzles other than 9x9 boards. 413 | 414 | Other ideas would be to create a better interface to input a sudoku puzzle like a command line tool, web app, etc. Feel free to submit a pull request into the `followers` folder to share your projects! 415 | 416 | We hope you enjoyed this little hands-on tutorial. 417 | 418 | 419 | 420 | -------------------------------------------------------------------------------- /followers/README.md: -------------------------------------------------------------------------------- 1 | #Submitting a pull request 2 | 3 | If you're interested in submitting a project that you put together with this sudoku solver as a basis, please submit your pull request with a new file in this folder. Interesting interfaces, different algorithms, or whatever are welcome! 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sudoku_solvers", 3 | "version": "0.0.1", 4 | "description": "As a JS noob, it can be tough to come up with ideas to practice your skills, but at NOOBjs, we've got you in mind with our noob-first design philosophy, so we have a little project for you.", 5 | "main": "sample_puzzles.js", 6 | "dependencies": { 7 | "chai": "^3.5.0", 8 | "mocha": "^2.4.5" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "mocha" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/mmerkes/sudoku_solvers.git" 17 | }, 18 | "author": "Matt Merkes", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/mmerkes/sudoku_solvers/issues" 22 | }, 23 | "homepage": "https://github.com/mmerkes/sudoku_solvers#readme" 24 | } 25 | -------------------------------------------------------------------------------- /sample_puzzles.js: -------------------------------------------------------------------------------- 1 | /* 2 | EASY 3 | EXPECTED INPUT 4 | '090000006\n' + 5 | '000960485\n' + 6 | '000581000\n' + 7 | '004000000\n' + 8 | '517200900\n' + 9 | '602000370\n' + 10 | '100804020\n' + 11 | '706000810\n' + 12 | '300090000'; 13 | 14 | EXPECTED OUTPUT 15 | '895742136\n' + 16 | '271963485\n' + 17 | '463581792\n' + 18 | '934617258\n' + 19 | '517238964\n' + 20 | '682459371\n' + 21 | '159874623\n' + 22 | '746325819\n' + 23 | '328196547'; 24 | 25 | HARD 26 | EXPECTED INPUT 27 | '040001000\n' + 28 | '630080100\n' + 29 | '000790050\n' + 30 | '003000028\n' + 31 | '500040009\n' + 32 | '190000700\n' + 33 | '060058000\n' + 34 | '009020035\n' + 35 | '000900040'; 36 | 37 | FIENDISH 38 | EXPECTED INPUT 39 | '006008500\n' + 40 | '000070613\n' + 41 | '000000009\n' + 42 | '000090001\n' + 43 | '001000800\n' + 44 | '400530000\n' + 45 | '107053000\n' + 46 | '050064000\n' + 47 | '300100060'; 48 | 49 | EXPECTED OUTPUT 50 | '296318574\n' + 51 | '584972613\n' + 52 | '713645289\n' + 53 | '625897341\n' + 54 | '931426857\n' + 55 | '478531926\n' + 56 | '167253498\n' + 57 | '859764132\n' + 58 | '342189765\n'; 59 | */ 60 | -------------------------------------------------------------------------------- /solve.js: -------------------------------------------------------------------------------- 1 | // solve.js 2 | var solver = require('./sudoku_solver'); 3 | var board = ""; 4 | 5 | process.stdin.setEncoding('utf8'); 6 | 7 | process.stdin.on('readable', () => { 8 | var chunk = process.stdin.read(); 9 | 10 | if (chunk !== null) { 11 | board += chunk.replace(/,/g, "").replace(/-/g, "0") 12 | } 13 | }); 14 | 15 | process.stdin.on('end', () => { 16 | var solution = solver.solveSudoku(board); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /sudoku_solver.js: -------------------------------------------------------------------------------- 1 | module.exports.parseBoard = function(board) { 2 | // split the board at each new line, and use map 3 | // to split each row into an array of characters 4 | return board.split('\n').map(function(row) { 5 | // use map to convert the characters into integers 6 | return row.split('').map(function(num) { 7 | return +num; 8 | }); 9 | }); 10 | }; 11 | 12 | module.exports.saveEmptyPositions = function(board) { 13 | // Create an array to save the positions 14 | var emptyPositions = []; 15 | 16 | // Check every square in the puzzle for a zero 17 | for(var i = 0; i < board.length; i++) { 18 | for(var j = 0; j < board[i].length; j++) { 19 | // If a zero is found, so that position 20 | if(board[i][j] === 0) { 21 | emptyPositions.push([i, j]); 22 | } 23 | } 24 | } 25 | 26 | // Return the positions 27 | return emptyPositions; 28 | }; 29 | 30 | module.exports.checkRow = function(board, row, value) { 31 | // Iterate through every value in the row 32 | for(var i = 0; i < board[row].length; i++) { 33 | // If a match is found, return false 34 | if(board[row][i] === value) { 35 | return false; 36 | } 37 | } 38 | // If no match was found, return true 39 | return true; 40 | }; 41 | 42 | module.exports.checkColumn = function(board, column, value) { 43 | // Iterate through each value in the column 44 | for(var i = 0; i < board.length; i++) { 45 | // If a match is found, return false 46 | if(board[i][column] === value) { 47 | return false; 48 | } 49 | } 50 | // If no match was found, return true 51 | return true; 52 | }; 53 | 54 | module.exports.check3x3Square = function(board, column, row, value) { 55 | // Save the upper left corner 56 | var columnCorner = 0, 57 | rowCorner = 0, 58 | squareSize = 3; 59 | 60 | // Find the left-most column 61 | while(column >= columnCorner + squareSize) { 62 | columnCorner += squareSize; 63 | } 64 | 65 | // Find the upper-most row 66 | while(row >= rowCorner + squareSize) { 67 | rowCorner += squareSize; 68 | } 69 | 70 | // Iterate through each row 71 | for(var i = rowCorner; i < rowCorner + squareSize; i++) { 72 | // Iterate through each column 73 | for(var j = columnCorner; j < columnCorner + squareSize; j++) { 74 | // Return false is a match is found 75 | if(board[i][j] === value) { 76 | return false; 77 | } 78 | } 79 | } 80 | // If no match was found, return true 81 | return true; 82 | }; 83 | 84 | module.exports.checkValue = function(board, column, row, value) { 85 | if(this.checkRow(board, row, value) && 86 | this.checkColumn(board, column, value) && 87 | this.check3x3Square(board, column, row, value)) { 88 | return true; 89 | } else { 90 | return false; 91 | } 92 | }; 93 | 94 | module.exports.solvePuzzle = function(board, emptyPositions) { 95 | // Variables to track our position in the solver 96 | var limit = 9, 97 | i, row, column, value, found; 98 | for(i = 0; i < emptyPositions.length;) { 99 | row = emptyPositions[i][0]; 100 | column = emptyPositions[i][1]; 101 | // Try the next value 102 | value = board[row][column] + 1; 103 | // Was a valid number found? 104 | found = false; 105 | // Keep trying new values until either the limit 106 | // was reached or a valid value was found 107 | while(!found && value <= limit) { 108 | // If a valid value is found, marked found to true, 109 | // set the position to the value, and move to the 110 | // next position 111 | if(this.checkValue(board, column, row, value)) { 112 | found = true; 113 | board[row][column] = value; 114 | i++; 115 | } 116 | // Otherwise, try the next value 117 | else { 118 | value++; 119 | } 120 | } 121 | // If no valid value was found and the limit was 122 | // reached, move back to the previous position 123 | if(!found) { 124 | board[row][column] = 0; 125 | i--; 126 | } 127 | } 128 | 129 | // A solution was found! Log it 130 | board.forEach(function(row) { 131 | console.log(row.join()); 132 | }); 133 | 134 | // return the solution 135 | return board; 136 | }; 137 | 138 | module.exports.solveSudoku = function(board) { 139 | var parsedBoard = this.parseBoard(board); 140 | var emptyPositions = this.saveEmptyPositions(parsedBoard); 141 | 142 | return this.solvePuzzle(parsedBoard, emptyPositions); 143 | }; 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /sudoku_solver_spec.js: -------------------------------------------------------------------------------- 1 | // sudoku_solver_spec.js 2 | var Chai = require('chai'), 3 | expect = Chai.expect, 4 | solver = require('./sudoku_solver'); 5 | 6 | describe('Sudoku Solver', function() { 7 | // all tests should be inserted here 8 | var board = '090000006\n' + 9 | '000960485\n' + 10 | '000581000\n' + 11 | '004000000\n' + 12 | '517200900\n' + 13 | '602000370\n' + 14 | '100804020\n' + 15 | '706000810\n' + 16 | '300090000'; 17 | var parsedBoard, expectedBoard, emptyPositions; 18 | 19 | describe('#parseBoard()', function() { 20 | it('should parse a sudoku board into a 2D array ' + 21 | ' and convert to integers', function() { 22 | parsedBoard = solver.parseBoard(board); 23 | var expectedBoard = [ 24 | [0,9,0,0,0,0,0,0,6], 25 | [0,0,0,9,6,0,4,8,5], 26 | [0,0,0,5,8,1,0,0,0], 27 | [0,0,4,0,0,0,0,0,0], 28 | [5,1,7,2,0,0,9,0,0], 29 | [6,0,2,0,0,0,3,7,0], 30 | [1,0,0,8,0,4,0,2,0], 31 | [7,0,6,0,0,0,8,1,0], 32 | [3,0,0,0,9,0,0,0,0] 33 | ]; 34 | 35 | expect(parsedBoard.length).to.equal(9); 36 | expect(parsedBoard[0].length).to.equal(9); 37 | expect(parsedBoard).to.eql(expectedBoard); 38 | }); 39 | }); 40 | 41 | describe('#saveEmptyPositions()', function() { 42 | it('should save all of the empty positions, 0s, in a parsed board', function() { 43 | emptyPositions = solver.saveEmptyPositions(parsedBoard); 44 | 45 | var expectedPositions = [ 46 | [0,0],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[1,0],[1,1], 47 | [1,2],[1,5],[2,0],[2,1],[2,2],[2,6],[2,7],[2,8],[3,0], 48 | [3,1],[3,3],[3,4],[3,5],[3,6],[3,7],[3,8],[4,4],[4,5], 49 | [4,7],[4,8],[5,1],[5,3],[5,4],[5,5],[5,8],[6,1],[6,2], 50 | [6,4],[6,6],[6,8],[7,1],[7,3],[7,4],[7,5],[7,8],[8,1], 51 | [8,2],[8,3],[8,5],[8,6],[8,7],[8,8] 52 | ]; 53 | 54 | expect(emptyPositions.length).to.equal(51); 55 | expect(emptyPositions).to.eql(expectedPositions); 56 | }); 57 | }); 58 | 59 | describe('#checkRow()', function() { 60 | it('should check that each value in the row does not equal the input', function() { 61 | // No match. Return true. 62 | expect(solver.checkRow(parsedBoard, 0, 2)).to.be.ok; 63 | // Match found. Return false; 64 | expect(solver.checkRow(parsedBoard, 0, 9)).to.not.be.ok; 65 | }); 66 | }); 67 | 68 | describe('#checkColumn()', function() { 69 | it('should check that each value in a column does not equal the input', function() { 70 | // No match. Return true 71 | expect(solver.checkColumn(parsedBoard, 0, 9)).to.be.ok; 72 | // Match found. Return false 73 | expect(solver.checkColumn(parsedBoard, 0, 5)).to.not.be.ok; 74 | }); 75 | }); 76 | 77 | describe('#check3x3Square()', function() { 78 | it('should check that each value in a 3x3 square does not match the input', function() { 79 | // No match. Return true 80 | expect(solver.check3x3Square(parsedBoard, 2, 2, 1)).to.be.ok; 81 | expect(solver.check3x3Square(parsedBoard, 7, 7, 9)).to.be.ok; 82 | // Match found. Return false 83 | expect(solver.check3x3Square(parsedBoard, 2, 2, 9)).to.not.be.ok; 84 | expect(solver.check3x3Square(parsedBoard, 7, 7, 1)).to.not.be.ok; 85 | }); 86 | }); 87 | 88 | describe('#checkValue()', function() { 89 | it('should check whether a value is valid for a particular position', function() { 90 | // No match. Return true 91 | expect(solver.checkValue(parsedBoard, 0, 0, 2)).to.be.ok; 92 | expect(solver.checkValue(parsedBoard, 3, 7, 3)).to.be.ok; 93 | // Match found. Return false 94 | expect(solver.checkValue(parsedBoard, 0, 0, 9)).to.not.be.ok; 95 | expect(solver.checkValue(parsedBoard, 3, 7, 1)).to.not.be.ok; 96 | }); 97 | }); 98 | 99 | var expectedSolution = [[ 8,9,5,7,4,2,1,3,6 ], 100 | [ 2,7,1,9,6,3,4,8,5 ], 101 | [ 4,6,3,5,8,1,7,9,2 ], 102 | [ 9,3,4,6,1,7,2,5,8 ], 103 | [ 5,1,7,2,3,8,9,6,4 ], 104 | [ 6,8,2,4,5,9,3,7,1 ], 105 | [ 1,5,9,8,7,4,6,2,3 ], 106 | [ 7,4,6,3,2,5,8,1,9 ], 107 | [ 3,2,8,1,9,6,5,4,7 ]]; 108 | 109 | describe('#solvePuzzle()', function() { 110 | it('should find a solution to the puzzle array and emptyPositions passed in', function() { 111 | var solution = solver.solvePuzzle(parsedBoard, emptyPositions); 112 | 113 | expect(solution).to.eql(expectedSolution); 114 | }); 115 | }); 116 | 117 | describe('#solveSudoku()', function() { 118 | it('should find a solution to the puzzle string passed in', function() { 119 | var solution = solver.solveSudoku(board); 120 | 121 | expect(solution).to.eql(expectedSolution); 122 | }); 123 | }); 124 | }); 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | --------------------------------------------------------------------------------