7

Goal is to create a 9x9 Sudoku matrix in Python.

So I got this far. But I cannot seem to get the program to get the interior contingent boxes correct.

def sudoku(size):
    import random as rn
    mydict = {}
    n = 0
    while len(mydict) < 9:
        n += 1
        x = range(1, size+1)
        testlist = rn.sample(x, len(x))

        isgood = True
        for dictid,savedlist in mydict.items():
            if isgood == False:
                break
            for v in savedlist:
                if testlist[savedlist.index(v)] == v:
                    isgood = False
                    break
        if isgood == True:
            #print 'success', testlist
            mydict[len(mydict)] = testlist
    return mydict, n

return_dict, total_tries = sudoku(9)
for n,v in return_dict.items():
    print n,v
print 'in',total_tries,'tries'
Ivan Kolesnikov
  • 1,787
  • 1
  • 29
  • 45
Justin Malinchak
  • 509
  • 1
  • 6
  • 11

6 Answers6

38

You can generate a random sudoku solution board where all numbers are filled in and then remove some of them to create the puzzle. This will ensure that the puzzle always has a solution. Making sure that it has exactly one solution is a bit more challenging (hint: you must leave at least 17 numbers for a 9x9 sudoku)

The algorithm below will generate a NxN random sudoku solution board instantly for N < 1000.

base  = 3
side  = base*base

# pattern for a baseline valid solution
def pattern(r,c): return (base*(r%base)+r//base+c)%side

# randomize rows, columns and numbers (of valid base pattern)
from random import sample
def shuffle(s): return sample(s,len(s)) 
rBase = range(base) 
rows  = [ g*base + r for g in shuffle(rBase) for r in shuffle(rBase) ] 
cols  = [ g*base + c for g in shuffle(rBase) for c in shuffle(rBase) ]
nums  = shuffle(range(1,base*base+1))

# produce board using randomized baseline pattern
board = [ [nums[pattern(r,c)] for c in cols] for r in rows ]

for line in board: print(line)

[6, 2, 5, 8, 4, 3, 7, 9, 1]
[7, 9, 1, 2, 6, 5, 4, 8, 3]
[4, 8, 3, 9, 7, 1, 6, 2, 5]
[8, 1, 4, 5, 9, 7, 2, 3, 6]
[2, 3, 6, 1, 8, 4, 9, 5, 7]
[9, 5, 7, 3, 2, 6, 8, 1, 4]
[5, 6, 9, 4, 3, 2, 1, 7, 8]
[3, 4, 2, 7, 1, 8, 5, 6, 9]
[1, 7, 8, 6, 5, 9, 3, 4, 2]

You can then remove some of the numbers from the sudoku solution to create the puzzle:

squares = side*side
empties = squares * 3//4
for p in sample(range(squares),empties):
    board[p//side][p%side] = 0

numSize = len(str(side))
for line in board:
    print(*(f"{n or '.':{numSize}} " for n in line))

6  .  .  .  .  3  .  .  1
.  9  .  .  .  .  .  .  3
4  .  3  .  .  .  6  .  .
.  .  .  5  9  .  2  .  6
.  .  .  .  .  .  .  .  .
.  .  7  .  .  .  .  .  4
.  .  .  .  .  .  1  7  .
.  .  2  .  .  8  .  .  .
.  .  8  .  .  .  .  4  2

For 4x4 up to 36x36 puzzles, you could make a nicer print of the board like this:

def expandLine(line):
    return line[0]+line[5:9].join([line[1:5]*(base-1)]*base)+line[9:13]
line0  = expandLine("╔═══╤═══╦═══╗")
line1  = expandLine("║ . │ . ║ . ║")
line2  = expandLine("╟───┼───╫───╢")
line3  = expandLine("╠═══╪═══╬═══╣")
line4  = expandLine("╚═══╧═══╩═══╝")

symbol = " 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ"
nums   = [ [""]+[symbol[n] for n in row] for row in board ]
print(line0)
for r in range(1,side+1):
    print( "".join(n+s for n,s in zip(nums[r-1],line1.split("."))) )
    print([line2,line3,line4][(r%side==0)+(r%base==0)])

╔═══╤═══╤═══╦═══╤═══╤═══╦═══╤═══╤═══╗
║ 6 │   │   ║   │   │ 3 ║   │   │ 1 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │ 9 │   ║   │   │   ║   │   │ 3 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║ 4 │   │ 3 ║   │   │   ║ 6 │   │   ║
╠═══╪═══╪═══╬═══╪═══╪═══╬═══╪═══╪═══╣
║   │   │   ║ 5 │ 9 │   ║ 2 │   │ 6 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │   │   ║   │   │   ║   │   │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │   │ 7 ║   │   │   ║   │   │ 4 ║
╠═══╪═══╪═══╬═══╪═══╪═══╬═══╪═══╪═══╣
║   │   │   ║   │   │   ║ 1 │ 7 │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │   │ 2 ║   │   │ 8 ║   │   │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │   │ 8 ║   │   │   ║   │ 4 │ 2 ║
╚═══╧═══╧═══╩═══╧═══╧═══╩═══╧═══╧═══╝

[EDIT]

Here are some additional information on the shuffling process ...

Shuffling rows is broken down in groups of 3 rows. It is ok to swap groups as a whole but we can't swap rows across groups without breaking the integrity of the blocks. (the same reasoning applies to columns)

For example:

0 [6, 2, 5,  8, 4, 3,  7, 9, 1] \                 -|
1 [7, 9, 1,  2, 6, 5,  4, 8, 3] |  group 0 -|     -| r in shuffle(rBase) 
2 [4, 8, 3,  9, 7, 1,  6, 2, 5] /           |     -|
                                            |
3 [8, 1, 4,  5, 9, 7,  2, 3, 6] \           |     -|
4 [2, 3, 6,  1, 8, 4,  9, 5, 7] |  group 1 -| *   -| r in shuffle(rBase)
5 [9, 5, 7,  3, 2, 6,  8, 1, 4] /           |     -|
                                            |
6 [5, 6, 9,  4, 3, 2,  1, 7, 8] \           |     -|
7 [3, 4, 2,  7, 1, 8,  5, 6, 9] |  group 2 -|     -| r in shuffle(rBase)
8 [1, 7, 8,  6, 5, 9,  3, 4, 2] /                 -|

                                * for g in shuffle(rBase)

We can swap groups 0,1,2 by moving all 3 of their rows at the same time:

3 [8, 1, 4,  5, 9, 7,  2, 3, 6] \           |     -|
4 [2, 3, 6,  1, 8, 4,  9, 5, 7] |  group 1 -|     -| r in shuffle(rBase)
5 [9, 5, 7,  3, 2, 6,  8, 1, 4] /           |     -|
                                            |
6 [5, 6, 9,  4, 3, 2,  1, 7, 8] \           |     -|
7 [3, 4, 2,  7, 1, 8,  5, 6, 9] |  group 2 -| *   -| r in shuffle(rBase)
8 [1, 7, 8,  6, 5, 9,  3, 4, 2] /                 -|
                                            |
0 [6, 2, 5,  8, 4, 3,  7, 9, 1] \           |     -|
1 [7, 9, 1,  2, 6, 5,  4, 8, 3] |  group 0 -|     -| r in shuffle(rBase) 
2 [4, 8, 3,  9, 7, 1,  6, 2, 5] /           |     -|

                                * for g in shuffle(rBase)

And we can swap between the 3 rows of a group (e.g. 3,4,5) ...

0 [6, 2, 5,  8, 4, 3,  7, 9, 1] \                 -|
1 [7, 9, 1,  2, 6, 5,  4, 8, 3] |  group 0 -|     -| r in shuffle(rBase) 
2 [4, 8, 3,  9, 7, 1,  6, 2, 5] /           |     -|
                                            |
5 [9, 5, 7,  3, 2, 6,  8, 1, 4] \           |     -|
4 [2, 3, 6,  1, 8, 4,  9, 5, 7] |  group 1 -| *   -| r in shuffle(rBase)
3 [8, 1, 4,  5, 9, 7,  2, 3, 6] /           |     -|
                                            |
6 [5, 6, 9,  4, 3, 2,  1, 7, 8] \           |     -|
7 [3, 4, 2,  7, 1, 8,  5, 6, 9] |  group 2 -|     -| r in shuffle(rBase)
8 [1, 7, 8,  6, 5, 9,  3, 4, 2] /                 -|

                                * for g in shuffle(rBase)

We CANNOT swap rows across groups (e.g. 1 <--> 3):

0 [6, 2, 5,  8, 4, 3,  7, 9, 1] \                 -|
3 [8, 1, 4,  5, 9, 7,  2, 3, 6] |  group 0 -|     -| r in shuffle(rBase) 
2 [4, 8, 3,  9, 7, 1,  6, 2, 5] /           |     -|
                                            |
1 [7, 9, 1,  2, 6, 5,  4, 8, 3] \           |     -|
4 [2, 3, 6,  1, 8, 4,  9, 5, 7] |  group 1 -| *   -| r in shuffle(rBase)
5 [9, 5, 7,  3, 2, 6,  8, 1, 4] /           |     -|
                                            |
6 [5, 6, 9,  4, 3, 2,  1, 7, 8] \           |     -|
7 [3, 4, 2,  7, 1, 8,  5, 6, 9] |  group 2 -|     -| r in shuffle(rBase)
8 [1, 7, 8,  6, 5, 9,  3, 4, 2] /                 -|

                                * for g in shuffle(rBase)

See the duplicate 8 in the top left block, duplicate 7 below that, etc.

Single solution puzzle

In order to generate a sudoku puzzle with only one solution you will need a solver function that can tell you if there are more than one solution. The strategy I would suggest is to start with 75% (or more) of the numbers removed, then check that there is only one solution. If there is more than one solution, put back a number and check again. You can put back a number at a random position or select a position where the solutions differ (which will converge faster to a single solution puzzle)

First write a solver that will generate all solutions that it finds (ideally as a generator because we only need the first 2). Here's a simple one:

def shortSudokuSolve(board):
    side   = len(board)
    base   = int(side**0.5)
    board  = [n for row in board for n in row ]
    blanks = [i for i,n in enumerate(board) if n==0 ]
    cover  = { (n,p):{*zip([2*side+r, side+c, r//base*base+c//base],[n]*(n and 3))}
                for p in range(side*side) for r,c in [divmod(p,side)] for n in range(side+1) }
    used   = set().union(*(cover[n,p] for p,n in enumerate(board) if n))
    placed = 0
    while placed>=0 and placed<len(blanks):
        pos        = blanks[placed]
        used      -= cover[board[pos],pos]
        board[pos] = next((n for n in range(board[pos]+1,side+1) if not cover[n,pos]&used),0)
        used      |= cover[board[pos],pos]
        placed    += 1 if board[pos] else -1
        if placed == len(blanks):
            solution = [board[r:r+side] for r in range(0,side*side,side)]
            yield solution
            placed -= 1

Starting with a solution variable with all numbers present and board variable containing the puzzle with 3/4 of numbers cleared, you can add numbers back to the board until there is only one way to solve it:

solution=[[9, 5, 3, 1, 6, 7, 4, 2, 8],
          [4, 2, 8, 3, 5, 9, 7, 6, 1],
          [7, 6, 1, 8, 2, 4, 9, 5, 3],
          [5, 8, 4, 9, 3, 6, 2, 1, 7],
          [6, 3, 9, 7, 1, 2, 5, 8, 4],
          [2, 1, 7, 4, 8, 5, 6, 3, 9],
          [3, 4, 5, 6, 9, 1, 8, 7, 2],
          [8, 7, 2, 5, 4, 3, 1, 9, 6],
          [1, 9, 6, 2, 7, 8, 3, 4, 5]]    
board=[ [0, 0, 0, 0, 0, 0, 0, 0, 8],
        [0, 2, 0, 0, 5, 0, 7, 6, 0],
        [0, 6, 0, 0, 0, 0, 0, 0, 3],
        [5, 0, 0, 0, 0, 0, 2, 0, 7],
        [0, 3, 0, 0, 1, 0, 0, 0, 0],
        [2, 0, 0, 4, 0, 0, 0, 3, 0],
        [0, 0, 0, 6, 0, 0, 0, 0, 0],
        [8, 0, 0, 0, 0, 0, 0, 0, 0],
        [1, 0, 0, 2, 7, 0, 0, 4, 0]]
    
import random
from itertools import islice
while True:
    solved  = [*islice(shortSudokuSolve(board),2)]
    if len(solved)==1:break
    diffPos = [(r,c) for r in range(9) for c in range(9)
               if solved[0][r][c] != solved[1][r][c] ] 
    r,c = random.choice(diffPos)
    board[r][c] = solution[r][c]

output:

╔═══╤═══╤═══╦═══╤═══╤═══╦═══╤═══╤═══╗
║   │   │   ║   │   │ 7 ║   │   │ 8 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │ 2 │   ║   │ 5 │   ║ 7 │ 6 │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │ 6 │   ║ 8 │   │ 4 ║   │   │ 3 ║
╠═══╪═══╪═══╬═══╪═══╪═══╬═══╪═══╪═══╣
║ 5 │   │   ║   │   │   ║ 2 │   │ 7 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║   │ 3 │   ║   │ 1 │   ║   │   │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║ 2 │   │   ║ 4 │   │   ║   │ 3 │   ║
╠═══╪═══╪═══╬═══╪═══╪═══╬═══╪═══╪═══╣
║   │ 4 │   ║ 6 │   │   ║   │   │   ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║ 8 │   │   ║   │   │   ║ 1 │   │ 6 ║
╟───┼───┼───╫───┼───┼───╫───┼───┼───╢
║ 1 │   │   ║ 2 │ 7 │   ║   │ 4 │   ║
╚═══╧═══╧═══╩═══╧═══╧═══╩═══╧═══╧═══╝

Note that this will work in a reasonable time for 9x9 sudoku boards but you will need a much better/faster solver function for larger boards

Also note that this will often produce "easy" puzzles as it will add in more numbers than absolutely necessary in some cases. Choosing the minimum set of numbers to add back-in would require a layered or backtracking approach which would be a bit more complex and much slower to run. Starting out with more empty spaces (e.g. 80% or more) will have a good chance of producing a more difficult puzzle within a reasonable timeframe.

Alain T.
  • 40,517
  • 4
  • 31
  • 51
  • base * (r % base) + r // base + c) % side : So how does this part of the algorithm work exactly? I tried to unpack it but could you try and describe in plain english what this is doing, and how it mathematically works in the context of the for loops to create unique rows and columns? ( I understand the syntax of mod and floor etc, just wondering how you came up with this part of the algorithm ) . – Johnny Gasyna Dec 02 '19 at 22:56
  • 3
    The objective is to place numbers 1 through 9 with a rotation on each line that will ensure no repetitions within columns and blocks. This produces a valid solution starting with 1,2,3... on the first line. Everything else is just randomization of that baseline solution by shuffling rows, columns and numbers. To see the base pattern produced by the formula, you can change the shuffle() function to return the original range as is. – Alain T. Dec 11 '19 at 14:34
  • Yes that is helpful, It looks like the array index follows the pattern 0,3,6,1,4,7,2,5,8 (or 1,4,7,2,5,8,3,6,9 if you ignore 0 based), do you think its possible to simplify the formula base * (r % base) + r // base + c) % side to achieve this indexing pattern? – Johnny Gasyna Dec 12 '19 at 16:14
  • 3
    For a 9x9, you could build an offset array for rows: `offsets = [0,3,6,1,4,7,2,5,8]` and then use it with a simpler formula `(offsets[r]+c)%9` in the comprehension. BTW this technique gives very fast results but it can only generate a subset (roughly 3x10^12) of the 6x10^21 possible solution board. Valid variations of the offsets table will produce additional board patterns. – Alain T. Dec 12 '19 at 19:16
  • I know that i'm quite late, but I am pondering upon this problem now and I am struggling to grasp the idea of randomizing the rows and columns. I get the shift 0, 1, 2 becomes 3, 4, 5, so all the blocks, cols and rows have unique 1-9, but why isn't the part where you call shuffle(rBase) breaking this? Can you elaborate please? – Viktor Stefanov Jan 26 '21 at 21:07
  • 1
    rBase represents block numbers (0..base) and row/column numbers within a block. You can shuffle rows and columns within a block and you can shuffle blocks within the board without breaking integrity. This is why the row numbers and column numbers are formed by combining a block number with a row/col number *within* that group. (e.g. `g*base + r`) – Alain T. Jan 26 '21 at 22:53
  • I get that. You say that you can shuffle rows and columns within a block. But isn't rows = shuffle([i for i in range(9)]) theoretically the same as g*base + r? It will return a shuffled list of range(0, 9)? I am having a hard time to grasp this concept, the pattern function is clear to me, could you explain further? – Viktor Stefanov Jan 27 '21 at 15:34
  • 1
    Not really. `for r in shuffle(rBase)` in the comprehension only shuffles the 3 rows of the current group `g` between themselves. Shuffling across the whole (9 row) range would break integrity between blocks. In other words, we can swap rows 0 and 2 but not rows 1 and 3 because that would move numbers across horizontal blocks 0 and 1 possibly creating duplicates within these blocks. – Alain T. Jan 27 '21 at 15:39
1

This should work.

def sudoku(size):
    import time
    start_time=time.time()

    import sys
    import random as rn
    mydict = {}
    n = 0
    print '--started calculating--'
    while len(mydict) < 9:
        n += 1
        x = range(1, size+1)
        testlist = rn.sample(x, len(x))

        isgood = True
        for dictid,savedlist in mydict.items():
            if isgood == False:
                break
            for v in savedlist:
                if testlist[savedlist.index(v)] == v:
                    isgood = False
                    break

        if isgood == True:
            isgoodafterduplicatecheck = True
            mod = len(mydict) % 3
            dsavedlists = {}
            dtestlists = {}
            dcombindedlists = {}
            for a in range(1,mod + 1):
                savedlist = mydict[len(mydict) - a]               
                for v1 in savedlist:
                    modsavedlists = (savedlist.index(v1) / 3) % 3
                    dsavedlists[len(dsavedlists)] = [modsavedlists,v1]
                for t1 in testlist:
                    modtestlists = (testlist.index(t1) / 3) % 3
                    dtestlists[len(dtestlists)] = [modtestlists,t1]
                for k,v2 in dsavedlists.items():
                    dcombindedlists[len(dcombindedlists)] = v2
                    dcombindedlists[len(dcombindedlists)] = dtestlists[k]
            vsave = 0
            lst1 = []
            for k, vx in dcombindedlists.items():
                vnew = vx[0]
                if not vnew == vsave:
                    lst1 = []
                    lst1.append(vx[1])
                else:
                    if vx[1] in lst1:
                        isgoodafterduplicatecheck = False
                        break
                    else:
                        lst1.append(vx[1])
                vsave = vnew

            if isgoodafterduplicatecheck == True:

                mydict[len(mydict)] = testlist
                print 'success found', len(mydict), 'row'   

    print '--finished calculating--'
    total_time = time.time()-start_time
    return mydict, n, total_time

return_dict, total_tries, amt_of_time = sudoku(9)
print ''
print '--printing output--'
for n,v in return_dict.items():
    print n,v
print 'process took',total_tries,'tries in', round(amt_of_time,2), 'secs'
print '-------------------'
Justin Malinchak
  • 509
  • 1
  • 6
  • 11
  • Hey Justin, your code is hard to understand. An explantion would be great. I have recognized that you didn't apply 3×3 subgrids check, therfore the result is wrong. Make sure to know what a sudoku is generated of at [Sudoku - Wikipedia](https://en.wikipedia.org/wiki/Sudoku). – Joe Feb 25 '21 at 08:17
1

If your goal is to create 9 x 9 Sudoku, then why not a simpler program? Works on any of n^2 x n^2 (boards) in poly-time. To create a puzzle, you may have to remove elements manually. Guaranteeing one solution requires some backtracking. Poly-time is what you want for larger n^2 x n^2 Sudoku Latin-Squares.

#Provide a list of non-repeating n-elements to output a valid sudoku grid.
#this code runs on python3
print('enter with [1,2,3...] brackets')
tup = input()[1:-1].split(',')
    #Input required to map out valid n x m or n^2 x n^2 Sudoku Grid
x = input('Enter mapping valid Sudoku eg. 3 for 9 x 9:')
e = input('Enter 9 for 9 x 9 ...12 for 12 x 12:')
f = input('Enter 3 if its a 9 x 9 ... n^2 x n^2:')
x = int(x)
e = int(e)
f = int(f)
    #Manipulation of Elements to prepare valid grid
squares = []
for i in range(len(tup)):
      squares.append(tup[i:] + tup[:i])

        #Everything below here is just printing
for s in range(x):
          for d in range(0,e,f):
            for si in range(s,e,f):
              for li in range(d,d+f):
                print(squares[si][li], end = '')
            print('')

#Remember that if you want
#to create a single board of n^2 x n^2
#you need to edit the below
#for a valid grid
#for example
#a 9 x 9 
#would be a 3 x 3
#and so on.

No repeating elements! For grids larger than 9 x 9 please use additonal brackets for readablity. eg. [[01],[02],[03],....] Also, please remember that you need to know multiplication to output a properly mapped n^2 x n^2. For example, a 25 x 25 should be a 5 x 5 for the inputs as follows

For x, x = 5

For e, e = 25

for f, f = 5

Also, I had a buddy who helped me convert my algorithm into this python code for my amateur Sudoku project. Thanks to that Reddit user.

By the way, it's actually O(m^2) time. Proof

Thank you Reddit buddy for the help.

Travis Wells
  • 321
  • 1
  • 2
  • 11
  • Sure, some tips on improving this answer in no particular order: (0) Specify what this does, i.e. creates an n^2 by n^2 Sudoku grid when provided a permutation of n elements; (1) the comment about constant time is a bit strange, as it's impossible to output n^4 of anything in constant time; (2) Indentation is weird. Use 4 spaces per indent level and don't randomly use extra indentation with comments; (3) just input `n` and remove `x`, `f`, and `e`, replacing `x` and `f` with `n` and `e` with `n**2`, (5) you never mentioned what m is, and the proof that it's O(n^4) is trivial anyway. – lirtosiast Jul 28 '19 at 04:35
0

Here's my solution

import random

def create_board(height, width):
    board = [[(i + k) % 9 + 1 for i in range(1, height + 1)] for k in range(width)] # Creates a board where each row counts to 9 such that no row contains more than one kind of each number. You can run this separately to see what it generates.
    random.shuffle(board) # Shuffles this list of lists
    board = [[board[x][y] for x in range(9)] for y in range(9)] # Reads each row and puts it into a column. (basically rotates it to its side)
    random.shuffle(board) # Shuffles this list again but while its on its side
    return board
    

Hope yall like it. It doesn't remove numbers but this can be done after you use the function randomly with this function

def remove_numbers(board, remove_amount):
    h, w, r = len(board), len(board[0]), []
    spaces = [[x, y] for x in range(h) for y in range(w)]
    for k in range(remove_amount):
        r = random.choice(spaces)
        board[r[0]][r[1]] = 0
        spaces.remove(r)
    return board
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
0
# import pygame library
import pygame
 
# initialise the pygame font
pygame.font.init()
 
# Total window
screen = pygame.display.set_mode((500, 600))
 
# Title and Icon
pygame.display.set_caption("SUDOKU SOLVER USING BACKTRACKING")
img = pygame.image.load('icon.png')
pygame.display.set_icon(img)
 
x = 0
y = 0
dif = 500 / 9
val = 0
# Default Sudoku Board.
grid =[
        [7, 8, 0, 4, 0, 0, 1, 2, 0],
        [6, 0, 0, 0, 7, 5, 0, 0, 9],
        [0, 0, 0, 6, 0, 1, 0, 7, 8],
        [0, 0, 7, 0, 4, 0, 2, 6, 0],
        [0, 0, 1, 0, 5, 0, 9, 3, 0],
        [9, 0, 4, 0, 6, 0, 0, 0, 5],
        [0, 7, 0, 3, 0, 0, 0, 1, 2],
        [1, 2, 0, 0, 0, 7, 4, 0, 0],
        [0, 4, 9, 2, 0, 6, 0, 0, 7]
    ]
 
# Load test fonts for future use
font1 = pygame.font.SysFont("comicsans", 40)
font2 = pygame.font.SysFont("comicsans", 20)
def get_cord(pos):
    global x
    x = pos[0]//dif
    global y
    y = pos[1]//dif
 
# Highlight the cell selected
def draw_box():
    for i in range(2):
        pygame.draw.line(screen, (255, 0, 0), (x * dif-3, (y + i)*dif), (x * dif + dif + 3, (y + i)*dif), 7)
        pygame.draw.line(screen, (255, 0, 0), ( (x + i)* dif, y * dif ), ((x + i) * dif, y * dif + dif), 7)  
 
# Function to draw required lines for making Sudoku grid        
def draw():
    # Draw the lines
        
    for i in range (9):
        for j in range (9):
            if grid[i][j]!= 0:
 
                # Fill blue color in already numbered grid
                pygame.draw.rect(screen, (0, 153, 153), (i * dif, j * dif, dif + 1, dif + 1))
 
                # Fill grid with default numbers specified
                text1 = font1.render(str(grid[i][j]), 1, (0, 0, 0))
                screen.blit(text1, (i * dif + 15, j * dif + 15))
    # Draw lines horizontally and verticallyto form grid          
    for i in range(10):
        if i % 3 == 0 :
            thick = 7
        else:
            thick = 1
        pygame.draw.line(screen, (0, 0, 0), (0, i * dif), (500, i * dif), thick)
        pygame.draw.line(screen, (0, 0, 0), (i * dif, 0), (i * dif, 500), thick)     
 

    # Fill value entered in cell     
    def draw_val(val):
        text1 = font1.render(str(val), 1, (0, 0, 0))
        screen.blit(text1, (x * dif + 15, y * dif + 15))   
     
    # Raise error when wrong value entered
    def raise_error1():
        text1 = font1.render("WRONG !!!", 1, (0, 0, 0))
        screen.blit(text1, (20, 570)) 
    def raise_error2():
        text1 = font1.render("Wrong !!! Not a valid Key", 1, (0, 0, 0))
        screen.blit(text1, (20, 570)) 
     
    # Check if the value entered in board is valid
    def valid(m, i, j, val):
        for it in range(9):
            if m[i][it]== val:
                return False
            if m[it][j]== val:
                return False
        it = i//3
        jt = j//3
        for i in range(it * 3, it * 3 + 3):
            for j in range (jt * 3, jt * 3 + 3):
                if m[i][j]== val:
                    return False
        return True
     
    # Solves the sudoku board using Backtracking Algorithm
    def solve(grid, i, j):
         
        while grid[i][j]!= 0:
            if i<8:
                i+= 1
            elif i == 8 and j<8:
                i = 0
                j+= 1
            elif i == 8 and j == 8:
                return True
        pygame.event.pump()   
        for it in range(1, 10):
            if valid(grid, i, j, it)== True:
                grid[i][j]= it
                global x, y
                x = i
                y = j
                # white color background\
                screen.fill((255, 255, 255))
                draw()
                draw_box()
                pygame.display.update()
                pygame.time.delay(20)
                if solve(grid, i, j)== 1:
                    return True
                else:
                    grid[i][j]= 0
                # white color background\
                screen.fill((255, 255, 255))
             
                draw()
                draw_box()
                pygame.display.update()
                pygame.time.delay(50)   
        return False 
     
    # Display instruction for the game
    def instruction():
        text1 = font2.render("PRESS D TO RESET TO DEFAULT / R TO EMPTY", 1, (0, 0, 0))
        text2 = font2.render("ENTER VALUES AND PRESS ENTER TO VISUALIZE", 1, (0, 0, 0))
        screen.blit(text1, (20, 520))       
        screen.blit(text2, (20, 540))
     
    # Display options when solved
    def result():
        text1 = font1.render("FINISHED PRESS R or D", 1, (0, 0, 0))
        screen.blit(text1, (20, 570))   
    run = True
    flag1 = 0
    flag2 = 0
    rs = 0
    error = 0
    # The loop thats keep the window running
    while run:
         
        # White color background
        screen.fill((255, 255, 255))
        # Loop through the events stored in event.get()
        for event in pygame.event.get():
            # Quit the game window
            if event.type == pygame.QUIT:
                run = False 
            # Get the mouse position to insert number   
            if event.type == pygame.MOUSEBUTTONDOWN:
                flag1 = 1
                pos = pygame.mouse.get_pos()
                get_cord(pos)
            # Get the number to be inserted if key pressed   
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    x-= 1
                    flag1 = 1
                if event.key == pygame.K_RIGHT:
                    x+= 1
                    flag1 = 1
                if event.key == pygame.K_UP:
                    y-= 1
                    flag1 = 1
                if event.key == pygame.K_DOWN:
                    y+= 1
                    flag1 = 1   
                if event.key == pygame.K_1:
                    val = 1
                if event.key == pygame.K_2:
                    val = 2   
                if event.key == pygame.K_3:
                    val = 3
                if event.key == pygame.K_4:
                    val = 4
                if event.key == pygame.K_5:
                    val = 5
                if event.key == pygame.K_6:
                    val = 6
                if event.key == pygame.K_7:
                    val = 7
                if event.key == pygame.K_8:
                    val = 8
                if event.key == pygame.K_9:
                    val = 9 
                if event.key == pygame.K_RETURN:
                    flag2 = 1  
                # If R pressed clear the sudoku board
                if event.key == pygame.K_r:
                    rs = 0
                    error = 0
                    flag2 = 0
                    grid =[
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0],
                    [0, 0, 0, 0, 0, 0, 0, 0, 0]
                    ]
                # If D is pressed reset the board to default
                if event.key == pygame.K_d:
                    rs = 0
                    error = 0
                    flag2 = 0
                    grid =[
                        [7, 8, 0, 4, 0, 0, 1, 2, 0],
                        [6, 0, 0, 0, 7, 5, 0, 0, 9],
                        [0, 0, 0, 6, 0, 1, 0, 7, 8],
                        [0, 0, 7, 0, 4, 0, 2, 6, 0],
                        [0, 0, 1, 0, 5, 0, 9, 3, 0],
                        [9, 0, 4, 0, 6, 0, 0, 0, 5],
                        [0, 7, 0, 3, 0, 0, 0, 1, 2],
                        [1, 2, 0, 0, 0, 7, 4, 0, 0],
                        [0, 4, 9, 2, 0, 6, 0, 0, 7]
                    ]
        if flag2 == 1:
            if solve(grid, 0, 0)== False:
                error = 1
            else:
                rs = 1
            flag2 = 0   
        if val != 0:           
            draw_val(val)
            # print(x)
            # print(y)
            if valid(grid, int(x), int(y), val)== True:
                grid[int(x)][int(y)]= val
                flag1 = 0
            else:
                grid[int(x)][int(y)]= 0
                raise_error2()  
            val = 0   
           
        if error == 1:
            raise_error1() 
        if rs == 1:
            result()       
        draw() 
        if flag1 == 1:
            draw_box()      
        instruction()   
     
        # Update window
        pygame.display.update() 
     
    # Quit pygame window   
    pygame.quit()    
Siddhartha Mukherjee
  • 2,703
  • 2
  • 24
  • 29
-2

First, randomly create a completed sudoku solution. This part require to have a sudoku solver.

From the sudoku solution, constantly remove numbers at random locations. For each removal, check if the sudoku is still valid. That is, the sudoku has a unique solution. This part needs to find out if there is more than one solution. It is another version of sudoku solver.

If not, we put back the number and try another location. The process keeps going until all locations have tried.

import random
import numpy as np

def PossibleValueAtPosition(pz:[], row:int, col:int):
    r=row//3*3
    c=col//3*3
    return {1,2,3,4,5,6,7,8,9}.difference(set(pz[r:r+3,c:c+3].flat)).difference(set(pz[row,:])).difference(set(pz[:,col]))

def Solution_Count(pz:[], n:int, Nof_solution:int):
    if Nof_solution>1:
        return Nof_solution
    if n>=81:
        Nof_solution+=1
        return Nof_solution
    (row,col) = divmod(n,9)
    if pz[row][col]>0:
        Nof_solution = Solution_Count(pz, n+1, Nof_solution)
    else:
        l = PossibleValueAtPosition(pz, row,col)
        for v in l:
            pz[row][col] = v
            Nof_solution = Solution_Count(pz, n+1, Nof_solution)
            pz[row][col] = 0
    return Nof_solution 

def SudokuSolver(pz:[], n:int):
    if n==81:
        return True
    (row,col) = divmod(n,9)
    if pz[row][col]>0:
        if SudokuSolver(pz, n+1):
            return True
    else:
        l = list(PossibleValueAtPosition(pz, row,col))
        random.shuffle(l)
        for v in l:
            pz[row][col] = v
            if SudokuSolver(pz, n+1):
                return True
            pz[row][col] = 0
    return False

def DigHoles(pz:[], randomlist:[], n:int, nof_holes:int):
    if n>=81 or nof_holes>=64:
        return
    (row,col) = divmod(randomlist[n],9)
    if pz[row][col]>0:
        pz_check=pz.copy()
        pz_check[row][col]=0
        Nof_solution = Solution_Count(pz_check, 0, 0)
        if Nof_solution==1:
            pz[row][col]=0
            nof_holes+=1
            print(pz)
            print("{} zeros".format(nof_holes))
            print()
    DigHoles(pz, randomlist, n+1, nof_holes)

def main():
    puzzle = np.zeros((9,9), dtype=int)
    SudokuSolver(puzzle, 0)
    print(puzzle, "--------- Answer\n")
    randomlist = list(range(81))
    random.shuffle(randomlist)
    DigHoles(puzzle, randomlist, 0, 0)

if __name__ == "__main__":
    main()