Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating a Sudoku game

0.00/5 (No votes)
12 Mar 2009 1  
How to create a Sudoku game with speed in mind.

Introduction

This is the code-behind to generate / solve / use a Sudoku grid. I won't describe or provide the code to display a grid because there are millions of ways to do that and I will let you decide how you want to do it.

But, I will provide an executable with an interface to demonstrate how it can be used.

sudoku_normal.jpg

sudoku_adv.jpg

Background

This code was written with speed in mind.

Using the Code

In the zip file, I have provided a project file, a module and four classes. The code inside the class could be moved into the module; since the function / sub inside them are shared, I decided to split it that way so it's clearer. Let's start with the project file; there is one major thing inside it: I enabled "Remove integer overflow checks" in the advanced compiler settings, everything else are the default settings.

I have the following variables declared in the module:

Private Const debugTime As String = "hh:mm:ss.fffffff"
Private r As New Random()

Private iSquare As Integer
Private squareExp2 As Integer
Private squareExp3 As Integer
Private gridRow() As Integer
Private gridCol() As Integer
Private gridbox() As Integer
Private gridbox1() As Integer

Private selectedGameType As gridSize = gridSize.grid9X9
Private Enum gridSize
    grid9X9 = 3
    grid16X16 = 4
End Enum

Private gameType As benchType = benchType.random
Private Enum benchType
    random
    solve
End Enum

I initialized iSquare, squareExp2, and squareExp3 by doing the following:

iSquare = selectedGameType
squareExp2 = iSquare * iSquare
squareExp3 = squareExp2 * squareExp2 - 1

To populate the row and column arrays, I use the code below:

For i = 0 To squareExp3
    gridCol(i) = i Mod squareExp2
    gridRow(i) = i - gridCol(i) 
Next

In the beginning, I had this code to populate the box array, but I didn't like it:

For j = 0 To squareExp2 - 1
 For i = 0 To squareExp2 - 1
     box = (j * squareExp2) + i

     gridbox(box) = ((j \ Square) * squareExp2 * Square) + _
       ((j Mod Square) * Square) + (i \ Square) * squareExp2 + (i Mod Square)
     gridbox1(gridbox(box)) = ((j \ Square) * squareExp2 * Square) + _
       (((j Mod Square) * Square) * Square)
 Next
Next

So, I went to stackoverflow and I submitted this question.

I got the following response from another user:

So for the gridbox, you are basically taking i and j and interleaving their digits in base(square). E.g., given i and j in base(scale), you are computing: i(2)j(2)i(1)j(1), where (n) signifies the digit base(scale). So that's easy enough to speed up: just keep a running tally of each digit's contribution to the final value of gridbox(...).

Speeding up gridbox1(...) is even easier. Basically, you are computing:

Square * Square *(j\Square * Square + (j Mod Square))

Now, (j Mod Square) expands to:

(j - j\Square*Square)

which reduces the above to:

Square * Square *(j\Square * Square + (j - j\Square*Square))

which simplifies to:

Square * Square *(j\Square * Square + j - j\Square*Square)

which simplifies to:

Square * Square * (j)

I changed my code after reading the response, as follows:

Dim box As Integer
Dim Square2 As Integer = iSquare * iSquare
Dim Square3 As Integer = Square2 * iSquare
Dim Square4 As Integer = Square2 * Square2
Dim j2_offset As Integer, j1_offset As Integer
Dim j1_interleaved_offset As Integer, i2_offset As Integer
Dim i2_interleaved_offset As Integer, j_offset_value As Integer
Dim offset_value As Integer, interleaved_value As Integer

For j2 As Integer = 0 To iSquare - 1
    j1_offset = 0
    j1_interleaved_offset = 0
    For j1 As Integer = 0 To iSquare - 1
        i2_offset = 0
        i2_interleaved_offset = 0
        j_offset_value = j2_offset + j1_offset
        For i2 As Integer = 0 To iSquare - 1
            offset_value = j_offset_value + i2_offset
            interleaved_value = j2_offset + _
               i2_interleaved_offset + j1_interleaved_offset
            For i1 As Integer = 0 To iSquare - 1
                 box = offset_value + i1
                 gridbox(box) = interleaved_value + i1
                 gridbox1(gridbox(box)) = j_offset_value
             Next
             i2_offset += iSquare
             i2_interleaved_offset += Square2
         Next
        j1_offset += Square2
        j1_interleaved_offset += iSquare
    Next
    j2_offset += Square3
Next

Why do all these steps to fill these arrays? To be able to use them this way:

To get every number inside a row, based on a position (which is i in these examples):

j = myGridRow(i)
For x = j To (j + squareExp2) - 1
    foundNumber(grid(x)) = grid(x)
Next

To get every number inside a column:

j = myGridCol(i)
For x = j To squareExp3 Step squareExp2
    foundNumber(grid(x)) = grid(x)
Next

To get every number inside a box, normally, you would do something like this:

k = (gridRow(pos) \ iSquare) * iSquare
l = (gridCol(pos) \ iSquare) * iSquare

For i = l To l + iSquare - 1
    For j = k To k + iSquare - 1
        box = (j * squareExp2) + i
        foundNumber(grid(box)) = grid(box)
    Next
Next

But, since with the gridbox and gridbox1 array you already know every position inside the box, you only need to do this:

j = myGridbox1(i)
For x = j To (j + squareExp2) - 1
    foundNumber(grid(myGridbox(x))) = grid(myGridbox(x))
Next

For the classes, I always pass all the pre-calculated variables, arrays, and the random variable.

The function returning a random grid returns an one dimensional array. To display it, you just need to use a loop and make a new row after every squareExp2. For example:

Dim msg As String = Nothing

For i = 0 To squareExp3
    If i Mod squareExp2 = 0 Then
       msg += vbCrLf
    End If
    msg += grid(i).ToString
Next
MsgBox(msg)

To generate a grid, I have two main parts inside the function. The first part contains the backtracking logic, and the second part finds every possible number for a specific position and then selects a random one from the resulting list of possible numbers.

The Grid array has a size of squareExp3, and I loop through it to fill it sequentially.

Let's start with the backtracking logic:

If count = 0 Then
   x = i

   If tried > squareExp2 Then
      tried = 0

      If i > squareExp2Mult2 Then
         i -= squareExp2Mult2 - 2
      Else
         i = 0
      End If
   Else
      tried += 1
      i -= squareExp2 - 2
   End If

   For j = i To x
      grid(j) = 0
   Next
End If

squareExp2Mult2 equals squareExp2 * 2.

count shows how many possibilities were in the previous position, and I initialize it to one so it won't enter when I start the loop.

If there was no possible number in the previous position, the code will check to see how many times it entered the backtracking logic for the current grid, and if it was, for an example with a 9x9 grid 10 times, it will check what is the current position (i) to make sure it won't go into a negative value and remove, for the same 9x9 grid, a maximum of 16 numbers; if it was less than 10 times, it would just remove 7 numbers.

When that is done, the main loop counter will change so it can continue to fill the grid. I picked the numbers "16" and "7". When I was doing benchmarking, they seem to be the best numbers so far, but if you find others, let me know.

For the second part:

j = myGridRow(i)
For x = j To (j + squareExp2) - 1
    foundNumber(grid(x)) = grid(x)
Next

j = myGridCol(i)
For x = j To squareExp3 Step squareExp2
    foundNumber(grid(x)) = grid(x)
Next

j = myGridbox1(i)
For x = j To (j + squareExp2) - 1
    foundNumber(grid(myGridbox(x))) = grid(myGridbox(x))
Next

count = 0
For x = 1 To squareExp2
    If foundNumber(x) = 0 Then
       foundNumberRnd(count) = x
       count += 1
    End If
    foundNumber(x) = 0
Next

If count > 1 Then
     grid(i) = foundNumberRnd(r.Next(count))
Else
     grid(i) = foundNumberRnd(0)
End If

I already explained how the first three loops are working, so I will start explaining from the "count = 0" line.

I loop through the foundNumber array, which has a size of squareExp2, to find every 0. When a 0 is found, it means there is a possible number for the current position (i), so I put the value of counter x inside the second array (foundNumberRnd), which is used to find the random number, and then increment the counter. Finally, I reset the foundNumber value to 0 for the next loop (position).

If there is more than one number found, count > 1, I pick a random number inside the foundNumberRnd array. Otherwise, I pick the first number inside the foundNumberRnd array which can be a valid or invalid number; if it is an invalid number, it means the count is still 0, and the backtracking logic will be triggered. The function returning hints needs two more parameters, the actual grid() and the position, and it will return a one dimensional array. To use the result, you only need to check the array between 1 and squareExp2 and find every 0.

I wrote a function that verifies if the grid is valid. You need to pass the grid() to the function and it will return true/false.

The function to solve the grid is far from done, it's a work in progress.

Beware, there is no "stop" inside this logic so you can get an infinite loop if the grid is not valid or the current algorithm cannot find a solution. The function needs a string to represent the grid, or you can pass nothing to actually create one. When a solution is found, it will return an array, and you can use the code used for the generator to display it.

Points of Interest

While I was implementing this code, I learned a few optimization tricks. For example:

  • An array is still faster than a List(Of T).
  • Enabling "Remove integer overflow checks" in advanced compiler settings givess some boost.
  • An one dimensional array is a lot faster than a multi dimensional array, but it's harder to work with.
  • It's better to declare a random variable at class / application level than redo it all the time in a sub / function.

And a few more minor ones.

I can do over 53,000 9x9 random grids per second, and over 310 16x16 random grids per second in my home computer.

History

  • Version 1.3, 2009-03-12.
  • Updated demo to fix a bug when resized.

  • Version 1.2, 2009-03-10
  • .

    Added a Zip file for Visual Studio 2005.

  • Version 1.1, 2009-03-08.
  • Added how the grid is generated.

  • Initial release version 1.0, 2009-03-05.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here