94d08332caf0ea18523631e54b39e65d926103d2
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 __init__(self
, grid
, row
, column
):
60 self
._grid
= proxy(grid
)
63 self
._values
= range(self
._grid
.size
)
64 self
._normalized
= False
66 def set(self
, value
, normalize
=True):
67 """Sets the value of this cell. If `normalize` is True or omitted, the
68 grid will be updated accordingly.
70 self
._values
= [value
]
72 self
._normalized
= False
78 """Checks to see if this cell has only one possible value left. If
79 so, sets that as its value and eliminates it from every related cell.
80 This method is exhaustive; that repeated calls should have no effect.
87 # Set this now just in case of infinite looping
88 self
._normalized
= True
91 # Don't know the value yet
95 for group_type
in 'row', 'column', 'box':
96 group
= getattr(self
, group_type
)
97 for cell
in group
.cells
:
100 cell
.eliminate(self
.value
)
103 def eliminate(self
, value
):
104 """Eliminates the given value as a possibility for this cell."""
105 if value
in self
._values
:
106 self
._values
.remove(value
)
108 if len(self
._values
) == 0:
109 # XXX give me a real exception here
112 self
._normalized
= False
117 """Represents a Sudoku grid."""
121 def _cellidx(self
, row
, col
):
122 """Hashes a row and column into a flat array index."""
123 return row
* self
._size
+ col
126 def _infer_box_size(cls
, dimension
):
127 """Attempts to infer the size of a box, given some dimension of the
128 entire grid, i.e. the number of cells per box/row/column.
130 Returns a tuple of height, width."""
132 # Most obvious: probably n * n.
133 root
= int(sqrt(dimension
))
134 if root
** 2 == dimension
:
137 # Otherwise, probably n * (n - 1) or n * (n - 2).
138 # These puzzles generally have the wide side (n) as the width.
139 # This is of course entirely unreliable, but better than nothing..
140 # n^2 - (1|2)n - dimension = 0
141 # Determinant is (1|2)^2 + 4 * dimension and has to be square
142 for difference
in 1, 2:
143 determinant
= difference
** 2 + 4 * dimension
144 root
= int(sqrt(determinant
))
145 if root
** 2 != determinant
:
148 n
= (-difference
+ root
) / 2
153 return n
- difference
, n
155 # Okay, I don't have a clue.
156 raise GridSizeError("Can't infer box height and width for grid size "
157 "%d; please specify them manually" % dimension
)
161 def _get_box_height(self
):
162 return self
._box_height
163 box_height
= property(_get_box_height
)
165 def _get_box_width(self
):
166 return self
._box_width
167 box_width
= property(_get_box_width
)
171 size
= property(_get_size
)
173 def _get_cell_groups(self
):
174 return self
._rows
+ self
._columns
+ self
._boxes
175 cell_groups
= property(_get_cell_groups
)
179 def __init__(self
, box_height
=3, box_width
=None):
181 box_width
= box_height
183 self
._box_height
= box_height
184 self
._box_width
= box_width
185 self
._size
= box_height
* box_width
187 self
._rows
= [Row(self
, i
) for i
in xrange(self
._size
)]
188 self
._columns
= [Column(self
, i
) for i
in xrange(self
._size
)]
189 self
._boxes
= [Box(self
, i
) for i
in xrange(self
._size
)]
191 self
._cells
= range(self
._size
** 2)
192 for row
in xrange(self
._size
):
193 for col
in xrange(self
._size
):
194 self
._cells
[self
._cellidx(row
, col
)] \
195 = Cell(self
, row
, col
)
198 def from_matrix(cls
, rows
, box_height
=None, box_width
=None):
199 """Creates and returns a grid read from a list of lists."""
202 box_height
, box_width
= cls
._infer_box_size(len(rows
))
204 box_width
= box_height
206 self
= cls(box_width
=box_width
, box_height
=box_height
)
208 for row
in xrange(self
._size
):
209 for col
in xrange(self
._size
):
210 value
= rows
[row
][col
]
213 self
.cell(row
, col
).set(value
- 1, normalize
=False)
218 def from_string(cls
, grid
, box_height
=None, box_width
=None):
219 # XXX sanity check dimensions
220 """Creates and returns a grid from a string of symbols.
222 Symbols are the digits 1 to 9 followed by the letters of the alphabet.
223 Zeroes and periods are assumed to be empty cells. All other characters
226 Since whitespace is ignored, the string could be all in one line or
227 laid out visually in a square, one row per line."""
229 # Collapse the string down to just characters we want
231 grid
= re
.sub('\\.', '0', grid
)
232 grid
= re
.sub('[^0-9a-z]', '', grid
)
234 # Figure out the length of one side/box
235 size
= int(sqrt(len(grid
)))
236 if size
** 2 != len(grid
):
237 raise GridSizeError("Provided string does not form a square")
241 box_height
, box_width
= cls
._infer_box_size(size
)
243 box_width
= box_height
245 self
= cls(box_width
=box_width
, box_height
=box_height
)
247 for row
in xrange(self
._size
):
248 for col
in xrange(self
._size
):
249 ch
= grid
[ self
._cellidx(row
, col
) ]
252 self
.cell(row
, col
).set(symbols
.index(ch
), normalize
=False)
258 def cell(self
, row
, column
):
259 return self
._cells
[self
._cellidx(row
, column
)]
262 for cell
in self
._cells
:
263 if cell
.value
== None:
270 """Returns True iff the grid is solved. Raises an exception if an
271 integrity problem is found, such as a value appearing twice in a row.
273 # TODO remove this; name sucks and concept also sucks
278 """Attempts to solve the grid."""
279 # XXX track how many cells are changed and repeat as appropriate
281 # Step 0: Normalize cells, i.e. find any that can only be one value
282 self
.normalize_cells()
284 # Step 1: Find values that can only go in one cell in a group
285 for group
in self
.cell_groups
:
286 group
.resolve_uniques()
289 def normalize_cells(self
):
290 """Normalizes every cell in the grid."""
291 for cell
in self
._cells
: