2
\$\begingroup\$

I wrote this program for a Sudoku solver in Python. It utilizes tkinter GUI. I've uploaded the project to my GitHub.

Please can you have a look and let me know of any ideas or improvements you think I should make?

from tkinter import Frame,Entry,Button,messagebox
class app(Frame):
 def __init__(self): #Initialising
 Frame.__init__(self)
 self.grid()
 self.create_grid() #Creating the sudoku grid
 self.create_buttons() #Creating the reset and solve buttons
 def create_grid(self):
 self.cells = {} #Creating a dict for the cells in the grid
 self.tableheight = 9
 self.tablewidth = 9
 counter = 0
 for row in range(self.tableheight): #Iterating through the rows and columns creating entries
 for col in range(self.tablewidth):
 self.cells[counter] = Entry(self,width=5,justify='center') #Creating the Entry
 if (counter % 3==2): #Creating extra vertical breaks to split into 3x3 boxes
 pad_x = (3,10)
 else:
 pad_x = 3
 if ((counter // 9) == 2) or ((counter // 9) == 5): #Creating extra horizontal breaks to split into 3x3 boxes
 pad_y = (3,10)
 else:
 pad_y = 3
 if (counter % 9) == 0: #Extra space to the sides of the board
 pad_x = (25,3)
 elif (counter % 9) == 8:
 pad_x = (3,25)
 if (counter // 9) == 0:
 pad_y = (25,3)
 elif (counter // 9) == 8:
 pad_y = (3,25)
 self.cells[counter].grid(row=row,column=col,padx=pad_x,pady=pad_y) #Setting in a grid and adding padding
 counter+=1
 self.master.bind('<Up>',self.up) #Key bindings to move between cells
 self.master.bind('<Left>',self.left)
 self.master.bind('<Right>',self.right)
 self.master.bind('<Down>',self.down)
 def up(self,event):
 current_cell = str(self.focus_get())[12:] #Splitting the string of default entry names to get the number
 if len(current_cell) == 0: #Naming started with no number at the end so setting to 1
 current_cell = '1'
 current_cell = int(current_cell) - 1 #Getting the current cell index
 row = current_cell//9 #Finding the row the cell is on
 if row == 0: #Getting new row
 new_row = 8
 else:
 new_row = row -1
 next_cell = new_row*9 + (current_cell % 9) #Getting index of target cell 
 self.cells[next_cell].focus_set() #Setting focus to new entry 
 def left(self,event):
 current_cell = str(self.focus_get())[12:] #Splitting the string of default entry names to get the number
 if len(current_cell) == 0: #Naming started with no number at the end so setting to 1
 current_cell = '1'
 current_cell = int(current_cell) - 1 #Getting the current cell index
 if (current_cell == 0): #Index of new entry
 next_cell = 80
 else:
 next_cell = current_cell - 1
 self.cells[next_cell].focus_set() #Setting focus to new entry 
 def right(self,event):
 current_cell = str(self.focus_get())[12:] #Splitting the string of default entry names to get the number
 if len(current_cell) == 0: #Naming started with no number at the end so setting to 1
 current_cell = '1'
 current_cell = int(current_cell) - 1 #Getting the current cell index
 if (current_cell == 80): #Index of new entry
 next_cell = 0
 else:
 next_cell = current_cell + 1 
 self.cells[next_cell].focus_set() #Setting focus to new entry 
 def down(self,event):
 current_cell = str(self.focus_get())[12:] #Splitting the string of default entry names to get the number
 if len(current_cell) == 0: #Naming started with no number at the end so setting to 1
 current_cell = '1'
 current_cell = int(current_cell) - 1 #Getting the current cell index
 row = current_cell//9 #Finding the row the cell is on
 if row == 8: #Getting new row
 new_row = 0
 else:
 new_row = row + 1
 next_cell = new_row*9 + (current_cell % 9) #Getting index of target cell 
 self.cells[next_cell].focus_set() #Setting focus to new entry 
 def create_buttons(self): #Creating buttons
 self.reset = Button(self,text='Reset',command=self.reset_values) #Reset
 self.reset.grid(row=11,column=1) 
 self.solve = Button(self,text='Solve',command=self.solve_sudoku) #Solve
 self.solve.grid(row = 11, column=7)
 def reset_values(self): #Reset entries
 for counter in range(81):
 self.cells[counter].delete(0,'end')
 self.cells[counter].configure(fg='black')
 def check_values(self): #Checks cell values are allowed
 self.fetch_values () #Fetching values
 self.cells_given = 0
 for x in self.cells_list: #Checking the number of cells intially filled is greater than 17 to allow for unique solution
 if x != 0:
 self.cells_given += 1
 if (self.cells_given < 17): #Less than 17 enteries results in non unique solution
 messagebox.showwarning('Non Unique Solution','Entering less that 17 initial cells means the solution can not be unique')
 self.different_values_given = len(list(set(self.cells_list))) #Getting length of the list of unique cell values given
 if self.different_values_given < 9: #Checking the number of different values given is greater than 9 (8 for unique sudoku but this also counts 0)
 messagebox.showwarning('Non Unique Solution','Entering less than 8 different intial values means the solution can not be unique.')
 self.convert_to_board() #Getting the list of cell values in a board format
 for row in range(9): #iterating through each cell
 for col in range(9):
 value = self.board[row][col]
 if value!=0:
 if (self.check_valid(row,col,value)): #Check for violations of sudoku rules at start
 print(row,col)
 messagebox.showerror('Entry Error','Initial board contains violation of Sudoku rules')
 def fetch_values(self): #Gets cell values from board
 self.cells_list = list(self.cells.values()) #Getting the cell values as a list
 for x in range(81):
 self.cells_list[x] = self.cells_list[x].get()
 if (self.cells_list[x] not in ['','0','1','2','3','4','5','6','7','8','9']): #Checking the cell values are allowed
 messagebox.showerror('Value Error','Please ensure all enteries are between 1-9.\nLeave empty cells blank.')
 self.reset_values()
 raise Sudoku_Error()
 if (self.cells_list[x] == ''): #Converting empty cells to 0s
 self.cells_list[x] = '0'
 self.cells[x].configure(fg='blue')
 self.cells_list[x] = int(self.cells_list[x]) #Converting cell values from strings to integers
 def convert_to_board(self): #Converting from a list length 81 to list of 9 lists length 9
 self.board = [] 
 for iteration in range(9): #Iterating through different rows
 row = []
 for pos in range(iteration*9,(iteration+1)*9): #Iterating through different columns in the set row
 row.append(self.cells_list[pos]) #Appending to the current rows list
 self.board.append(row) #Appending row list to the board
 def check_valid(self,row,col,value): #Checking if a test value is valid
 row_valid = all([value != x for x in self.board[row]])#grid[row][x] for x in range(9)]) #Check if value violates row condition
 col_valid = all([value != self.board[y][col] for y in range(9)]) #Check if value violates column condition
 if not(row_valid and col_valid): #Returning false unless value is valid for both row and column constraints
 return False
 box_start_row,box_start_col = 3*(row//3),3*(col//3) #Finding the position values for top left of the 3x3 box which the current test cell resides in
 for y in range(box_start_row,box_start_row + 3): #Checking if test value violates box condition
 for x in range(box_start_col,box_start_col + 3):
 if self.board[y][x] == value:
 return False
 return True
 def solve_sudoku(self): #Solving
 self.check_values() #Checking intitial 
 self.solve_initial() #Initial solving algorithm
 self.publish_answer() #Publishing results
 def solve_initial(self):
 repeat = False #Setting a variable to repeat if grid was changed this iteration
 for row in range(9):
 for col in range(9):
 if self.board[row][col] == 0:
 possible_values = [1,2,3,4,5,6,7,8,9] #Setting possible values before other cell's value contraints
 row_values = [self.board[row][x] for x in range(9)] #Getting values from the cells row
 col_values = [self.board[y][col] for y in range(9)] #Getting values from the cells column
 box_values = []
 box_start_row,box_start_col = 3*(row//3),3*(col//3) 
 for y in range(box_start_row,box_start_row + 3): #Getting values from the cells box
 for x in range(box_start_col,box_start_col + 3):
 box_values.append(self.board[y][x])
 restricted_values = row_values + col_values + box_values #Combining to get the total list of values not allowed
 restricted_values = list(set(restricted_values)) #Removing duplicates
 restricted_values.remove(0) #Have to remove 0 as that isnt a possible value for completed suduko
 for value in restricted_values: #Removing the restricted values from the possible value list
 possible_values.remove(value)
 if len(possible_values) == 1: #If only one possible value, set that value and set repeat to true
 self.board[row][col] = possible_values[0]
 repeat = True
 if repeat: #Repeating if repeat is true
 self.solve_initial() 
 if (self.solve_brute()): #If not repeating attempt brute force to check if solved and if not then finish
 return
 def solve_brute(self,row = 0,col = 0): #Solve function with inital params for row and col set
 row, col = self.next_cell(row,col)
 if (row == -1) and (col == -1): #If no more cells to solve, return completed grid
 return True
 for value in range(1,10):
 if self.check_valid(row,col,value):
 self.board[row][col] = value #Updating the cell to the new value if it is valid
 if self.solve_brute(row,col): 
 return True
 self.board[row][col] = 0 #Reset the current cell to unfilled for backtracking
 return False
 def next_cell(self,row,col): #Finding the next cell to fill
 for y in range(row,9): #Check grid from min row col values for unfilled cells (treating 0 as unfilled)
 for x in range(col,9):
 if self.board[y][x] == 0:
 return y,x
 for y in range(9): #Check entire grid for unfilled cells
 for x in range(9):
 if self.board[y][x] == 0:
 return y,x
 return -1,-1 #Returns -1,-1 if all cells filled
 def publish_answer(self):
 self.answer_cells = []
 for y in range(9):
 for x in range(9):
 self.answer_cells.append(self.board[y][x])
 for pos in range(81):
 if (self.cells_list[pos] == 0):
 self.cells[pos].insert(0,self.answer_cells[pos])
class Sudoku_Error(Exception):
 pass 
prog = app()
prog.master.title('Sudoku Solver')
prog.mainloop()
Ben A
10.7k5 gold badges37 silver badges101 bronze badges
asked Jan 5, 2020 at 21:01
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

I strongly feel that you should separate the logic of the graphics and the algorithm of solving the sudoku. The algorithm is just some operation defined on some data structure of your choosing (actually a good way to regard solving sudokus is as extending a colouring of a graph).

I would write this algorithm to act on whatever data structure is most convenient and makes the algorithm as evident as possible.

Then have functions to adapt between the algorithm representation and the board internal representation.

Then have functions which take you from the boards internal representation to the GUI representation.

answered Feb 18, 2020 at 23:02
\$\endgroup\$
1
\$\begingroup\$

Class Names

Classes should be in PascalCase, not lowercase. So your class should be App.

Operator Spacing

There should be a space before and after every operator (+-*/=, etc) in your program. It improves the readability of your code greatly.

Comments

When commenting, it's common to put them a line before, so the reader sees the comment then the preceding line of code that the comment addresses. Inline comments, especially when really long like yours, are unnecessary.

check_valid

You can greatly simplify how you check a valid sudoku board. Using any, it will return True if any of the element in the iterator are True. But in this case, since you want all False values to be checked, simply do not any(...). Take a look:

def check_valid(self,row,col,value):
 row_valid = all([value != x for x in self.board[row]])
 col_valid = all([value != self.board[y][col] for y in range(9)])
 if not(row_valid and col_valid):
 return False
 box_start_row, box_start_col = 3 * (row // 3), 3 * (col // 3)
 return not any(
 self.board[y][x] == value
 for y in range(box_start_row, box_start_row + 3)
 for x in range(box_start_col, box_start_col + 3)
 )

Contants

I see you've defined self.tableheight and self.tablewidth as constants (although they should be snake_case then UPPER_CASE). That's good. But you only use them once and never again! Then you have stray 9 values all over your code. You should utilize these class constants.

answered Feb 18, 2020 at 22:52
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.