ce7a70193a19abad9fc71cd88a82a31597130514
1 from __future__
import division
5 from weakref
import proxy
7 from cellgroup
import Row
, Column
, Box
9 symbols
= [str(x
+ 1) for x
in range(9)] + [chr(x
+ 97) for x
in xrange(26)]
11 class GridSizeError(Exception):
12 """Exception thrown when an impossible grid size is encountered."""
15 class GridIntegrityError(Exception):
16 """Exception thrown when a grid's state violates its own constraints. This
17 should only happen if there are bugs in the code itself.
22 """Represents a single cell/value within a sudoku grid."""
26 def _get_solved(self
):
27 """True iff this cell has been solved."""
28 return len(self
._values
) == 1
29 solved
= property(_get_solved
)
32 """Returns this cell's value, if it has one known."""
34 return self
._values
[0]
36 value
= property(_get_value
)
39 """Returns the Row object associated with this cell."""
40 return self
._grid
._rows
[self
._row
]
41 row
= property(_get_row
)
43 def _get_column(self
):
44 """Returns the Column object associated with this cell."""
45 return self
._grid
._columns
[self
._col
]
46 column
= property(_get_column
)
49 """Returns the Box object associated with this cell."""
50 # Some actual math required here!
51 # Row 0..2 -> box 0..2
52 # Col 0..2 -> box 0, 3, 6 (box col 0)
53 box_row
= self
._row
// self
._grid
._box_height
54 box_col
= self
._col
// self
._grid
._box_width
55 box_idx
= box_row
* self
._grid
._box_height
+ box_col
56 return self
._grid
._boxes
[box_idx
]
57 box
= property(_get_box
)
59 def _get_constraints(self
):
60 return [ self
.row
, self
.column
, self
.box
]
61 constraints
= property(_get_constraints
)
63 def _get_groups(self
):
66 def __init__(self
, grid
, row
, column
):
67 self
._grid
= proxy(grid
)
70 self
._values
= range(self
._grid
.size
)
71 self
._normalized
= False
73 def set(self
, value
, normalize
=True):
74 """Sets the value of this cell. If `normalize` is True or omitted, the
75 grid will be updated accordingly.
77 self
._values
= [value
]
79 self
._normalized
= False
85 """Checks to see if this cell has only one possible value left. If
86 so, sets that as its value and eliminates it from every related cell.
87 This method is exhaustive; that repeated calls should have no effect.
94 # Set this now just in case of infinite looping
95 self
._normalized
= True
98 # Don't know the value yet
102 for group
in self
.constraints
:
103 for cell
in group
.cells
:
106 cell
.eliminate(self
.value
)
109 def eliminate(self
, value
):
110 """Eliminates the given value as a possibility for this cell."""
111 if value
in self
._values
:
112 self
._values
.remove(value
)
114 if len(self
._values
) == 0:
115 # XXX give me a real exception here
118 self
._normalized
= False
123 """Represents a Sudoku grid."""
127 def _cellidx(self
, row
, col
):
128 """Hashes a row and column into a flat array index."""
129 return row
* self
._size
+ col
132 def _infer_box_size(cls
, dimension
):
133 """Attempts to infer the size of a box, given some dimension of the
134 entire grid, i.e. the number of cells per box/row/column.
136 Returns a tuple of height, width."""
138 # Most obvious: probably n * n.
139 root
= int(sqrt(dimension
))
140 if root
** 2 == dimension
:
143 # Otherwise, probably n * (n - 1) or n * (n - 2).
144 # These puzzles generally have the wide side (n) as the width.
145 # This is of course entirely unreliable, but better than nothing..
146 # n^2 - (1|2)n - dimension = 0
147 # Determinant is (1|2)^2 + 4 * dimension and has to be square
148 for difference
in 1, 2:
149 determinant
= difference
** 2 + 4 * dimension
150 root
= int(sqrt(determinant
))
151 if root
** 2 != determinant
:
154 n
= (-difference
+ root
) / 2
159 return n
- difference
, n
161 # Okay, I don't have a clue.
162 raise GridSizeError("Can't infer box height and width for grid size "
163 "%d; please specify them manually" % dimension
)
167 def _get_box_height(self
):
168 return self
._box_height
169 box_height
= property(_get_box_height
)
171 def _get_box_width(self
):
172 return self
._box_width
173 box_width
= property(_get_box_width
)
177 size
= property(_get_size
)
179 def _get_constraints(self
):
180 return self
._rows
+ self
._columns
+ self
._boxes
181 constraints
= property(_get_constraints
)
185 def __init__(self
, box_height
=3, box_width
=None):
187 box_width
= box_height
189 self
._box_height
= box_height
190 self
._box_width
= box_width
191 self
._size
= box_height
* box_width
193 self
._rows
= [Row(self
, i
) for i
in xrange(self
._size
)]
194 self
._columns
= [Column(self
, i
) for i
in xrange(self
._size
)]
195 self
._boxes
= [Box(self
, i
) for i
in xrange(self
._size
)]
197 self
._cells
= range(self
._size
** 2)
198 for row
in xrange(self
._size
):
199 for col
in xrange(self
._size
):
200 self
._cells
[self
._cellidx(row
, col
)] \
201 = Cell(self
, row
, col
)
204 def from_matrix(cls
, rows
, box_height
=None, box_width
=None):
205 """Creates and returns a grid read from a list of lists."""
208 box_height
, box_width
= cls
._infer_box_size(len(rows
))
210 box_width
= box_height
212 self
= cls(box_width
=box_width
, box_height
=box_height
)
214 for row
in xrange(self
._size
):
215 for col
in xrange(self
._size
):
216 value
= rows
[row
][col
]
219 self
.cell(row
, col
).set(value
- 1, normalize
=False)
224 def from_string(cls
, grid
, box_height
=None, box_width
=None):
225 # XXX sanity check dimensions
226 """Creates and returns a grid from a string of symbols.
228 Symbols are the digits 1 to 9 followed by the letters of the alphabet.
229 Zeroes and periods are assumed to be empty cells. All other characters
232 Since whitespace is ignored, the string could be all in one line or
233 laid out visually in a square, one row per line."""
235 # Collapse the string down to just characters we want
237 grid
= re
.sub('\\.', '0', grid
)
238 grid
= re
.sub('[^0-9a-z]', '', grid
)
240 # Figure out the length of one side/box
241 size
= int(sqrt(len(grid
)))
242 if size
** 2 != len(grid
):
243 raise GridSizeError("Provided string does not form a square")
247 box_height
, box_width
= cls
._infer_box_size(size
)
249 box_width
= box_height
251 self
= cls(box_width
=box_width
, box_height
=box_height
)
253 for row
in xrange(self
._size
):
254 for col
in xrange(self
._size
):
255 ch
= grid
[ self
._cellidx(row
, col
) ]
258 self
.cell(row
, col
).set(symbols
.index(ch
), normalize
=False)
264 def cell(self
, row
, column
):
265 return self
._cells
[self
._cellidx(row
, column
)]
268 for cell
in self
._cells
:
269 if cell
.value
== None:
276 """Returns True iff the grid is solved. Raises an exception if an
277 integrity problem is found, such as a value appearing twice in a row.
279 # TODO remove this; name sucks and concept also sucks
284 """Attempts to solve the grid."""
285 # XXX track how many cells are changed and repeat as appropriate
287 # Step 0: Normalize cells, i.e. find any that can only be one value
288 self
.normalize_cells()
290 # Step 1: Find values that can only go in one cell in a group
291 for group
in self
.constraints
:
292 group
.resolve_uniques()
295 def normalize_cells(self
):
296 """Normalizes every cell in the grid."""
297 for cell
in self
._cells
: