1 from __future__
import division
4 from operator
import attrgetter
8 from constraints
import Constraint
, Row
, Column
, Box
10 symbols
= [str(x
+ 1) for x
in range(9)] + [chr(x
+ 97) for x
in xrange(26)]
12 class GridSizeError(Exception):
13 """Exception thrown when an impossible grid size is encountered."""
16 class GridIntegrityError(Exception):
17 """Exception thrown when a grid's state violates its own constraints. This
18 should only happen if there are bugs in the code itself.
24 """Represents a Sudoku grid."""
28 def _cellidx(self
, row
, col
):
29 """Hashes a row and column into a flat array index."""
30 return row
* self
.size
+ col
33 def _infer_box_size(cls
, dimension
):
34 """Attempts to infer the size of a box, given some dimension of the
35 entire grid, i.e. the number of cells per box/row/column.
37 Returns a tuple of height, width."""
39 # Most obvious: probably n * n.
40 root
= int(sqrt(dimension
))
41 if root
** 2 == dimension
:
44 # Otherwise, probably n * (n - 1) or n * (n - 2).
45 # These puzzles generally have the wide side (n) as the width.
46 # This is of course entirely unreliable, but better than nothing..
47 # n^2 - (1|2)n - dimension = 0
48 # Determinant is (1|2)^2 + 4 * dimension and has to be square
49 for difference
in 1, 2:
50 determinant
= difference
** 2 + 4 * dimension
51 root
= int(sqrt(determinant
))
52 if root
** 2 != determinant
:
55 n
= (-difference
+ root
) / 2
60 return n
- difference
, n
62 # Okay, I don't have a clue.
63 raise GridSizeError("Can't infer box height and width for grid size "
64 "%d; please specify them manually" % dimension
)
68 def get_constraints(self
, constraint_class
=Constraint
):
69 """Returns constraints of a certain type. Returns all of them by
73 condition
= lambda constraint
: isinstance(constraint
, constraint_class
)
74 return filter(condition
, self
._constraints
)
76 rows
= property(lambda self
: self
.get_constraints(Row
))
77 columns
= property(lambda self
: self
.get_constraints(Column
))
78 boxes
= property(lambda self
: self
.get_constraints(Box
))
80 box_height
= property(attrgetter('_box_height'))
81 box_width
= property(attrgetter('_box_width'))
82 size
= property(attrgetter('_size'))
83 constraints
= property(attrgetter('_constraints'))
87 def __init__(self
, box_height
=3, box_width
=None):
89 box_width
= box_height
91 self
._box_height
= box_height
92 self
._box_width
= box_width
93 self
._size
= box_height
* box_width
95 self
._cells
= range(self
.size
** 2)
96 for row
in xrange(self
.size
):
97 for col
in xrange(self
.size
):
98 self
._cells
[self
._cellidx(row
, col
)] \
99 = Cell(self
, row
, col
)
101 self
._constraints
= []
102 self
.add_default_constraints()
105 def from_matrix(cls
, rows
, box_height
=None, box_width
=None):
106 """Creates and returns a grid read from a list of lists."""
109 box_height
, box_width
= cls
._infer_box_size(len(rows
))
111 box_width
= box_height
113 self
= cls(box_width
=box_width
, box_height
=box_height
)
115 for row
in xrange(self
.size
):
116 for col
in xrange(self
.size
):
117 value
= rows
[row
][col
]
120 self
.cell(row
, col
).set(value
- 1, normalize
=False)
125 def from_string(cls
, grid
, box_height
=None, box_width
=None):
126 # XXX sanity check dimensions
127 """Creates and returns a grid from a string of symbols.
129 Symbols are the digits 1 to 9 followed by the letters of the alphabet.
130 Zeroes and periods are assumed to be empty cells. All other characters
133 Since whitespace is ignored, the string could be all in one line or
134 laid out visually in a square, one row per line."""
136 # Collapse the string down to just characters we want
138 grid
= re
.sub('\\.', '0', grid
)
139 grid
= re
.sub('[^0-9a-z]', '', grid
)
141 # Figure out the length of one side/box
142 size
= int(sqrt(len(grid
)))
143 if size
** 2 != len(grid
):
144 raise GridSizeError("Provided string does not form a square")
148 box_height
, box_width
= cls
._infer_box_size(size
)
150 box_width
= box_height
152 self
= cls(box_width
=box_width
, box_height
=box_height
)
154 for row
in xrange(self
.size
):
155 for col
in xrange(self
.size
):
156 ch
= grid
[ self
._cellidx(row
, col
) ]
159 self
.cell(row
, col
).set(symbols
.index(ch
), normalize
=False)
165 def add_constraint(self
, constraint
):
166 self
._constraints
.append(constraint
)
167 for cell
in constraint
.cells
:
168 cell
.add_constraint(constraint
)
170 def add_default_constraints(self
):
171 for i
in xrange(self
.size
):
172 self
.add_constraint(Row(self
, i
))
173 self
.add_constraint(Column(self
, i
))
174 self
.add_constraint(Box(self
, i
))
180 def cell(self
, row
, column
):
181 return self
._cells
[self
._cellidx(row
, column
)]
184 for cell
in self
._cells
:
185 if cell
.value
== None:
192 """Returns True iff the grid is solved. Raises an exception if an
193 integrity problem is found, such as a value appearing twice in a row.
195 # TODO remove this; name sucks and concept also sucks
199 def normalize_cells(self
):
200 """Normalizes every cell in the grid.
202 Returns the number of cell changes."""
206 for cell
in self
._cells
:
207 cell_changes
+= cell
.normalize()
211 def resolve_uniques(self
):
212 """Searches each group of cells for a value that can only appear in a
215 Returns the number of cell changes."""
219 for constraint
in self
.constraints
:
220 cell_changes
+= constraint
.resolve_uniques()
230 """Attempts to solve the grid by running through various methods of
231 elimination one at a time, from simplest to most complex."""
232 # XXX track how many cells are changed and repeat as appropriate
234 while not self
.is_filled():
237 for method
in self
._solution_steps
:
238 cell_changes
+= method(self
)
240 # If we changed something, start over with simple steps again
244 # If we didn't do anything this round, we're stuck
245 if cell_changes
== 0: