From 3df0a0950dbc551df702c5d1e3e163ad27702f2f Mon Sep 17 00:00:00 2001 From: Henrik Blidh Date: Thu, 8 Oct 2015 23:13:00 +0200 Subject: [PATCH] Version 0.3.0 Refactoring. Works fine now compared to 0.2.0 and is faster then before. Added hard sudoku and detected error in new test. Added classification troves to setup.py --- hbldhdoku/sudoku.py | 164 ++++++++++++++++++++++++++----------------- hbldhdoku/utils.py | 2 +- setup.py | 14 +++- tests/hard.sud | 10 +++ tests/hard_sol.sud | 10 +++ tests/test_sudoku.py | 15 +++- 6 files changed, 148 insertions(+), 67 deletions(-) create mode 100644 tests/hard.sud create mode 100644 tests/hard_sol.sud diff --git a/hbldhdoku/sudoku.py b/hbldhdoku/sudoku.py index bf8375c..7ec7150 100644 --- a/hbldhdoku/sudoku.py +++ b/hbldhdoku/sudoku.py @@ -29,15 +29,17 @@ def __init__(self, n=3): self.order = n self.side = n**2 self.matrix = utils.get_list_of_lists(self.side, self.side, fill_with=0) - self._values = six.moves.range(0, (n ** 2) + 1) self.solution_steps = [] + self._values = tuple(six.moves.range(0, (n ** 2) + 1)) self._poss_rows = {} self._poss_cols = {} self._poss_mats = {} + self._possibles = {} def __str__(self): - pass + for row in self.matrix: + print("".join([str(v) for v in row]).replace('0', '*')) def __eq__(self, other): if isinstance(other, Sudoku): @@ -77,6 +79,7 @@ def _read_to_matrix(self, file_path): res = pattern.search(line) if res: s_lines.append(res.groups()[0]) + # TODO: Make it work for larger Sudokus as well... if len(s_lines) != 9: raise SudokuException("File did not contain a correctly defined sudoku.") @@ -88,15 +91,18 @@ def _read_to_matrix(self, file_path): def solve(self, verbose=False): n = 0 while not self.is_solved: + # Increase counter and update possibles arrays. n += 1 self._update() + # See if any position can be singled out. singles_found = False or self._fill_naked_singles() or self._fill_hidden_singles() if singles_found: - # Found some uniquely defined. Rerun to see if new ones have shown up. + # Found some uniquely defined. Run another iteration to see if new ones have shown up. continue else: + # Could not print(self.matrix) raise SudokuException("This Sudoku requires more advanced methods!") if verbose: @@ -105,79 +111,109 @@ def solve(self, verbose=False): print(step) def _update(self): - # TODO: Use previously stored information. + # Update possible values in each row and each column. for i in six.moves.range(self.side): - self._poss_rows[i] = {} - mat_i = (i // 3) * 3 - self._poss_mats[mat_i] = {} + self._poss_cols[i] = set(self._values).difference(set([row[i] for row in self.matrix])) + self._poss_rows[i] = set(self._values).difference(self.matrix[i]) + # Update possible values for each of the boxes. + for i in six.moves.range(self.order): + for j in six.moves.range(self.order): + this_box_index = (i * self.order) + j + box = [] + for k in six.moves.range(i * self.order, (i * self.order) + self.order): + for kk in six.moves.range(j * self.order, (j * self.order) + self.order): + box.append(self.matrix[k][kk]) + self._poss_mats[this_box_index] = set(self._values).difference(set(box)) + + # Iterate over the entire Sudoku and combine information about possible values + # from rows, columns and boxes to get a set of possible values for each cell. + for i in six.moves.range(self.side): + self._possibles[i] = {} + mat_i = (i // self.order) for j in six.moves.range(self.side): - if j not in self._poss_cols: - self._poss_cols[j] = {} - mat_j = (j // 3) * 3 - if not self.matrix[i][j]: - possible_values = set(self._values).difference(self.matrix[i]) - possible_values = possible_values.difference(set([row[j] for row in self.matrix])) - box = [] - for k in six.moves.range(mat_i, mat_i + 3): - for kk in six.moves.range(mat_j, mat_j + 3): - box.append(self.matrix[k][kk]) - possible_values = list(possible_values.difference(set(box))) - self._poss_rows[i][j] = possible_values - self._poss_cols[j][i] = possible_values - self._poss_mats[mat_i] = possible_values + self._possibles[i][j] = set() + mat_j = (j // self.order) + this_box_index = (mat_i * self.order) + mat_j + if self.matrix[i][j] > 0: + continue + self._possibles[i][j] = self._poss_rows[i].intersection( + self._poss_cols[j]).intersection(self._poss_mats[this_box_index]) def _fill_naked_singles(self): + """Look for naked singles, i.e. cells with ony one possible value. + + :return: If any Naked Single has been found. + :rtype: bool + + """ simple_found = False - for ind_i in self._poss_rows: - for ind_j in self._poss_rows[ind_i]: - if len(self._poss_rows[ind_i][ind_j]) == 1: - # Only one possible value. Assign. - self.matrix[ind_i][ind_j] = self._poss_rows[ind_i][ind_j][0] - self.solution_steps.append(self._format_step("NAKED", - (ind_i, ind_j), - self._poss_rows[ind_i][ind_j][0])) + for i in six.moves.range(self.side): + for j in six.moves.range(self.side): + if self.matrix[i][j] > 0: + continue + p = self._possibles[i][j] + if len(p) == 1: + self.matrix[i][j] = p.pop() + self.solution_steps.append(self._format_step("NAKED", (i, j), self.matrix[i][j])) simple_found = True - elif len(self._poss_rows[ind_i][ind_j]) == 0: - raise SudokuException("Error made! No possible value for ({0},{1})!", - ind_i + 1, ind_j + 1) + elif len(p) == 0: + raise SudokuException("Error made! No possible value for ({0},{1})!".format(i + 1, j + 1)) + return simple_found def _fill_hidden_singles(self): - simple_found = False - # Go through each row. - for ind_i in self._poss_rows: - for ind_j in self._poss_rows[ind_i]: - possibles = self._poss_rows[ind_i][ind_j] - for ind_k in self._poss_rows[ind_i]: - if ind_k != ind_j: - possibles = list(set(possibles).difference(self._poss_rows[ind_i][ind_k])) - if len(possibles) == 1: - # Found a hidden single in a row! - self.matrix[ind_i][ind_j] = possibles[0] - self.solution_steps.append(self._format_step("HIDDEN-ROW", - (ind_i, ind_j), - possibles[0])) - simple_found = True + """Look for hidden singles, i.e. cells with only one unique possible value in row, column or box. - # Go through each column. - for ind_j in self._poss_cols: - for ind_i in self._poss_cols[ind_j]: - possibles = self._poss_cols[ind_j][ind_i] - for ind_k in self._poss_cols[ind_j]: - if ind_k != ind_i: - possibles = list(set(possibles).difference(self._poss_cols[ind_j][ind_k])) - if len(possibles) == 1: - # Found a hidden single in a row! - self.matrix[ind_i][ind_j] = possibles[0] - self.solution_steps.append(self._format_step("HIDDEN-COL", - (ind_i, ind_j), - possibles[0])) - simple_found = True + :return: If any Hidden Single has been found. + :rtype: bool - # Go through each block. - # TODO: Write block checker. + """ + for i in six.moves.range(self.side): + mat_i = (i // self.order) * self.order + for j in six.moves.range(self.side): + mat_j = (j // self.order) * self.order + # Skip if this cell is determined already. + if self.matrix[i][j] > 0: + continue + + # Look for hidden single in rows. + p = self._possibles[i][j] + for k in six.moves.range(self.side): + if k == j: + continue + p = p.difference(self._possibles[i][k]) + if len(p) == 1: + # Found a hidden single in a row! + self.matrix[i][j] = p.pop() + self.solution_steps.append(self._format_step("HIDDEN-ROW", (i, j), self.matrix[i][j])) + return True + + # Look for hidden single in columns + p = self._possibles[i][j] + for k in six.moves.range(self.side): + if k == i: + continue + p = p.difference(self._possibles[k][j]) + if len(p) == 1: + # Found a hidden single in a column! + self.matrix[i][j] = p.pop() + self.solution_steps.append(self._format_step("HIDDEN-COL", (i, j), self.matrix[i][j])) + return True + + # Look for hidden single in box + p = self._possibles[i][j] + for k in six.moves.range(mat_i, mat_i + self.order): + for kk in six.moves.range(mat_j, mat_j + self.order): + if k == i and kk == j: + continue + p = p.difference(self._possibles[k][kk]) + if len(p) == 1: + # Found a hidden single in a column! + self.matrix[i][j] = p.pop() + self.solution_steps.append(self._format_step("HIDDEN-BOX", (i, j), self.matrix[i][j])) + return True - return simple_found + return False def _format_step(self, step_name, indices, value): return "[{0},{1}] = {2}, {3}".format(indices[0] + 1, indices[1] + 1, value, step_name) diff --git a/hbldhdoku/utils.py b/hbldhdoku/utils.py index 4dbc4db..9ff90c6 100644 --- a/hbldhdoku/utils.py +++ b/hbldhdoku/utils.py @@ -22,7 +22,7 @@ import six -def get_list(n_rows, fill_with=0.0): +def get_list(n_rows, fill_with=0): return [fill_with, ] * n_rows diff --git a/setup.py b/setup.py index 9a678b9..10a64bd 100644 --- a/setup.py +++ b/setup.py @@ -21,13 +21,25 @@ setup( name='hbldhdoku', - version='0.2.0', + version='0.3.0', author='Henrik Blidh', author_email='henrik.blidh@nedomkull.com', description='Sudoku Solver', long_description="TBD", license='MIT', url='https://github.com/hbldh/hbldhdoku', + classifiers=[ + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX :: Linux', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: MacOS :: MacOS X', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + ], packages=find_packages(), install_requires=[line.strip() for line in open("requirements.txt")], package_data={'tests': ['*.sud', ]}, diff --git a/tests/hard.sud b/tests/hard.sud new file mode 100644 index 0000000..0b20125 --- /dev/null +++ b/tests/hard.sud @@ -0,0 +1,10 @@ +# Hard 1 +****5**7* +*5**1*4*2 +871****** +*6*42***5 +**4***6** +2***76*1* +******851 +5*7*4**3* +*1**9**** \ No newline at end of file diff --git a/tests/hard_sol.sud b/tests/hard_sol.sud new file mode 100644 index 0000000..a92b66f --- /dev/null +++ b/tests/hard_sol.sud @@ -0,0 +1,10 @@ +# Hard 1 +642359178 +953817462 +871264593 +169423785 +734581629 +285976314 +496732851 +527148936 +318695247 \ No newline at end of file diff --git a/tests/test_sudoku.py b/tests/test_sudoku.py index 3cc6c50..32a685a 100644 --- a/tests/test_sudoku.py +++ b/tests/test_sudoku.py @@ -21,6 +21,8 @@ import os +from nose.tools import raises + from hbldhdoku.sudoku import Sudoku from hbldhdoku.exceptions import SudokuException @@ -32,7 +34,6 @@ def __init__(self): self.test_dir = os.path.dirname(os.path.abspath(__file__)) def test_solve_simple_sudoku(self): - s = Sudoku.load_sudoku(os.path.join(self.test_dir, 'simple.sud')) s.solve() correct_solution = Sudoku.load_sudoku(os.path.join(self.test_dir, 'simple_sol.sud')) @@ -43,3 +44,15 @@ def test_solve_medium_sudoku(self): s.solve() correct_solution = Sudoku.load_sudoku(os.path.join(self.test_dir, 'medium_sol.sud')) assert s == correct_solution + + def test_solve_hard_sudoku(self): + s = Sudoku.load_sudoku(os.path.join(self.test_dir, 'hard.sud')) + s.solve() + correct_solution = Sudoku.load_sudoku(os.path.join(self.test_dir, 'hard_sol.sud')) + assert s == correct_solution + + @raises(SudokuException) + def test_raises_error_when_unsolvable(self): + s = Sudoku.load_sudoku(os.path.join(self.test_dir, 'hard.sud')) + s.matrix[0][0] = 2 + s.solve()