├── .gitignore ├── LICENSE ├── README.md ├── ac3.py ├── backtrack.py ├── data └── 20sudokus.txt ├── heuristics.py ├── img └── result.png ├── solver.py ├── sudoku.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | output.txt 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # celery beat schedule file 87 | celerybeat-schedule 88 | 89 | # SageMath parsed files 90 | *.sage.py 91 | 92 | # Environments 93 | .env 94 | .venv 95 | env/ 96 | venv/ 97 | ENV/ 98 | env.bak/ 99 | venv.bak/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # mypy 112 | .mypy_cache/ 113 | .dmypy.json 114 | dmypy.json 115 | 116 | # Pyre type checker 117 | .pyre/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 G. Cosson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sudoku Solver 2 | ## Backtracking / AC-3 / MRV / Least Containing Value 3 | 4 | ## I] Requirements 5 | 6 | - [Git](https://git-scm.com/downloads) to download the source code 7 | 8 | - [python3](https://www.python.org/downloads/) to execute the code 9 | 10 | - [pip3](https://pypi.org/project/pip/) to install dependencies 11 | 12 | ## II] Installation 13 | 14 | Clone the project to your local machine: 15 | ```bash 16 | git clone https://github.com/stressGC/python-backtracking-CSP-sudoku-solver.git 17 | cd python-backtracking-CSP-sudoku-solver 18 | ``` 19 | 20 | You will need to install the following modules : 21 | ```bash 22 | pip3 install argparse itertool 23 | ``` 24 | 25 | ## III] Execution 26 | 27 | The following arguments are available: 28 | 29 | - **--help** to get some informations about the script 30 | ```bash 31 | python solver.py --help 32 | ``` 33 | - **--string** followed by a string representing one or more Sudokus. It will be parsed by the script. Accepted characters : [1,9], "#", "@", "X". A list of 20 sudokus can be found [data/20sudokus.txt](https://raw.githubusercontent.com/stressGC/python-backtracking-CSP-sudoku-solver/master/data/20sudokus.txt) . 34 | ```bash 35 | python solver.py --string 36 | ``` 37 | - **--level** followed by a level for the default sudoku. 38 | Accepted values : ["easy", "medium", "hard"], default : "medium". 39 | ```bash 40 | python solver.py --level 41 | ``` 42 | 43 | Note : **--string** and **--level** can't be specified at the same time as they are concurrent. 44 | 45 | ## IV] Result 46 | 47 | All the current computing steps will be printed in console for each one of the sudokus. 48 | 49 | ![Screenshot of a result](https://github.com/stressGC/python-backtracking-CSP-sudoku-solver/blob/master/img/result.png "Screenshot of a result") 50 | 51 | -------------------------------------------------------------------------------- /ac3.py: -------------------------------------------------------------------------------- 1 | from utils import is_different 2 | 3 | """ 4 | Constraint Propagation with AC-3 5 | pseudo code found @ https://en.wikipedia.org/wiki/AC-3_algorithm 6 | python implementation inspired by http://aima.cs.berkeley.edu/python/csp.html 7 | """ 8 | def AC3(csp, queue=None): 9 | 10 | if queue == None: 11 | queue = list(csp.binary_constraints) 12 | 13 | while queue: 14 | 15 | (xi, xj) = queue.pop(0) 16 | 17 | if remove_inconsistent_values(csp, xi, xj): 18 | 19 | # if a cell has 0 possibilities, sudoku has no solution 20 | if len(csp.possibilities[xi]) == 0: 21 | return False 22 | 23 | for Xk in csp.related_cells[xi]: 24 | if Xk != xi: 25 | queue.append((Xk, xi)) 26 | 27 | return True 28 | 29 | """ 30 | remove_inconsistent_values 31 | 32 | returns true if a value is removed 33 | """ 34 | def remove_inconsistent_values(csp, cell_i, cell_j): 35 | 36 | removed = False 37 | 38 | # for each possible value remaining for the cell_i cell 39 | for value in csp.possibilities[cell_i]: 40 | 41 | # if cell_i=value is in conflict with cell_j=poss for each possibility 42 | if not any([is_different(value, poss) for poss in csp.possibilities[cell_j]]): 43 | 44 | # then remove cell_i=value 45 | csp.possibilities[cell_i].remove(value) 46 | removed = True 47 | 48 | # returns true if a value has been removed 49 | return removed 50 | -------------------------------------------------------------------------------- /backtrack.py: -------------------------------------------------------------------------------- 1 | from heuristics import select_unassigned_variable, order_domain_values 2 | from utils import is_consistent, assign, unassign 3 | 4 | """ 5 | Backtracking Algorithm 6 | pseudo code found @ https://sandipanweb.files.wordpress.com/2017/03/im31.png 7 | """ 8 | def recursive_backtrack_algorithm(assignment, sudoku): 9 | 10 | # if assignment is complete then return assignment 11 | if len(assignment) == len(sudoku.cells): 12 | return assignment 13 | 14 | # var = select-unassigned-variables(csp) 15 | cell = select_unassigned_variable(assignment, sudoku) 16 | 17 | # for each value in order-domain-values(csp, var) 18 | for value in order_domain_values(sudoku, cell): 19 | 20 | # if value is consistent with assignment 21 | if is_consistent(sudoku, assignment, cell, value): 22 | 23 | # add {cell = value} to assignment 24 | assign(sudoku, cell, value, assignment) 25 | 26 | # result = backtrack(assignment, csp) 27 | result = recursive_backtrack_algorithm(assignment, sudoku) 28 | 29 | # if result is not a failure return result 30 | if result: 31 | return result 32 | 33 | # remove {cell = value} from assignment 34 | unassign(sudoku, cell, assignment) 35 | 36 | # return failure 37 | return False -------------------------------------------------------------------------------- /data/20sudokus.txt: -------------------------------------------------------------------------------- 1 | 003020600900305001001806400008102900700000008006708200002609500800203009005010300200080300060070084030500209000105408000000000402706000301007040720040060004010003000000907000420180000705026100904000050000040000507009920108000034059000507000000030050040008010500460000012070502080000603000040109030250000098001020600080060020020810740700003100090002805009040087400208003160030200302700060005600008076051090100920000524010000000000070050008102000000000402700090060000000000030945000071006043080250600000000000001094900004070000608000010200003820500000000000005034090710480006902002008001900370060840010200003704100001060049020085007700900600609200018000900002050123400030000160908000000070000090000000205091000050007439020400007000001900003900700160030005007050000009004302600200000070600100030042007006500006800000125400008400000420800000030000095060902010510000060000003049000007200001298000062340750100005600570000040000094800400000006005830000030000091006400007059083260300000000005009000200504000020000700160000058704310600000890100000067080000005437630000000000500008005674000000020000003401020000000345000007004080300902947100080000020040008035000000070602031046970200000000000501203049000730000000010800004000361025900080960010400000057008000471000603000259000800740000005020018060005470329050807020600010090702540006070020301504000908103080070900076205060090003080103040080005000000003457000070809060400903007010500408007020901020000842300000000100080003502900000040000106000305900251008070408030800763001308000104000020000005104800000000000009805100051907420290401065000000000140508093026709580005103600000000000 -------------------------------------------------------------------------------- /heuristics.py: -------------------------------------------------------------------------------- 1 | from utils import number_of_conflicts 2 | 3 | """ 4 | Most Constrained Variable (MRV) heuristic 5 | 6 | definitions & explanations @ https://www.cs.unc.edu/~lazebnik/fall10/lec08_csp2.pdf 7 | returns the variable with the fewest possible values remaining 8 | """ 9 | def select_unassigned_variable(assignment, sudoku): 10 | 11 | unassigned = [] 12 | 13 | # for each of the cells 14 | for cell in sudoku.cells: 15 | 16 | # if the cell is not in the assignment 17 | if cell not in assignment: 18 | 19 | # add it 20 | unassigned.append(cell) 21 | 22 | # the criterion here is the length of the possibilities (MRV) 23 | criterion = lambda cell: len(sudoku.possibilities[cell]) 24 | 25 | # we return the variable with the fewest possible values remaining 26 | return min(unassigned, key=criterion) 27 | 28 | """ 29 | Least Constraining Value (LCV) heuristic 30 | 31 | @ https://cs.stackexchange.com/questions/47870/what-is-least-constraining-value 32 | from "Artificial Intelligence: A Modern Approach (Russel & Norvig)"'s definition: 33 | prefers the value that rules out the fewest choices for the neighboring variables in the constraint graph. 34 | """ 35 | def order_domain_values(sudoku, cell): 36 | 37 | # since we are looking for the least constraining value 38 | # contained in [1, 2, 3, ..., 8, 9], smallest case possible is length of 1 39 | if len(sudoku.possibilities[cell]) == 1: 40 | return sudoku.possibilities[cell] 41 | 42 | # we want to sort based on the number of conflicts 43 | criterion = lambda value: number_of_conflicts(sudoku, cell, value) 44 | return sorted(sudoku.possibilities[cell], key=criterion) -------------------------------------------------------------------------------- /img/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stressGC/Python-AC3-Backtracking-CSP-Sudoku-Solver/cc0b30533aaf24bb60176ab87e0281ab96b1a117/img/result.png -------------------------------------------------------------------------------- /solver.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | from sudoku import Sudoku 4 | from ac3 import AC3 5 | from backtrack import recursive_backtrack_algorithm 6 | from utils import fetch_sudokus, print_grid 7 | 8 | """ 9 | default sudokus' grid 10 | """ 11 | sudokus = dict( 12 | easy = "000079065000003002005060093340050106000000000608020059950010600700600000820390000", 13 | medium = "102004070000902800009003004000240006000107000400068000200800700007501000080400109", 14 | hard = "002008050000040070480072000008000031600080005570000600000960048090020000030800900") 15 | 16 | """ 17 | solve 18 | solves a sudoku based on its String grid 19 | """ 20 | def solve(grid, index, total): 21 | 22 | print("\nSudoku {}/{} : \n{}".format(index, total, print_grid(grid))) 23 | 24 | 25 | print("{}/{} : AC3 starting".format(index, total)) 26 | 27 | 28 | # instanciate Sudoku 29 | sudoku = Sudoku(grid) 30 | 31 | # launch AC-3 algorithm of it 32 | AC3_result = AC3(sudoku) 33 | 34 | # Sudoku has no solution 35 | if not AC3_result: 36 | print("{}/{} : this sudoku has no solution".format(index, total)) 37 | 38 | else: 39 | 40 | # check if AC-3 algorithm has solve the Sudoku 41 | if sudoku.isFinished(): 42 | 43 | print("{}/{} : AC3 was enough to solve this sudoku !".format(index,total)) 44 | print("{}/{} : Result: \n{}".format(index, total, sudoku)) 45 | 46 | # continue the resolution 47 | else: 48 | 49 | print("{}/{} : AC3 finished, Backtracking starting...".format(index,total)) 50 | 51 | assignment = {} 52 | 53 | # for the already known values 54 | for cell in sudoku.cells: 55 | 56 | if len(sudoku.possibilities[cell]) == 1: 57 | assignment[cell] = sudoku.possibilities[cell][0] 58 | 59 | # start backtracking 60 | assignment = recursive_backtrack_algorithm(assignment, sudoku) 61 | 62 | # merge the computed values for the cells at one place 63 | for cell in sudoku.possibilities: 64 | sudoku.possibilities[cell] = assignment[cell] if len(cell) > 1 else sudoku.possibilities[cell] 65 | 66 | if assignment: 67 | print("{}/{} : Result: \n{}".format(index, total, sudoku)) 68 | 69 | else: 70 | print("{}/{} : No solution exists".format(index, total)) 71 | 72 | 73 | if __name__ == "__main__": 74 | 75 | # argument parsing using argparse module 76 | # doc @ https://docs.python.org/3/library/argparse.html 77 | parser = argparse.ArgumentParser(description='Solve a Sudoku with CSP') 78 | parser.add_argument('--string', type=str, help='sudoku as a String, can be multiple sudokus in a row in the same line') 79 | parser.add_argument('--level', type=str, default='medium', choices=['easy', 'medium', 'hard'], help='selects default sudoku\'s based on level (default: %(default)s)') 80 | args = parser.parse_args() 81 | 82 | sudoku_grid_as_string = "" 83 | 84 | # if user didnt provide a custom string or its string has not the right format 85 | if not args.string: 86 | 87 | # let's use the level's default 88 | sudoku_grid_as_string = sudokus[args.level] 89 | print("\nUsing default sudoku, level : {}".format(args.level)) 90 | 91 | else: 92 | 93 | sudoku_grid_as_string = args.string 94 | 95 | # fetch sudokus from user input 96 | sudoku_queue = fetch_sudokus(sudoku_grid_as_string) 97 | 98 | # for each sudoku, solve it ! 99 | for index, sudoku_grid in enumerate(sudoku_queue): 100 | solve(sudoku_grid, index + 1, len(sudoku_queue)) -------------------------------------------------------------------------------- /sudoku.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import sys 3 | 4 | # coordinates idea from @ https://github.com/speix/sudoku-solver/blob/master/sudoku.py 5 | rows = "123456789" 6 | cols = "ABCDEFGHI" 7 | 8 | class Sudoku: 9 | 10 | """ 11 | INITIALIZATION 12 | """ 13 | def __init__(self, grid): 14 | game = list(grid) 15 | 16 | # generation of all the coords of the grid 17 | self.cells = list() 18 | self.cells = self.generate_coords() 19 | 20 | # generation of all the possibilities for each one of these coords 21 | self.possibilities = dict() 22 | self.possibilities = self.generate_possibilities(grid) 23 | 24 | # generation of the line / row / square constraints 25 | rule_constraints = self.generate_rules_constraints() 26 | 27 | # convertion of these constraints to binary constraints 28 | self.binary_constraints = list() 29 | self.binary_constraints = self.generate_binary_constraints(rule_constraints) 30 | 31 | # generating all constraint-related cells for each of them 32 | self.related_cells = dict() 33 | self.related_cells = self.generate_related_cells() 34 | 35 | #prune 36 | self.pruned = dict() 37 | self.pruned = {v: list() if grid[i] == '0' else [int(grid[i])] for i, v in enumerate(self.cells)} 38 | 39 | 40 | """ 41 | generates all the coordinates of the cells 42 | """ 43 | def generate_coords(self): 44 | 45 | all_cells_coords = [] 46 | 47 | # for A,B,C, ... ,H,I 48 | for col in cols: 49 | 50 | #for 1,2,3 ,... ,8,9 51 | for row in rows: 52 | 53 | # A1, A2, A3, ... , H8, H9 54 | new_coords = col + row 55 | all_cells_coords.append(new_coords) 56 | 57 | return all_cells_coords 58 | 59 | """ 60 | generates all possible value remaining for each cell 61 | """ 62 | def generate_possibilities(self, grid): 63 | 64 | grid_as_list = list(grid) 65 | 66 | possibilities = dict() 67 | 68 | for index, coords in enumerate(self.cells): 69 | # if value is 0, then the cell can have any value in [1, 9] 70 | if grid_as_list[index] == "0": 71 | possibilities[coords] = list(range(1,10)) 72 | # else value is already defined, possibilities is this value 73 | else: 74 | possibilities[coords] = [int(grid_as_list[index])] 75 | 76 | return possibilities 77 | 78 | """ 79 | generates the constraints based on the rules of the game: 80 | value different from any in row, column or square 81 | """ 82 | def generate_rules_constraints(self): 83 | 84 | row_constraints = [] 85 | column_constraints = [] 86 | square_constraints = [] 87 | 88 | # get rows constraints 89 | for row in rows: 90 | row_constraints.append([col + row for col in cols]) 91 | 92 | # get columns constraints 93 | for col in cols: 94 | column_constraints.append([col + row for row in rows]) 95 | 96 | # get square constraints 97 | # how to split coords (non static): 98 | # https://stackoverflow.com/questions/9475241/split-string-every-nth-character 99 | rows_square_coords = (cols[i:i+3] for i in range(0, len(rows), 3)) 100 | rows_square_coords = list(rows_square_coords) 101 | 102 | cols_square_coords = (rows[i:i+3] for i in range(0, len(cols), 3)) 103 | cols_square_coords = list(cols_square_coords) 104 | 105 | # for each square 106 | for row in rows_square_coords: 107 | for col in cols_square_coords: 108 | 109 | current_square_constraints = [] 110 | 111 | # and for each value in this square 112 | for x in row: 113 | for y in col: 114 | current_square_constraints.append(x + y) 115 | 116 | square_constraints.append(current_square_constraints) 117 | 118 | # all constraints is the sum of these 3 rules 119 | return row_constraints + column_constraints + square_constraints 120 | 121 | """ 122 | generates the binary constraints based on the rule constraints 123 | """ 124 | def generate_binary_constraints(self, rule_constraints): 125 | generated_binary_constraints = list() 126 | 127 | # for each set of constraints 128 | for constraint_set in rule_constraints: 129 | 130 | binary_constraints = list() 131 | 132 | # 2 because we want binary constraints 133 | # solution taken from : 134 | # https://stackoverflow.com/questions/464864/how-to-get-all-possible-combinations-of-a-list-s-elements 135 | 136 | #for tuple_of_constraint in itertools.combinations(constraint_set, 2): 137 | for tuple_of_constraint in itertools.permutations(constraint_set, 2): 138 | binary_constraints.append(tuple_of_constraint) 139 | 140 | # for each of these binary constraints 141 | for constraint in binary_constraints: 142 | 143 | # check if we already have this constraint saved 144 | # = check if already exists 145 | # solution from https://stackoverflow.com/questions/7571635/fastest-way-to-check-if-a-value-exist-in-a-list 146 | constraint_as_list = list(constraint) 147 | if(constraint_as_list not in generated_binary_constraints): 148 | generated_binary_constraints.append([constraint[0], constraint[1]]) 149 | 150 | return generated_binary_constraints 151 | 152 | """ 153 | generates the the constraint-related cell for each one of them 154 | """ 155 | def generate_related_cells(self): 156 | related_cells = dict() 157 | 158 | #for each one of the 81 cells 159 | for cell in self.cells: 160 | 161 | related_cells[cell] = list() 162 | 163 | # related cells are the ones that current cell has constraints with 164 | for constraint in self.binary_constraints: 165 | if cell == constraint[0]: 166 | related_cells[cell].append(constraint[1]) 167 | 168 | return related_cells 169 | 170 | """ 171 | checks if the Sudoku's solution is finished 172 | we loop through the possibilities for each cell 173 | if all of them has only one, then the Sudoku is solved 174 | """ 175 | def isFinished(self): 176 | for coords, possibilities in self.possibilities.items(): 177 | if len(possibilities) > 1: 178 | return False 179 | 180 | return True 181 | 182 | """ 183 | returns a human-readable string 184 | """ 185 | def __str__(self): 186 | 187 | output = "" 188 | count = 1 189 | 190 | # for each cell, print its value 191 | for cell in self.cells: 192 | 193 | # trick to get the right print in case of an AC3-finished sudoku 194 | value = str(self.possibilities[cell]) 195 | if type(self.possibilities[cell]) == list: 196 | value = str(self.possibilities[cell][0]) 197 | 198 | output += "[" + value + "]" 199 | 200 | # if we reach the end of the line, 201 | # make a new line on display 202 | if count >= 9: 203 | count = 0 204 | output += "\n" 205 | 206 | count += 1 207 | 208 | return output 209 | 210 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | """ 4 | is_different 5 | checks if two cells are the same 6 | """ 7 | def is_different(cell_i, cell_j): 8 | result = cell_i != cell_j 9 | return result 10 | 11 | """ 12 | number of conflicts 13 | counts the number of conflicts for a cell with a specific value 14 | """ 15 | def number_of_conflicts(sudoku, cell, value): 16 | 17 | count = 0 18 | 19 | # for each of the cells that can be in conflict with cell 20 | for related_c in sudoku.related_cells[cell]: 21 | 22 | # if the value of related_c is not found yet AND the value we look for exists in its possibilities 23 | if len(sudoku.possibilities[related_c]) > 1 and value in sudoku.possibilities[related_c]: 24 | 25 | # then a conflict exists 26 | count += 1 27 | 28 | return count 29 | 30 | """ 31 | is_consistent 32 | 33 | checks if the value is consistent in the assignments 34 | """ 35 | def is_consistent(sudoku, assignment, cell, value): 36 | 37 | is_consistent = True 38 | 39 | # for each tuple of cell/value in assignment 40 | for current_cell, current_value in assignment.items(): 41 | 42 | # if the values are the equal and the cells are related to each other 43 | if current_value == value and current_cell in sudoku.related_cells[cell]: 44 | 45 | # then cell is not consistent 46 | is_consistent = False 47 | 48 | # else is it consistent 49 | return is_consistent 50 | 51 | """ 52 | assign 53 | add {cell: val} to assignment 54 | inspired by @ http://aima.cs.berkeley.edu/python/csp.html 55 | """ 56 | def assign(sudoku, cell, value, assignment): 57 | 58 | # add {cell: val} to assignment 59 | assignment[cell] = value 60 | 61 | if sudoku.possibilities: 62 | 63 | # forward check 64 | forward_check(sudoku, cell, value, assignment) 65 | 66 | """ 67 | unassign 68 | remove {cell: val} from assignment (backtracking) 69 | inspired by @ http://aima.cs.berkeley.edu/python/csp.html 70 | """ 71 | def unassign(sudoku, cell, assignment): 72 | 73 | # if the cell is in assignment 74 | if cell in assignment: 75 | 76 | # for coord, each value in pruned 77 | for (coord, value) in sudoku.pruned[cell]: 78 | 79 | # add it to the possibilities 80 | sudoku.possibilities[coord].append(value) 81 | 82 | # reset pruned for the cell 83 | sudoku.pruned[cell] = [] 84 | 85 | # and delete its assignment 86 | del assignment[cell] 87 | 88 | """ 89 | forward check 90 | domain reduction for the current assignment 91 | idea based on @ https://github.com/ishpreet-singh/Sudoku 92 | """ 93 | def forward_check(sudoku, cell, value, assignment): 94 | 95 | # for each related cell of cell 96 | for related_c in sudoku.related_cells[cell]: 97 | 98 | # if this cell is not in assignment 99 | if related_c not in assignment: 100 | 101 | # and if the value remains in the possibilities 102 | if value in sudoku.possibilities[related_c]: 103 | 104 | # removed it from the possibilities 105 | sudoku.possibilities[related_c].remove(value) 106 | 107 | # and add it to pruned 108 | sudoku.pruned[cell].append((related_c, value)) 109 | 110 | """ 111 | fetch sudokus 112 | fetches sudokus based on user's input 113 | """ 114 | def fetch_sudokus(input): 115 | 116 | DEFAULT_SIZE = 81 117 | 118 | # if the input is an multiple of DEFAULT_SIZE=81 119 | if (len(input) % DEFAULT_SIZE) != 0: 120 | print("Error : the string must be a multiple of {}".format(DEFAULT_SIZE)) 121 | sys.exit() 122 | 123 | else: 124 | formatted_input = input.replace("X", "0").replace("#", "0").replace("@", "0") 125 | 126 | if not formatted_input.isdigit(): 127 | 128 | print("Error : only the following characters are allowed: [1,9], 'X', '#' and '@'") 129 | sys.exit() 130 | 131 | else: 132 | return [formatted_input[i:i+DEFAULT_SIZE] for i in range(0, len(formatted_input), DEFAULT_SIZE)] 133 | 134 | def print_grid(grid): 135 | 136 | output = "" 137 | count = 1 138 | 139 | # for each cell, print its value 140 | for cell in grid: 141 | 142 | value = cell 143 | output += "[" + value + "]" 144 | 145 | # if we reach the end of the line, 146 | # make a new line on display 147 | if count >= 9: 148 | count = 0 149 | output += "\n" 150 | 151 | count += 1 152 | 153 | return output --------------------------------------------------------------------------------