Skip to content

Commit

Permalink
Version 0.3.0
Browse files Browse the repository at this point in the history
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
  • Loading branch information
hbldh committed Oct 8, 2015
1 parent f2855b7 commit 3df0a09
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 67 deletions.
164 changes: 100 additions & 64 deletions hbldhdoku/sudoku.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.")
Expand All @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion hbldhdoku/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
14 changes: 13 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', ]},
Expand Down
10 changes: 10 additions & 0 deletions tests/hard.sud
Original file line number Diff line number Diff line change
@@ -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****
10 changes: 10 additions & 0 deletions tests/hard_sol.sud
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Hard 1
642359178
953817462
871264593
169423785
734581629
285976314
496732851
527148936
318695247
15 changes: 14 additions & 1 deletion tests/test_sudoku.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

import os

from nose.tools import raises

from hbldhdoku.sudoku import Sudoku
from hbldhdoku.exceptions import SudokuException

Expand All @@ -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'))
Expand All @@ -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()

0 comments on commit 3df0a09

Please sign in to comment.