├── README.md └── sudoku.py /README.md: -------------------------------------------------------------------------------- 1 | # sudoku-solver 2 | -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | class Cell: 2 | """One individual cell on the Sudoku board""" 3 | 4 | def __init__(self, column_number, row_number, number, game): 5 | # Whether or not to include the cell in the backtracking 6 | self.solved = True if number > 0 else False 7 | self.number = number # the current value of the cell 8 | # Which numbers the cell could potentially be 9 | self.possibilities = set(range(1, 10)) if not self.solved else [] 10 | self.row = row_number # the index of the row the cell is in 11 | self.column = column_number # the index of the column the cell is in 12 | self.current_index = 0 # the index of the current possibility 13 | self.game = game # the sudoku game the cell belongs to 14 | if not self.solved: # runs the possibility checker 15 | self.find_possibilities() 16 | 17 | def check_area(self, area): 18 | """Checks to see if the cell's current value is a valid sudoku move""" 19 | values = [item for item in area if item != 0] 20 | return len(values) == len(set(values)) 21 | 22 | def set_number(self): 23 | """changes the number attribute and also changes the cell's value in the larger puzzle""" 24 | if not self.solved: 25 | self.number = self.possibilities[self.current_index] 26 | self.game.puzzle[self.row][self.column] = self.possibilities[self.current_index] 27 | 28 | def handle_one_possibility(self): 29 | """If the cell only has one possibility, set the cell to that value and mark it as solved""" 30 | if len(self.possibilities) == 1: 31 | self.solved = True 32 | self.set_number() 33 | 34 | def find_possibilities(self): 35 | """filter the possible values for the cell""" 36 | for item in self.game.get_row(self.row) + self.game.get_column(self.column) + self.game.get_box(self.row, self.column): 37 | if not isinstance(item, list) and item in self.possibilities: 38 | self.possibilities.remove(item) 39 | self.possibilities = list(self.possibilities) 40 | self.handle_one_possibility() 41 | 42 | def is_valid(self): 43 | """checks to see if the current number is valid in its row, column, and box""" 44 | for unit in [self.game.get_row(self.row), self.game.get_column(self.column), self.game.get_box(self.row, self.column)]: 45 | if not self.check_area(unit): 46 | return False 47 | return True 48 | 49 | def increment_value(self): 50 | """move number to the next possibility while the current number is invalid and there are possibilities left""" 51 | while not self.is_valid() and self.current_index < len(self.possibilities) - 1: 52 | self.current_index += 1 53 | self.set_number() 54 | 55 | 56 | class SudokuSolver: 57 | """contains logic for solving a sudoku puzzle -- even very difficult ones using a backtracking algorithm""" 58 | 59 | def __init__(self, puzzle): 60 | self.puzzle = puzzle # the 2d list of spots on the board 61 | self.solve_puzzle = [] # 1d list of the Cell objects 62 | # the size of the boxes within the puzzle -- 3 for a typical puzzle 63 | self.box_size = int(len(self.puzzle) ** .5) 64 | self.backtrack_coord = 0 # what index the backtracking is currently at 65 | 66 | def get_row(self, row_number): 67 | """Get the full row from the puzzle based on the row index""" 68 | return self.puzzle[row_number] 69 | 70 | def get_column(self, column_number): 71 | """Get the full column""" 72 | return [row[column_number] for row in self.puzzle] 73 | 74 | def find_box_start(self, coordinate): 75 | """Get the start coordinate for the small sudoku box""" 76 | return coordinate // self.box_size * self.box_size 77 | 78 | def get_box_coordinates(self, row_number, column_number): 79 | """Get the numbers of the small sudoku box""" 80 | return self.find_box_start(column_number), self.find_box_start(row_number) 81 | 82 | def get_box(self, row_number, column_number): 83 | """Get the small sudoku box for an x and y coordinate""" 84 | start_y, start_x = self.get_box_coordinates(row_number, column_number) 85 | box = [] 86 | for i in range(start_x, self.box_size + start_x): 87 | box.extend(self.puzzle[i][start_y:start_y+self.box_size]) 88 | return box 89 | 90 | def initialize_board(self): 91 | """create the Cells for each item in the puzzle and get its possibilities""" 92 | for row_number, row in enumerate(self.puzzle): 93 | for column_number, item in enumerate(row): 94 | self.solve_puzzle.append( 95 | Cell(column_number, row_number, item, self)) 96 | 97 | def move_forward(self): 98 | """Move forwards to the next cell""" 99 | while self.backtrack_coord < len(self.solve_puzzle) - 1 and self.solve_puzzle[self.backtrack_coord].solved: 100 | self.backtrack_coord += 1 101 | 102 | def backtrack(self): 103 | """Move forwards to the next cell""" 104 | self.backtrack_coord -= 1 105 | while self.solve_puzzle[self.backtrack_coord].solved: 106 | self.backtrack_coord -= 1 107 | 108 | def set_cell(self): 109 | """Set the current cell to work on""" 110 | cell = self.solve_puzzle[self.backtrack_coord] 111 | cell.set_number() 112 | return cell 113 | 114 | def reset_cell(self, cell): 115 | """set a cell back to zero""" 116 | cell.current_index = 0 117 | cell.number = 0 118 | self.puzzle[cell.row][cell.column] = 0 119 | 120 | def decrement_cell(self, cell): 121 | """runs the backtracking algorithm""" 122 | while cell.current_index == len(cell.possibilities) - 1: 123 | self.reset_cell(cell) 124 | self.backtrack() 125 | cell = self.solve_puzzle[self.backtrack_coord] 126 | cell.current_index += 1 127 | 128 | def change_cells(self, cell): 129 | """move forwards or backwards based on the validity of a cell""" 130 | if cell.is_valid(): 131 | self.backtrack_coord += 1 132 | else: 133 | self.decrement_cell(cell) 134 | 135 | def solve(self): 136 | """run the other functions necessary for solving the sudoku puzzle""" 137 | self.move_forward() 138 | cell = self.set_cell() 139 | cell.increment_value() 140 | self.change_cells(cell) 141 | 142 | def run_solve(self): 143 | """runs the solver until we are at the end of the puzzle""" 144 | while self.backtrack_coord <= len(self.solve_puzzle) - 1: 145 | self.solve() 146 | 147 | 148 | def solve(puzzle): 149 | solver = SudokuSolver(puzzle) 150 | solver.initialize_board() 151 | solver.run_solve() 152 | return solver.puzzle 153 | 154 | 155 | puzzle = [[5, 3, 0, 0, 7, 0, 0, 0, 0], 156 | [6, 0, 0, 1, 9, 5, 0, 0, 0], 157 | [0, 9, 8, 0, 0, 0, 0, 6, 0], 158 | [8, 0, 0, 0, 6, 0, 0, 0, 3], 159 | [4, 0, 0, 8, 0, 3, 0, 0, 1], 160 | [7, 0, 0, 0, 2, 0, 0, 0, 6], 161 | [0, 6, 0, 0, 0, 0, 2, 8, 0], 162 | [0, 0, 0, 4, 1, 9, 0, 0, 5], 163 | [0, 0, 0, 0, 8, 0, 0, 7, 9]] 164 | 165 | print(solve(puzzle)) 166 | 167 | solution = [[5, 3, 4, 6, 7, 8, 9, 1, 2], 168 | [6, 7, 2, 1, 9, 5, 3, 4, 8], 169 | [1, 9, 8, 3, 4, 2, 5, 6, 7], 170 | [8, 5, 9, 7, 6, 1, 4, 2, 3], 171 | [4, 2, 6, 8, 5, 3, 7, 9, 1], 172 | [7, 1, 3, 9, 2, 4, 8, 5, 6], 173 | [9, 6, 1, 5, 3, 7, 2, 8, 4], 174 | [2, 8, 7, 4, 1, 9, 6, 3, 5], 175 | [3, 4, 5, 2, 8, 6, 1, 7, 9]] 176 | --------------------------------------------------------------------------------