3
\$\begingroup\$

I have made a simple Sudoku solver in python and TKinter. However, I have used code from many different sources, so it is not neat. I can only write ugly code, so it is just about impossible for me to neaten it fully. I have made a start, but it is still bad. Here's what I have so far:


##  IMPORTS  ##

import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo

##  VARIABLES  ##

count = 0

##  METHODS  ##

def board_to_list(board):
    entryboard = [[],[],[],[],[],[],[],[],[]]
    for row in range(9):
        for item in range(9):
            try:
                if (board[row][item].get() == ""):
                    entryboard[row].append(-1)
                elif not(int(board[row][item].get()) in [1,2,3,4,5,6,7,8,9]):
                    raise ValueError
                else:
                    entryboard[row].append(int(board[row][item].get()))
            except:
                showinfo(message="Invalid sudoku")
                return False
    return entryboard
def find_next_empty(puzzle):
    for row in range(9):
        for column in range(9):
            if puzzle[row][column] == -1:
                return row, column
    return None, None
def is_valid(puzzle, guess, row, col):
    row_vals = puzzle[row]
    if guess in row_vals:
        return False
    col_vals = [puzzle[i][col] for i in range(9)]
    if guess in col_vals:
        return False
    row_start = (row // 3) * 3
    col_start = (col // 3) * 3
    for r in range(row_start, row_start + 3):
        for c in range(col_start, col_start + 3):
            if puzzle[r][c] == guess:
                return False
    return True
    
def solve_sudoku(puzzle):
    global count
    row, col = find_next_empty(puzzle)
    if row is None and col is None:
        return True
    for guess in range(1,10):
        count += 1
        if is_valid(puzzle, guess, row, col):
            puzzle[row][col] = guess
            if solve_sudoku(puzzle):
                return True
        puzzle[row][col] = -1
    return False
def is_impossible(puzzle):
    for i in range(9):
        row = {}
        column = {}
        block = {}
        row_cube = 3 * (i//3)
        column_cube = 3 * (i%3)
        for j in range(9):
            if puzzle[i][j]!= -1 and puzzle[i][j] in row:
                return False
            row[puzzle[i][j]] = 1
            if puzzle[j][i]!=-1 and puzzle[j][i] in column:
                return False
            column[puzzle[j][i]] = 1
            rc= row_cube+j//3
            cc = column_cube + j%3
            if puzzle[rc][cc] in block and puzzle[rc][cc]!=-1:
                return False
            block[puzzle[rc][cc]]=1
    return True
def handle_solve_click(event):
    global count
    count = 0
    entryboard = board_to_list(board)
    if not entryboard:
        return False
    if not(is_impossible(entryboard)):
        showinfo(message="Invalid sudoku")
        return False
    solve_sudoku(entryboard)
    time = count/5
    while time > 10000:
        time -= 1000
        print(time)
    pb.start(round(time/100))
    window.after(round(time), show_solution, entryboard)
    window.after(10, update_progress_bar)
def show_solution(entryboard):
    count = 0
    for row in range(9):
        for item in range(9):
            board[row][item].delete(0, tk.END)
            board[row][item].insert(0, entryboard[row][item])
    print("+" + "---+"*9)
    for i, row in enumerate(entryboard):
        print(("|" + " {}   {}   {} |"*3).format(*[x if x != -1 else " " for x in row]))
        if i % 3 == 2:
            print("+" + "---+"*9)
        else:
            print("+" + "   +"*9)
    pb.stop()
    pb['value'] = 100
def handle_clear_click(event):
    for row in range(9):
        for item in range(9):
            board[row][item].delete(0, tk.END)
    pb['value'] = 0
    progress['text'] = "0.0%"
def handle_hint_click(event):
    entryboard = board_to_list(board)
    otherboard = board_to_list(board)
    if not(entryboard):
        return False
    if not(is_impossible(entryboard)):
        showinfo(message="Impossible")
        return False
    solve_sudoku(entryboard)
    for row in range(9):
        for item in range(9):
            if otherboard[row][item] != entryboard[row][item]:
                board[row][item].delete(0, tk.END)
                board[row][item].insert(0, entryboard[row][item])
                return True
    showinfo(message="Already solved")
    
def update_progress_bar():
    if pb['value'] < 100:
        progress['text'] =  f"{pb['value']}%"
        window.after(10, update_progress_bar)
    else:
        pb['value'] = 100
        progress['text'] = "Complete"

##  MAIN LOOP  ##

if __name__ == "__main__":
    window = tk.Tk()
    board = [[],[],[],[],[],[],[],[],[]]
    entryboard = [[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,]]
    sudoku_frame = tk.Frame(relief=tk.SUNKEN, borderwidth=5)
    for row in range(9):
        for item in range(9):
            myentry = tk.Entry(master=sudoku_frame, width=1)
            row_start = (row // 3)
            col_start = (item // 3)
            rowpos = row_start + row
            colpos = col_start + item + 1
            myentry.grid(row=rowpos, column=colpos)
            board[row].append(myentry)
    sudoku_frame.pack()
    progress_frame = tk.Frame()
    pb = ttk.Progressbar(master=progress_frame, orient='horizontal', mode='determinate', length=280)
    pb.grid(row=0, column=0)
    progress = tk.Label(master=progress_frame, text="0.0%")
    progress.grid(row=1, column=0)
    progress_frame.pack(pady=15)
    button_frame = tk.Frame(relief=tk.RIDGE, borderwidth=5)
    solve_btn = tk.Button(master=button_frame, text="Solve", relief=tk.FLAT, borderwidth=2)
    solve_btn.bind("<Button-1>", handle_solve_click)
    solve_btn.grid(row=0,column=0)
    clear_btn = tk.Button(master=button_frame, text="Clear", relief=tk.FLAT, borderwidth=2)
    clear_btn.bind("<Button-1>", handle_clear_click)
    clear_btn.grid(row=0, column=1)
    hint_btn = tk.Button(master=button_frame, text="Hint", relief=tk.FLAT, borderwidth=2)
    hint_btn.bind("<Button-1>", handle_hint_click)
    hint_btn.grid(row=0, column=3)
    button_frame.pack()
    window.mainloop()
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

First, your program works. So that's a great place to start.

Some cosmetic comments:

  1. Put blank lines between your functions. This helps people reading your code to know when something new is happening.
  2. Put spaces between operators like +, -, ==, !=, etc. to make the lines easier to read.
  3. The sudoku boxes in the GUI window are rather narrow and difficult to click on and enter numbers. Making the boxes wider and the text entry centered looks better: myentry = tk.Entry(master=sudoku_frame, width=3, justify='center')

Initializing repetitive structures

In several places, you initialize sudoku data with literal lists and lists of lists like these:

    board = [[],[],[],[],[],[],[],[],[]]
    entryboard = [[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,],[-1,-1,-1,-1,-1,-1,-1,-1,-1,]]

You can use list comprehension to create these structures. The resulting code is shorter and easier to read.

    board = [[] for _ in range(9)]
    entryboard = [[-1 for _ in range(9)] for _ in range(9)]

The variable _ is often used as a dummy variable where we don't care about its value. We just need it to store the current value out of range(9).

Similar changes can be made to entryboard in board_to_list(). In that same function, elif not(int(board[row][item].get()) in [1,2,3,4,5,6,7,8,9]): can be written elif not(int(board[row][item].get()) in range(1, 10):

Initializing the entry grid

I don't understand the math being done in the loop creating all the tk.Entry widgets. This seems to work just as well.

    for row in range(9):
        for item in range(9):
            myentry = tk.Entry(master=sudoku_frame, width=3, justify='center')
            myentry.grid(row=row, column=item)
            board[row].append(myentry)

Global variables

I'm not talking about count at the top. That variable is modified in several functions and is declared global in those functions. That's fine and suits its purpose.

I'm mostly talking about the board variable that is created on the second line of the if __name__ == "__main__": block. This variable holds all of the text entry widgets that are used to both allow the user to enter numbers and display numbers when the GUI solves a cell. In almost all functions, the board variable is the one created at the bottom of the code. There's no indication where the board variable is defined when looking at the function. I understand that the bind() method that binds the functions to button clicks only take an Event as its only argument, but there's a way to sneak the board and any other variables you need into the callback.

As an example, let's look at the handle_clear_click() function. First, change the parameters of the called function to take a board argument.

def handle_clear_click(event, board, pb, progress):
    for row in range(9):
        for item in range(9):
            board[row][item].delete(0, tk.END)
    pb['value'] = 0
    progress['text'] = "0.0%"

Then, instead of directly passing just the function to the bind() call, create a lambda expression that provides the parameters that the bind() call cannot.

clear_btn.bind("<Button-1>", lambda event: handle_clear_click(event, board, pb, progress))

Now, there's no ambiguity in where the variables board, pb, and progress come from. Plus, this allows for easier changes later on. What if you want to allow work on more than one sudoku board in the same GUI? While that may not be likely for this program, other programs you write will have to juggle multiple sets of data.

Do the same thing for the other two buttons: Solve and Hint.

Unused data

The entryboard variable in the if __name__ == "__main__": block is not used anywhere, so you can delete it. The line count = 0 in show_solution() either needs to be deleted or have global put before it to reset the global count variable.

Several functions' return values that are not used. For example, handle_solve_click() returns False when the puzzle cannot be solved and nothing (implicitly None) otherwise. These return values don't do anything useful, so a simple return statement with nothing after it is fine.

def handle_solve_click(event):
    global count
    count = 0
    entryboard = board_to_list(board)
    if not entryboard:
        return
    if is_impossible(entryboard):
        showinfo(message="Invalid sudoku")
        return
    solve_sudoku(entryboard)
    time = count/5
    while time > 10000:
        time -= 1000
        print(time)
    pb.start(round(time/100))
    window.after(round(time), show_solution, entryboard)
    window.after(10, update_progress_bar)

Semantic bug

The function is_impossible() returns False when the puzzle is impossible to solve. This is the reverse of what I would expect given the name. I would switch True and False in all the return values of this function and fix the handle_hint_click() and handle_solve_click() functions so that the lines if not(is_impossible(entryboard)): do not have a not. The other choice is to rename the function is_possible().

\$\endgroup\$

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.