Sudoku Solver Using Python

Create a Sudoku Solver Using Python

Create a Sudoku Solver Using Python

Hello coder, welcome to the codewithrandom blog. In this blog, we will Create a Sudoku Solver Using Python. We will be using the backtracking algorithm to solve the puzzle. The backtracking algorithm is a brute-force algorithm that works by trying out all possible solutions and backtracking when it reaches a dead end.

What is Sudoku Solver ?🤔🤔

Sudoku is a logic-based puzzle game that involves filling a 9×9 grid with numbers from 1 to 9, with each row, column, and 3×3 subgrid containing each number only once. The game starts with a partially filled grid, and the objective is to fill the remaining empty cells with the correct numbers.

Sudoku Solver

Now, let’s get started on building our Sudoku Solver using Python.

step-by-step guide on how to build a Sudoku Solver using Python:

step 1: open any python code Editor.

step 2: Required Modules.

For this Sudoku Solver project, we need to install Tkinter  Gui. You can install these packages in your terminal.

$ pip install tk

step 3: Copy the code for Sudoku Solver using Python, which I provided Below in this article, and save it in a file named “main.py” (or any other name you prefer).

step 4: Run this  python file main.py to start the Sudoku Solver.

Create a Sudoku Solver Using Python

Complete Source Code For the Sudoku Solver (copy the code and run )👇👇👇

import tkinter as tk
from tkinter import font
# from time import sleep
import random

count = 0

class Sudoku:
    #Canvas background
    canvas_bg = "#fafafa" #impure white
    #Grid lines
    line_normal = "#4f4f4f" #dark grey
    line_thick = "#000000" #pure black
    #cell highlight box
    hbox_green = "#15fa00" #light green
    hbox_red = "#d61111" #red

    def __init__(self, master):
        #A record of all cells and their attributes
        self.grid = {}
        #A small edit window which will be initilized and displayed on click
        self.e = None
        self.canvas_width = 300
        self.canvas_height = 300
        #The sudoku grid
        self.canvas = tk.Canvas(master,bg=self.canvas_bg, width=self.canvas_width, height=self.canvas_height)
        self.t = tk.Entry(self.canvas)
        self.t.bind("<KeyRelease>",self.keyPressed)
        self.canvas.grid(columnspan=3)
        self.canvas.bind("<Button 1>",self.click)
        #Solve button
        self.btn_solve = tk.Button(master,text='Solve', command=self.wrapper, width=8)
        self.btn_solve.grid(row=1, padx=5, pady=5)
        #Generate button
        self.btn_gen = tk.Button(master,text='Generate', command=self.Generate, width=8)
        self.btn_gen.grid(row=1, column=1, padx=5, pady=5, sticky=tk.E)
        #Difficulty selector
        self.set_difficulty = tk.IntVar(master,1)
        self.difficulty_selector = tk.OptionMenu(master,self.set_difficulty,1,2,3,4,5)
        self.difficulty_selector.grid(row=1, column=2, pady=5, sticky=tk.W)
        #Individual cell width and height
        self.cell_width = self.canvas_width/9
        self.cell_height = self.canvas_height/9
        #Draw vertical lines
        for x in range(1,9):
            width=1
            fill=self.line_normal
            if(x%3==0):
                #Draw thicker black lines for seperating 3x3 boxes
                width=2
                fill=self.line_thick
            else:
                #Draw normal thin dark-grey lines
                width=1
                fill=self.line_normal
            self.canvas.create_line(self.cell_width*x, 0, self.cell_width*x, self.canvas_height, width=width, fill=fill)
        #Draw horizontal lines in the same way
        for y in range(1,9):
            width=1
            fill=self.line_normal
            if(y%3==0):
                width=2
                fill=self.line_thick
            else:
                width=1
                fill=self.line_normal
            self.canvas.create_line(0, self.cell_height*y, self.canvas_width, self.cell_height*y, width=width, fill=fill)
    
    def click(self, eventorigin):
        x = eventorigin.x
        y = eventorigin.y
        #Calcilate top-left x,y coords of cell clicked by mouse
        rect_x = int(x/self.cell_width)*self.cell_width
        rect_y = int(y/self.cell_height)*self.cell_height
        #Coords for drawing a square to highlight clicked cell
        coords = [rect_x,rect_y,rect_x+self.cell_width,rect_y,rect_x+self.cell_width,rect_y+self.cell_height,rect_x,rect_y+self.cell_height]
        # For some stupid reason, this line below didn't work as expected. So I had to choose the hard way.
        # h_box = self.canvas.create_rectangle(rect_x, rect_y, self.cell_width, self.cell_height, outline="#15fa00", width=3)
        #Get cell info
        editable = self.getCell(x/self.cell_width,y/self.cell_height)[1]
        if editable:
            #It's a cell you can edit
            #Show a green box highlight and edit
            h_box = self.canvas.create_polygon(coords, outline=self.hbox_green, fill='', width=3)
            self.edit(rect_x, rect_y)
        else:
            #It's a cell containing a clue number, cannot edit
            #Show a red box highlight
            h_box = self.canvas.create_polygon(coords, outline=self.hbox_red, fill='', width=3)
        self.canvas.after(200,lambda : self.canvas.delete(h_box))

    def edit(self,cordx:int,cordy:int):
        #Create a entry inside a small canvas window
        #make sure it's actuall initilized before deleting it
        if self.e is None:
            #Not initilized, else block skipped
            pass
        else:
            #Canvas window initilized, delete and reset it to current position
            self.canvas.delete(self.e)
        #Create a mini edit window that just fits the cell    
        self.e = self.canvas.create_window(cordx+1,cordy+1,window=self.t,width=self.cell_width-1,height=self.cell_height-2,anchor=tk.NW)
        #Clean up
        self.t.delete(0,tk.END)
        self.t.focus_set()
        
    def keyPressed(self, event):
        val = self.t.get().strip()
        try:
            #If input is a number between 1-9, this won't raise any errors
            val = int(val)
            if(val>9 or val<0):
                raise ValueError
        except ValueError:
            print("Invalid input!")
            self.t.delete(0,tk.END)
        else:
            #Get x,y coords of edit window and calculate cell row,column values
            x,y = (self.t.winfo_x())/self.cell_width,(self.t.winfo_y())/self.cell_height
            #Update cell with new value
            self.updateCell(val,x,y)
            self.canvas.delete(self.e)

    def updateCell(self,value,x,y,editable=True):
        #Get cell information stored in dict self.grid
        t = self.getCell(x,y)
        #Update values
        t[0] = value
        t[1] = editable
        text=value
        if value==0:
            text=' '
        #Update display value by using item id
        self.canvas.itemconfigure(t[2],text=text)
        self.canvas.update()
        #Update the dict
        self.grid[(x,y)] = t

    def getCell(self, x:int, y:int):
        #Returns info of cell at 'x' row 'y' column
        x=int(x)
        y=int(y)
        val = self.grid[(x,y)]
        return val

    def populate(self, X:[[]]):
        #Populates the sudoku grid with given 9x9 matrix and also store it in a dict
        c = self.canvas
        #The bookeeping is managed as shown below
        '''Dict->(X,Y) : [value,True/Flase,id]
                   ^        ^        ^     ^
              X,Y coords  value  editable  object id'''
        for i in range(9):
            for j in range(9):
                #Calculate x,y position of center of cell
                text_x = j*self.cell_width+self.cell_width/2
                text_y = i*self.cell_height+self.cell_height/2
                val = X[i][j]
                if val == 0:
                    t = c.create_text(text_x,text_y,text=' ',font=('Times', 14))
                    self.grid[(j,i)] = [ val, True, t]
                else:
                    t = c.create_text(text_x,text_y,text=val,font=('Times', 15, 'bold'))
                    self.grid[(j,i)] = [ val, False, t]

    def clearGrid(self):
        #Utility function to clear the grid, this will also wipe out the puzzle from memory
        for i in range(9):
            for j in range(9):
                self.updateCell(0,i,j)

    def getValue(self, row:int, col:int):
        #Return value at row, column
        return self.grid[(row,col)][0]

    def printGrid(self):
        #Utility function to print the grid
        for i in range(9):
            x=[]
            for j in range(9):
                x.append(self.getValue(j,i))
            print(x)

    def wrapper(self):
        #A small wrapper funtion that performs some small tasks before solving
        global count
        #Reset the count
        count = 0
        #Delete edit boxes if any
        self.canvas.delete(self.e)
        #Lock the buttons and start solving
        self.btn_gen.configure(state='disabled')
        self.btn_solve.configure(state='disabled')
        self.solve()
        #After solving, set the buttons back to normal
        self.btn_gen.configure(state='normal')
        self.btn_solve.configure(state='normal')

    def solve(self):
        global count
        #Start by finding an empty cell
        x,y = self.findEmpty()
        #If no cells are empty, our job is done
        if (x,y)==(None,None):
            #Print the no. of times solve() was called
            print("Recursed", count, "times.")
            return True
        
        #Keep a track of the number of function calls
        count+=1
        
        #Try putting in numbers from 1-9
        for i in range(1,10):
            #Check if number will satisfy sub-grid rule and row-column rule
            if self.is_SubGrid_Safe(i,x,y) and self.is_Cell_Safe(i,x,y):
                #Yes, then update the cell
                self.updateCell(i,x,y,False)
                # self.canvas.after(10,self.updateCell(i,x,y))
                #Now repeat for remaining cells
                nxt = self.solve()
                if nxt:
                    #All went well, so return true
                    return True
                else:
                    #The value chose earlier is wrong, so backtrack
                    self.updateCell(0,x,y,True)
                    
        #Cannot find any number, so return false (backtrack)
        return False
    
    def Generate(self, level=1):
        #Disable the buttons
        self.btn_solve.configure(state='disabled')
        self.btn_gen.configure(state='disabled')
        #Generate random puzzle with difficulty level 'level'
        #Start by generate diagonal sub-grids with randomly shuffled nos from 1-9
        nos = list(range(1,10))
        rand_grid = []
        for i in range(9):
            if i%3==0:
                random.shuffle(nos)
            t=[0]*9
            for j in range(3):
                t_pos = int(i/3)*3+j
                n_pos = (i%3)*3
                t[t_pos] = nos[n_pos+j]
            rand_grid.append(t)
        #Clean up
        self.clearGrid()
        #Cover up the window with a label 
        cover_label = tk.Label(text="GENERATING",font=('Arial',16))
        cover = self.canvas.create_window(0,0,window=cover_label,width=self.canvas_width,height=self.canvas_height,anchor=tk.NW)
        #Populate with the diagonal sub-grids
        self.populate(rand_grid)
        #Solve to get the completed puzzle
        self.solve()
        global count
        #Reset count
        count = 0
        #Remove random numbers based on set difficulty level, this needs work. Any Math wizards around?
        level = self.set_difficulty.get()
        if level<=2: level+=2
        for i in range(9):
            for j in range(9):
                remove = level>random.randint(1,5)
                if remove:
                    self.updateCell(0,i,j)
        
        #Set the fonts right
        g=self.grid
        for i in g.keys():
            cell = g[i]
            if cell[1]:
                #Editable cells have Times-14-regular font
                self.canvas.itemconfigure(cell[2],font=('Times',14))
            else:
                #Non-editable cells have Times-15-bold font
                self.canvas.itemconfigure(cell[2],font=('Times',15,'bold'))
        #Finally, lift the cover for the user to see the puzzle
        self.canvas.delete(cover)
        #and set the buttons back to normal
        self.btn_solve.configure(state='normal')
        self.btn_gen.configure(state='normal')
    
    def findEmpty(self):
        #Utility function to find an empty cell
        for i in range(9):
            for j in range(9):
                cell_val = self.getCell(j,i)[1]
                if cell_val:
                    return (j,i)
        return (None,None)

    def is_SubGrid_Safe(self,val,x,y)->bool:
        #Checks if the sub-grid rule is satisfied for the number 'val' at given row 'x',column 'y'
        #Figure out the sub grid x,y
        sgrid_x = int(x/3)*3
        sgrid_y = int(y/3)*3
        #Search the sub-grid
        for i in range(sgrid_x,sgrid_x+3):
            for j in range(sgrid_y,sgrid_y+3):
                #Check only non-editable cells, ignore cells edited by user
                if val==self.getValue(i,j) and not self.getCell(i,j)[1]:
                    #This number already exists, rule violated
                    return False
        #No duplicate number found in sub-grid, rule intact
        return True

    def is_Cell_Safe(self,val,x,y)->bool:
        #Check if the number 'val' already exists in the row 'x' or column 'y'
        for i in range(9):
            #Check only non-editable cells, ignore cells edited by user
            if val==self.getValue(x,i) and not self.getCell(x,i)[1]:
                return False
            if val==self.getValue(i,y) and not self.getCell(i,y)[1]:
                return False
        #Row-column rule intact
        return True
       
  
####################################Sudoku Solver using Python
master = tk.Tk()
master.title("PyDoku")
master.resizable(False, False)
game=Sudoku(master)
ex1= [   
    [3, 0, 6, 5, 0, 8, 4, 0, 0], 
    [5, 2, 0, 0, 0, 0, 0, 0, 0], 
    [0, 8, 7, 0, 0, 0, 0, 3, 1], 
    [0, 0, 3, 0, 1, 0, 0, 8, 0], 
    [9, 0, 0, 8, 6, 3, 0, 0, 5], 
    [0, 5, 0, 0, 9, 0, 6, 0, 0], 
    [1, 3, 0, 0, 0, 0, 2, 5, 0], 
    [0, 0, 0, 0, 0, 0, 0, 7, 4], 
    [0, 0, 5, 2, 0, 6, 3, 0, 0]
    ]
#Here's an extreme puzzel, ref : https://www.sudokuwiki.org/Daily_Sudoku
ex2=[
    [0, 5, 0, 0, 0, 0, 0, 0, 0], 
    [3, 0, 8, 0, 7, 0, 2, 0, 0], 
    [0, 0, 9, 3, 0, 6, 8, 0, 0], 
    [0, 8, 0, 0, 0, 9, 5, 0, 0], 
    [9, 0, 0, 0, 0, 0, 0, 0, 1], 
    [0, 0, 3, 8, 0, 0, 0, 9, 0], 
    [0, 0, 6, 5, 0, 7, 3, 0, 0], 
    [0, 0, 1, 0, 4, 0, 6, 0, 7], 
    [0, 0, 0, 0, 0, 0, 0, 4, 0]
    ]

game.populate(ex1)
tk.mainloop()

Output for the Sudoku Solver 👇👇

 

conclusion

Hurray! You have successfully Build a Sudoku Solver using Python. creating a Sudoku Solver using Python is a great way to challenge your programming skills and create a useful tool for solving Sudoku puzzles. With this tutorial, you should now have a good understanding of how to build a basic Sudoku Solver using Python. You can further customize and improve the solver by adding additional features such as error checking, difficulty levels, and user interfaces.  Hope you enjoyed building Sudoku Solver using Python! Visit our homepage and you get lot’s of projects💝

#Sudoku Solver using Python

 



Leave a Reply