This project has been the hardest for me yet! I got the idea of writing a sudoku
game that can generate a board and check the user’s solution after trying a hard
sudoku problem while having lunch with my math professor, Dr. Yasskin.
The HTML
This project is the first time I used an HTML preprocessor (Haml)
to simplify writing the HTML. Using Haml reduces the amount of markup by using loops
and nesting selectors.
I began with the board, which is organized into 3 horizontal rows which each contain
3 squares, which each contain 9 cells. This requires a few nested do loops which
is written below:
There is also a bar of numbers below the board which can be used to insert numbers
into the board:
The CSS
I used Sass as a preprocessor and first declared several variables:
The markup for styling the board is shown below. Making the squares float: left and
every third square clear: both is important to get the board to display correctly.
When hovering over a cell, I highlight it with background-color: $hover-color. I
also added contextual classes .selected, .error, and .valid for cells that
have been clicked on, cells that have invalid input, and cells that have valid input.
The JavaScript
After the UI was built to my satisfaction, I started thinking about the board
generation code. I decided to represent the board as a 2D array (although
later I realized I could have simply used an 81-item 1D array). Any value from the
board can be retrieved by its row index and column index: array[row-index][column-index].
Generating the Puzzle Board
I generated the starting board in two ways. For easy games, I took a solved board
and removed either 4 or 5 numbers within each square. For more difficult games, I
found a valid existing board that was not solved and permuted the numbers, the
row groups, column groups, and the rows and columns within each group of three.
This jumbles the board but ensures it remains valid.
Permuting Numbers
Permuting all the numbers is conceptually simple: one way to implement a permutation
is to increment each number on the board so that all 1’s become 2’s, all 2’s become
3’s, and so on until you reach 9’s which become 1’s. However, I wanted a slightly
more complex method: randomly selecting a number instead of incrementing. The key
to this method is making sure each number is only mapped to a single output number
and ensuring that the same number is not used twice for different inputs. If that
doesn’t make sense to you, take a look at the function I wrote:
Note: this function is encapsulated within the puzzle object, so this function
is actually a method on that object, hence the method: function() syntax.
Permuting Rows and Columns
The permutation algorithm I wrote for swapping rows and columns in the same group
works by iterating through rows/columns, randomly selecting two to swap, and continuing
until all row and column groups have had a swap:
Permuting Row and Column Groups
While similar in concept, permuting row and column groups of 3 is different
than the algorithm above. For the row groups, two are swapped at random using a
temporary variable (no iteration necessary). For the column groups, the rows are
iterated over and a temporary variable inside the loop is used to swap all cells
withing the columns. The complete function is shown below:
The Solver Algorithms
Now came the really hard part: writing the board solver function. I actually ended
up writing two different solver functions, one as a brute force algorithm
and one as a “smarter” algorithm. One important concept that I learned to keep
in mind while writing these functions is to distinguish between mutable cells
and immutable cells. Mutable cells are open or blank cells which are initially set
to 0 and can be changed by the function. Immutable cells are hints already given
on the puzzle board - they cannot be changed by the function because they are a
known part of the solution.
Brute Force Solver
The brute force method starts by assuming a 1 in the first mutable cell and checks for
validity in ascending numerical order before moving onto the next mutable cell.
If no number is valid for the cell, it retreats to the previous cell and
increments the cell value. As you can imagine, this takes a long time for
certain board configurations, but at least it guarantees to find a solution
if one exists.
While conceptually simple, implementing the algorithm proved more difficult. I first
realized I would need a Boolean variable which indicates whether the the function
is proceeding forward or backwards at the moment - I named this variable goingBack.
Note that it is within the vars object where I stored many other variables.
Next, I needed a way to determine where the first mutable cell is located, which I
did with a simple nested for loop and two variables storing the row and column
indices.
To start the solver, a for loop is initiated to iterate over all cells. Variables
i and j are declared as row and column indices, respectively. Next, an if
statement determines whether we have a mutable or immutable cell. Immutable cells
are simple skipped while mutable cells are dealt with. Otherwise, if the cell is
immutable and we’re going backwards (checking vars.goingBack), we keep going
back by decreasing the counter by 2.
Inside the if statement, we determine the current square number (0-8), cell number
(also 0-8), and declare the initial value we’re using (1). If we’re backtracking,
we increment the current cell’s value and check if it is now above 9, in which case
we reset the value to 0 and keep going back. If we’re at the beginning and we reach
the value 9 with an invalid board, the function throws an error and a message is
displayed (although this shouldn’t happen with valid boards).
Next, if we’re not backtracking, we start with the value 1 and check whether the
board is valid in its current configuration (we’ll get to this checking function later).
If the board is not valid, we increment! If the value goes over 9, we start backtracking
(change vars.goingBack to false) and start over. Otherwise, we keep whatever value
is valid and go on to the next cell.
“Smarter” Algorithm
The “smarter” algorithm uses essentially the same method as brute force, but
it first eliminates some of the solution board possibilities by checking the
adjacent row, square, and column cells for each cell.
Checking Validity
Remember that checkValid(num,solBoard,row,col,sq,cell) function we called in both
solving functions? It plays a vital role by checking whether the current configuration
is a valid board.
Adding Keyboard Functions
I also added useful keyboard functionality, including:
Entering a value by typing a number on the keyboard instead of from the bar of numbers below the board
Using the up/down/left/right arrow keys to navigate the board
Using the delete key to clear a cell
For keyboard numerical input, the userPick function determines which cell is selected
and then performes the appropriate action based on the input:
For board navigation using the arrow keys, some nested if/elseif statements are
required to test for the specific keys as well as handling behavior at the end of
rows and columns:
What I Learned
This took a LONG time. Like, it took a few weeks to get it right and fully complete.
One mistake I made which derailed me for a while was assuming that initiating a
variable and setting it equal to an existing array created a new array. Actually,
all that does is create a reference to the original array. I had to write a
function clone() that returns a copy of a given array.
Another difficulty was implementing the solving algorithms correctly. I can’t tell
you how many hours I spent debugging my functions. But I persevered, pushed through,
and now I can proudly present the finished product.
See the Pen [Dynamic Sudoku](https://codepen.io/acrenwelge/pen/bqybpv/) by Andrew ([@acrenwelge](https://codepen.io/acrenwelge)) on [CodePen](https://codepen.io).