Compare commits

..

53 commits
old ... master

Author SHA1 Message Date
2529533661 Adjust possible_values_for_square to behave like dlx expects 2017-10-16 12:40:29 -07:00
3689ac6974 Move existing test file alongside the sudoku __init__.py 2017-10-16 12:40:00 -07:00
c9a86d0fdc Add dlx solver (this is still a WIP)
The solver builds the constraint matrix. I have not fully verified its correctness, nor does the thing actually do the thing yet.
2017-10-16 12:25:30 -07:00
137e4e6436 Add Sudoku.get() -> value of board at (x,y) 2017-10-16 12:24:02 -07:00
fba52c7f09 Add Sudoku.possible_values_for_square() 2017-10-16 12:23:42 -07:00
1186305712 Clean up puzzles.py interface 2017-10-12 12:29:49 -07:00
a7dda24015 Print stuff in backtracking module only; clean up the \r stuff 2017-10-11 20:53:10 -07:00
3b380b17d2 Take care of some edge cases in the backtracker 2017-10-11 20:25:11 -07:00
a9a331df91 Move actual square setter to _set 2017-10-11 20:24:09 -07:00
387ccacb8d Print some stuff after setting a value 2017-10-11 19:46:16 -07:00
d25a320c30 puzzle script takes an algorithm to use to solve the given puzzles 2017-10-11 19:45:46 -07:00
65f4453e78 Attempt #1 at a backtracking solver 2017-10-11 19:45:00 -07:00
189589c0e6 solve() takes a function that solves the puzzle 2017-10-11 19:44:41 -07:00
9ebabd4b56 Add square setter to Sudoku 2017-10-11 19:44:19 -07:00
7a0e31858f Add __repr__ to Sudoku 2017-10-11 19:43:42 -07:00
181bc7d61e Remove idx from peer set 2017-10-11 19:43:15 -07:00
f7b6fb053f Memoize possible_values set 2017-10-11 19:42:11 -07:00
1549269661 Mutable squares, immutable clues set 2017-10-11 19:41:32 -07:00
e329208db9 Rewrite the printer to be cleaner 2017-10-11 19:40:39 -07:00
77456d5114 Add solvers package 2017-10-11 16:07:33 -07:00
301a56904c Relative import Sudoku symbol 2017-10-11 16:05:10 -07:00
0976ec8bb5 Move puzzles back up a level 2017-10-11 16:04:14 -07:00
7809c38104 Allow specifying path to puzzles library
By default look in the current directory for a puzzles/ directory.
2017-10-11 16:03:49 -07:00
ed41930092 Oops broke the tests 2017-10-11 15:59:14 -07:00
c0021ac2c2 Make a sudoku package 2017-10-11 15:58:31 -07:00
f12263b197 Stub of a solve() method 2017-10-11 15:53:25 -07:00
b8eda77093 Bold clues when printing the board 2017-10-11 15:53:17 -07:00
9534f04a08 Print the whole grid -- fallout from changing the meaning of size 2017-10-11 15:52:53 -07:00
6da7c2ecd6 Store indexes of clues as a set 2017-10-11 15:52:27 -07:00
a34987e3db Print multiple indexes 2017-10-11 15:51:55 -07:00
e2eadf9da7 Allow selecting and printing puzzles from the library to the command line 2017-10-11 15:49:18 -07:00
ab927ee41d Move some stuff around; add some docstrings 2017-10-10 12:54:33 -07:00
a0594d1561 Add some doc strings 2017-10-10 12:51:33 -07:00
fb1066f716 Fix the board size parameter in a test 2017-10-10 12:51:27 -07:00
e5d00debc7 Rename square -> box 2017-10-10 12:51:11 -07:00
35647fc8c9 Change the meaning of size
- size is the size of one side of a square
- row_size is the length of the grid on one side
- grid_size is the area of the grid
2017-10-10 12:44:24 -07:00
a9a01d3fd4 Fix solved property 2017-10-10 11:59:29 -07:00
cf43b02aa9 Add test_peers(); fix up some errors found 2017-10-10 11:43:02 -07:00
00225aca44 Add peers and index_peers for a given (x,y) 2017-10-10 09:55:51 -07:00
19a9bc9d37 Tweak the str printing a bit 2017-10-10 09:55:34 -07:00
93a4fecb26 Add helpers to get rows, cols, and squares for particular coordinates 2017-10-10 09:55:09 -07:00
3017b1e923 Rename initial init attribute to board; make self.board -> _board 2017-10-10 09:53:55 -07:00
88f0e07c06 Copy pasta error 2017-10-09 15:03:33 -07:00
5a32743d7b Add initial arg to Sudoku.__init__() 2017-10-08 08:26:34 -07:00
eae05487b9 Add puzzles and puzzles.py from the old branch 2017-10-08 08:26:18 -07:00
5696c087d8 Add a TODO 2017-10-08 08:06:34 -07:00
b5ac48bc10 Little bit o' cleanup 2017-10-08 08:04:34 -07:00
3fabd4b420 Rough, probably incomplete solved property 2017-10-08 08:00:45 -07:00
5ebe1f58ff Add index_* properties for returning ranges of indexes 2017-10-08 07:56:56 -07:00
01428e3972 Fix squares; add dimension property; add some tests 2017-10-08 07:23:03 -07:00
f98a0f4f75 Quick implement rows, columns, and square 2017-10-07 12:24:41 -07:00
2feef04ad9 Working on the solved property 2017-10-07 10:39:19 -07:00
53629f4d56 Empty Sudoku board, with a pretty printing __str__() 2017-10-07 09:20:49 -07:00
10 changed files with 574 additions and 410 deletions

2
.gitignore vendored
View file

@ -1,2 +0,0 @@
*.pyc
__pycache__/

93
puzzles.py Normal file → Executable file
View file

@ -1,58 +1,67 @@
#!/usr/bin/env python3
#!env python3
# Eryn Wells <eryn@erynwells.me>
'''
Parser for puzzles in the ./puzzles directory.
'''
import sudoku
import argparse
import os.path
import sys
from sudoku import Sudoku, solvers
euler = []
norvig = []
def parse_puzzle_library(path, quiet=True):
if not quiet:
print('Parsing puzzles in {}'.format(path))
puzzles = _get_puzzles(path, quiet)
return puzzles
def parse_euler(filename='./puzzles/euler.txt'):
def _get_puzzles(filename, quiet):
with open(filename, 'r') as f:
puzzle_lines = f.readlines()
puzzles = f.readlines()
return (_parse_puzzle(p, quiet) for p in puzzles if p)
for puzzle in puzzle_lines:
# Chop the newline
if puzzle[-1] == '\n':
puzzle = puzzle[:-1]
print('Parsing puzzle: {}'.format(puzzle))
if len(puzzle) != 81:
continue
kwargs = {}
for idx in range(len(puzzle)):
sq = puzzle[idx]
if sq not in '1234567890.':
continue
sq_int = 0 if sq == '.' else int(sq)
x, y = int(idx % 9), int(idx / 9)
if sq_int != 0:
kwargs[sudoku.Board.xy_kwargs_key(x, y)] = sq_int
euler.append(sudoku.Board(**kwargs))
def _parse_puzzle(puzzle, quiet):
puzzle = puzzle.strip()
if len(puzzle) == 81:
if not quiet:
print("Parsing '{}'".format(puzzle))
board = (int('0' if x == '.' else x) for x in puzzle)
return Sudoku(board=board)
else:
if not quiet:
print("Skipping '{}'".format(puzzle))
return None
def parse_args(args):
parser = argparse.ArgumentParser()
parser.add_argument('--solver', '-s', default=None,
help='The solver to use to solve this puzzle.')
parser.add_argument('--verbose', '-v', action='store_true', default=False,
help='Print extra information when parsing puzzle libraries.')
parser.add_argument('library',
help='A library file containing puzzles, one per line.')
parser.add_argument('indexes', metavar='N', nargs='+', type=int,
help='0-based indexes of puzzles in the library')
return parser.parse_args(args)
def parse_norvig(filename='./puzzles/norvig.txt'):
with open(filename, 'r') as f:
puzzle_lines = f.readlines()
def main():
args = parse_args(sys.argv[1:])
puzzle_library = list(parse_puzzle_library(args.library, quiet=not args.verbose))
for i in args.indexes:
puzzle = puzzle_library[i]
print(puzzle)
if args.solver is not None:
try:
solver = getattr(solvers, args.solver)
puzzle.solve(solver.solve)
except AttributeError:
print('No solver named {}'.format(args.solver))
print(puzzle)
return 0
for puzzle in puzzle_lines:
# Chop the newline
if puzzle[-1] == '\n':
puzzle = puzzle[:-1]
print('Parsing puzzle: {}'.format(puzzle))
if len(puzzle) != 81:
continue
kwargs = {}
for idx in range(len(puzzle)):
sq = puzzle[idx]
if sq not in '1234567890.':
continue
sq_int = 0 if sq == '.' else int(sq)
x, y = int(idx % 9), int(idx / 9)
if sq_int != 0:
kwargs[sudoku.Board.xy_kwargs_key(x, y)] = sq_int
norvig.append(sudoku.Board(**kwargs))
if __name__ == '__main__':
sys.exit(main())

View file

@ -93,4 +93,3 @@
.5..9....1.....6.....3.8.....8.4...9514.......3....2..........4.8...6..77..15..6.
.....2.......7...17..3...9.8..7......2.89.6...13..6....9..5.824.....891..........
3...8.......7....51..............36...2..4....7...........6.13..452...........8..


237
sudoku.py
View file

@ -1,237 +0,0 @@
#!/usr/bin/env python3
# Eryn Wells <eryn@erynwells.me>
'''
A Sudoku solver.
'''
import copy
import logging
import math
LOG = logging.getLogger(__name__)
class Board(dict):
def __init__(self, size=9, **kwargs):
self.size = size
# Verify size is a perfect square. The sqrt(size) is also the dimension of each box on the
# Sudoku grid.
self.box_size = int(math.sqrt(size))
assert self.box_size == int(self.box_size), 'Invalid size; value must be a perfect square'
# The range of possible values for a square.
self.possible_values = range(1, self.size + 1)
# Make the grid.
super(Board, self).__init__([(Board.xy_key(x, y), list(self.possible_values))
for x in range(self.size)
for y in range(self.size)])
for square in self:
initial_value = kwargs.get(Board.xy_kwargs_key(*square))
if initial_value is None:
continue
if initial_value not in self.possible_values:
raise ValueError('Invalid initial value for square ({}, {}): {}'.format(x, y, initial_value))
LOG.debug('Setting initial value of {} to {}'.format(square, initial_value))
self.assign(square, [initial_value])
@classmethod
def xy_key(self, x, y):
'''Given {x} and {y}, generate a key to refer to the square at coordinate (x, y) in the grid.'''
return (int(x), int(y))
@classmethod
def xy_kwargs_key(self, x, y):
'''
Given {x} and {y}, generate a key to refer to the square in the __init__ kwargs at coordinate (x, y) in the
grid.
'''
return 'x{}y{}'.format(x, y)
@property
def solved(self):
'''
Determines if the board has been solved. First, determine if all squares have no more than one value (i.e. they
have had values assigned to them). If not, return False. If so, check each unit (rows, columns, and boxes) to
make sure that each has one and only one of each value in the range of possible values for a unit. If not,
return False; otherwise, return True.
'''
if not all(len(s) == 1 for s in self.values()):
return False
def validate_unit(unit):
'''
Validate {unit} by ensuring it has exactly 1 of each value in the range of possible values.
'''
necessary_values = list(self.possible_values)
for square, values in unit.items():
v = values[0]
if v in necessary_values:
necessary_values.remove(v)
else:
return False
if len(necessary_values) != 0:
return False
return True
return ( all(validate_unit(self.row(r)) for r in range(self.size))
and all(validate_unit(self.col(c)) for c in range(self.size))
and all(validate_unit(self.box(x, y))
for x in range(0, self.size, self.box_size)
for y in range(0, self.size, self.box_size)))
def row(self, idx):
'''
Return a dict of all squares in the {idx}'th row.
'''
assert idx >= 0 and idx < self.size, 'Invalid row index; value must be >= 0 and < {}'.format(self.size)
# key[1] is Y coordinate in the grid.
return {k: v for k, v in self.items() if k[1] == idx}
def col(self, idx):
'''
Return a dict of all squares in the {idx}'th column.
'''
assert idx >= 0 and idx < self.size, 'Invalid column index; value must be >= 0 and < {}'.format(self.size)
# key[0] is X coordinate in the grid.
return {k: v for k, v in self.items() if k[0] == idx}
def box(self, x, y):
'''
Given a square at coordinates ({x}, {y}), return a dictionary containing the squares that make up the box that
contains the point.
'''
bx = int(x / self.box_size) * self.box_size
by = int(y / self.box_size) * self.box_size
bx_range = range(bx, bx + self.box_size)
by_range = range(by, by + self.box_size)
return {k: v for k, v in self.items() if k[0] in bx_range and k[1] in by_range}
def peers(self, x, y):
'''
Generate and return a list of peers for the square at coordinates ({x}, {y}). Peers are defined as all the
squares in the same row, column, and box as the given square.
'''
# Generate a dictionary of all the squares in the row, column, and box containing the given square.
peers = dict(list(self.row(y).items()) + list(self.col(x).items()) + list(self.box(x, y).items()))
# Remove the given square.
del peers[Board.xy_key(x, y)]
return peers
def clear(self, squares=None):
'''
Clear the board. Resets each square's possible value list to the full range of possible values for this board.
'''
for square in (self if squares is None else squares):
self.assign(square, list(self.possible_values))
def solve(self):
return self.search()
def search(self):
'''
Search for a solution, depth-first. Return the solved Board as a new instance of this class.
'''
if self.solved:
return self
# Chose the square with the fewest possible values.
_, smallest = min((len(self[sq]), sq) for sq in self if len(self[sq]) > 1)
# Deepcopy the board.
trials = []
for v in self[smallest]:
trial_board = copy.deepcopy(self)
if trial_board.assign(smallest, [v]):
trials.append(trial_board)
trial_board.search()
else:
trials.append(None)
for t in trials:
if t is not None:
return t
return False
def assign(self, square, value):
'''
Assign {value} to {square}. {value} is expected to be an iterable (a list). {key} should be a valid (x, y)
coordinate pair for referencing a square on the board.
'''
LOG.debug('Assigning values {} to square {}.'.format(value, square))
removed_values = set(self[square]) - set(value)
for v in removed_values:
if not self.eliminate(square, v):
return False
return True
def eliminate(self, square, value):
'''
Eliminate {value} from {square}, propagating changes to the squares peers as necessary. This process involves
two logical rules of Sudoku. First, if a square is reduced to a single possible value, assign that value to the
square and remove the value from the possible value lists of its peers. Second, if a unit has only one square
that can hold a value, put the value in that square and propagate that change to its peers.
'''
if value not in self[square]:
LOG.debug('Value {} not in square {}; skipping'.format(value, square))
return True
# Update peer value list.
super(Board, self).__setitem__(square, [v for v in self[square] if v != value])
LOG.debug('Eliminating {} from square {}'.format(value, square))
# (1) If a square is reduced to one value, eliminate that value from its peers.
if len(self[square]) == 0:
# Whoops. Removed the last value... We have a contradiction now.
LOG.error('Removed last value from square {}'.format(square))
return False
elif len(self[square]) == 1:
# One value left in this square. Propagate changes to its peers.
LOG.debug('One value left in square {}; eliminating {} from its peers'.format(square, self[square][0]))
for peer in self.peers(*square):
if not self.eliminate(peer, self[square][0]):
return False
# (2) If a unit has only one square for a value, put it there.
for unit in (self.row(square[1]), self.col(square[0]), self.box(*square)):
places = [sq for sq in unit if value in unit[sq]]
if len(places) == 0:
LOG.error('No place for value {} to go in unit {}'.format(value, unit))
return False
elif len(places) == 1:
LOG.debug('One place for value {} to be in unit {}; setting'.format(value, unit))
self.assign(places[0], [value])
return True
def __delitem__(self, key):
# Don't allow deleting keys from self.
pass
def __str__(self):
lines = []
box_lines = []
square_size = len(str(self.size))
for y in range(self.size):
row_squares = []
box_squares = []
for x in range(self.size):
square = self.get(Board.xy_key(x, y))
if len(square) == 1:
box_squares.append(str(square[0]).center(square_size))
else:
box_squares.append('.'.center(square_size))
if len(box_squares) == self.box_size:
row_squares.append(' '.join(box_squares))
box_squares = []
# Print a divider between boxes.
box_lines.append(' | '.join(row_squares))
if len(box_lines) == self.box_size:
lines.append('\n'.join(box_lines))
box_lines = []
if y < self.size - 1:
# Don't print a divider on the last line.
box_dividers = ['-' * ((square_size + 1) * self.box_size - 1) for box in range(self.box_size)]
lines.append('\n{}\n'.format('-+-'.join(box_dividers)))
return ''.join(lines)

230
sudoku/__init__.py Normal file
View file

@ -0,0 +1,230 @@
#!env python3
# Eryn Wells <eryn@erynwells.me>
'''
A Sudoku puzzle solver.
'''
import itertools
import math
BOLD_SEQUENCE = '\x1B[1m'
UNBOLD_SEQUENCE = '\x1B[0m'
class Sudoku:
def __init__(self, size=3, board=None):
self._size = size
sz4 = size ** 4
if board:
self._board = bytearray(board)[:sz4]
self._clues = frozenset(i for i in range(len(self._board)) if self._board[i] != 0)
else:
self._board = bytearray(sz4)
self._clues = frozenset()
self._possible_values = None
@property
def size(self):
'''
The size of the board. This dictates the length of one side of the boxes that the board is subdivided into.
'''
return self._size
@property
def row_size(self):
'''
The length of a row or column, or the area of a box in the grid.
'''
return self.size ** 2
@property
def grid_size(self):
'''
The total number of squares in the grid.
'''
return self.size ** 4
@property
def all_squares(self):
'''
Iterator of xy-coordinates for every square in the grid.
'''
return itertools.product(range(self.row_size), repeat=2)
@property
def all_boxes(self):
'''
Iterator of xy-coordinates for every box in the grid.
'''
return itertools.product(range(self.size), repeat=2)
@property
def possible_values(self):
'''
The set of valid values for any grid square. This method does not account for values made invalid by already
being present in a peer of a given square.
'''
if not self._possible_values:
self._possible_values = set(range(1, self.row_size + 1))
return self._possible_values
def possible_values_for_square(self, x, y):
value = self.get(x, y)
if value:
return {value}
else:
peers = self.peers(x, y)
return self.possible_values - peers
@property
def rows(self):
return self._apply_index_ranges(self.index_rows)
@property
def columns(self):
return self._apply_index_ranges(self.index_columns)
@property
def boxes(self):
return self._apply_index_ranges(self.index_boxes)
def peers(self, x, y):
'''
Return a set of values of the peers for a given square.
'''
return {self._board[i] for i in self.index_peers(x, y) if self._board[i] != 0}
@property
def index_rows(self):
'''
Return an iterable of ranges of indexes into the board, each defining a row.
'''
return (self._row(i) for i in range(self.row_size))
@property
def index_columns(self):
'''
Return an iterable of ranges of indexes into the board, each defining a column.
'''
return (self._column(i) for i in range(self.row_size))
@property
def index_boxes(self):
'''
Return an iterable of ranges of indexes into the board, each defining a box.
'''
return (self._box(x, y) for (x,y) in self.all_boxes)
def index_peers(self, x, y):
'''
Return a set of the peers, indexes into the board, for a given square.
'''
idx = self._xy_to_idx(x, y)
box = int(x / self.size), int(y / self.size)
return (set(self._row(y)) | set(self._column(x)) | set(self._box(*box))) - {idx}
def _row(self, r):
row_size = self.row_size
return range(r * row_size, r * row_size + row_size)
def _column(self, c):
return range(c, self.grid_size, self.row_size)
def _box(self, x, y):
size = self.size
row_size = self.row_size
offx, offy = (x * size, y * size * row_size)
def _range(i):
start = (offy + i * row_size) + offx
return range(start, start + size)
ranges = itertools.chain(*[_range(i) for i in range(size)])
return ranges
@property
def solved(self):
expected = self.possible_values
all_groups = itertools.chain(self.rows, self.columns, self.boxes)
return all(expected == set(g) for g in all_groups)
def solve(self, solver):
return solver(self)
def get(self, x, y):
idx = self._xy_to_idx(x, y)
value = self._board[idx]
return None if value == 0 else value
def set(self, x, y, value):
if value not in self.possible_values:
raise ValueError('{} not in set of possible values {}'.format(value, self.possible_values))
peers = self.peers(x, y)
if peers == self.possible_values:
raise NoPossibleValues('Peer set for ({},{}) contains all possible values'.format(x, y))
if value in peers:
raise ValueExistsInPeers('{} already exists in the peer set for ({},{})'.format(value, x, y))
self._set(x, y, value)
def unset(self, x, y):
self._set(x, y, 0)
def _set(self, x, y, value):
idx = self._xy_to_idx(x, y)
if idx in self._clues:
raise SquareIsClue('Cannot set clue square ({},{})'.format(x, y))
self._board[idx] = value
def _xy_to_idx(self, x, y):
return y * self.row_size + x
def _apply_index_ranges(self, ranges):
return ((self._board[i] for i in r) for r in ranges)
def __repr__(self):
return "{}(size={}, board='{}')".format(self.__class__.__name__,
self.size,
''.join(str(i) for i in self._board))
def __str__(self):
field_width = len(str(max(self.possible_values)))
spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * self.size) for _ in range(self.size)]))
fmt = ''
for (y,x) in self.all_squares:
if x == 0:
if y % self.size == 0:
if y != 0:
fmt += '\n'
fmt += '{spacer}'
fmt += '\n'
if x % self.size == 0:
fmt += '|'
idx = self._xy_to_idx(x,y)
if idx in self._clues:
bold = BOLD_SEQUENCE
unbold = UNBOLD_SEQUENCE
else:
bold = unbold = ''
fmt += '{bold}{{board[{i}]:^{{width}}}}{unbold}'.format(i=idx, bold=bold, unbold=unbold)
if x == (self.row_size - 1):
fmt += '|'
fmt += '\n{spacer}'
return fmt.format(board=[str(i) if i != 0 else ' ' for i in self._board], spacer=spacer, width=field_width)
class SudokuError(Exception):
pass
class SquareIsClue(SudokuError):
pass
class NoPossibleValues(SudokuError):
pass
class ValueExistsInPeers(SudokuError):
pass

View file

@ -0,0 +1,6 @@
# Eryn Wells <eryn@erynwells.me>
'''
A library of Sudoku solvers.
'''
from . import backtracker, dlx

View file

@ -0,0 +1,61 @@
# Eryn Wells <eryn@erynwells.me>
from .. import SquareIsClue, ValueExistsInPeers, NoPossibleValues
class Backtrack(Exception):
pass
def solve(sudoku):
'''
Implements a recursive backtracking Sudoku solver.
'''
return _solve_square(sudoku, 0, 0)
def _solve_square(sudoku, x, y):
should_backtrack = False
for value in sudoku.possible_values:
should_backtrack = False
try:
sudoku.set(x, y, value)
except SquareIsClue:
# Do nothing with this square; continue on to the next square below.
pass
except ValueExistsInPeers:
# Try next value.
should_backtrack = True
continue
except NoPossibleValues:
# Need to backtrack.
should_backtrack = True
break
print('\r{!r}'.format(sudoku), end='', flush=True)
next_coord = _next_coord(sudoku, x, y)
if not next_coord:
break
try:
return _solve_square(sudoku, *next_coord)
except Backtrack:
should_backtrack = True
continue
if should_backtrack:
try:
sudoku.unset(x, y)
except SquareIsClue:
pass
raise Backtrack()
print()
return sudoku
def _next_coord(sudoku, x, y):
x += 1
if x >= sudoku.row_size:
(x, y) = (0, y + 1)
if y >= sudoku.row_size:
return None
return (x, y)

137
sudoku/solvers/dlx.py Normal file
View file

@ -0,0 +1,137 @@
# Eryn Wells <eryn@erynwells.me>
from collections import namedtuple
from enum import Enum
from .. import SquareIsClue, ValueExistsInPeers, NoPossibleValues
class Backtrack(Exception):
pass
class ConstraintKind(Enum):
CELL = 1
ROW = 2
COL = 3
BOX = 4
Constraint = namedtuple('CellConstraint', ['kind', 'index', 'value'])
Possibility= namedtuple('Possibility', ['row', 'col', 'value'])
class Node:
'''
A doubly-linked list node that is a member of two distinct lists. One list is the row it is a member of. The other
list is the column it is a member of.
'''
def __init__(self):
# Horizontal linked list. West is "previous", east is "next".
self.west = self.east = self
# Vertical linked list. North is "previous", south is "next".
self.north = self.south = self
def insert_after_in_row(self, node):
self._insert(node, 'west', 'east')
def insert_before_in_col(self, node):
self._insert(node, 'south', 'north')
def iterate_row(self):
return self._iterate('east')
def iterate_col(self):
return self._iterate('south')
def _insert(self, node, prev_attr, next_attr):
self_east = getattr(self, next_attr) # Save my old next node
setattr(self, next_attr, node) # My next points to the new node
setattr(node, next_attr, self_east) # New node's next points to the old next
setattr(node, prev_attr, self) # New node's prev points to me
if self_east:
setattr(self_east, prev_attr, node) # Old next's prev points to the new node
def _iterate(self, next_attr):
cur = self
while cur:
yield cur
cur = getattr(cur, next_attr)
if cur == self:
break
class Header(Node):
'''
A column header, including a count of the number of rows in this column.
'''
def __init__(self, constraint):
Node.__init__(self)
self.constraint = constraint
self.number_of_rows = 0
def append(self, node):
self.insert_before_in_col(node)
self.number_of_rows += 1
node.header = self
class Cell(Node):
'''
A cell in the DLX matrix.
'''
def __init__(self, possibility):
super(Cell, self).__init__()
self.header = None
self.possibility = possibility
def __repr__(self):
return '{}({})'.format(self.__class__.__name__, self.possibility)
def solve(sudoku):
'''
Implements DLX, Don Knuth's Dancing Links version of Algorithm X, for solving exact cover problems.
'''
# TODO: Construct the matrix based on the provided sudoku.
# TODO: Perform the algorithm on the matrix.
# TODO: With the solution from running the algorithm above, fill in the sudoku.
return sudoku
def _build_matrix(sudoku):
# 1. Create headers for all columns.
headers = _build_headers(sudoku)
_build_rows(sudoku, headers)
return headers
def _build_headers(sudoku):
head = None
cur = None
def _insert_header(data):
header = Header(data)
if cur:
cur.insert_after_in_row(header)
return header
# Cell constraints
for i in range(sudoku.grid_size):
cur = _insert_header(Constraint(ConstraintKind.CELL, i, None))
# Row, Col, and Box constraints
for kind in (ConstraintKind.ROW, ConstraintKind.COL, ConstraintKind.BOX):
for i in range(sudoku.row_size):
for value in sudoku.possible_values:
cur = _insert_header(Constraint(kind, i, value))
# Head points to the first column header
head = cur.east
return head
def _build_rows(sudoku, headers):
for (index, coords) in enumerate(sudoku.all_squares):
board_value = sudoku.get(*coords)
possibilities = sudoku.possible_values_for_square(*coords)
for value in possibilities:
cur = None
for col in headers.iterate_row():
if col.constraint.index != index:
continue
cell = Cell(Possibility(*coords, value))
col.append(cell)
if cur:
cur.insert_after_in_row(cell)
cur = cell

89
sudoku/test.py Normal file
View file

@ -0,0 +1,89 @@
#!env python3
# Eryn Wells <eryn@erynwells.me>
'''
Unit tests for the Sudoku module.
'''
import unittest
import sudoku
class Sudoku4TestCase(unittest.TestCase):
def setUp(self):
self.board = sudoku.Sudoku(size=2)
class Sudoku4BasicTests(Sudoku4TestCase):
def test_that_board_is_sane(self):
self.assertEqual(self.board.size, 2)
self.assertEqual(len(self.board._board), 2**4)
def test_rows(self):
expected_rows = [
[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]
]
for (row, exrow) in zip(self.board.index_rows, expected_rows):
row_list = list(row)
with self.subTest(row=row_list, ex=exrow):
self.assertEqual(row_list, exrow)
def test_columns(self):
expected_columns = [
[0, 4, 8, 12],
[1, 5, 9, 13],
[2, 6, 10, 14],
[3, 7, 11, 15]
]
for (col, excol) in zip(self.board.index_columns, expected_columns):
col_list = list(col)
with self.subTest(col=col_list, ex=excol):
self.assertEqual(col_list, excol)
def test_boxes(self):
expected_boxes = {
(0,0): set([ 0, 1, 4, 5]),
(1,0): set([ 2, 3, 6, 7]),
(0,1): set([ 8, 9, 12, 13]),
(1,1): set([10, 11, 14, 15]),
}
for (coord, exbox) in expected_boxes.items():
with self.subTest(sq=coord, ex=exbox):
sq = set(self.board._box(*coord))
self.assertEqual(sq, exbox)
def test_peers(self):
expected_peers = {
(0,0): set([1, 2, 3, 4, 8, 12, 5]),
(1,0): set([0, 2, 3, 5, 9, 13, 4]),
(2,0): set([0, 1, 3, 6, 10, 14, 7]),
(3,0): set([0, 1, 2, 7, 11, 15, 6]),
(0,1): set([5, 6, 7, 0, 8, 12, 1]),
(1,1): set([4, 6, 7, 1, 9, 13, 0]),
(2,1): set([4, 5, 7, 2, 10, 14, 3]),
(3,1): set([4, 5, 6, 3, 11, 15, 2]),
(0,2): set([9, 10, 11, 0, 4, 12, 13]),
(1,2): set([8, 10, 11, 1, 5, 13, 12]),
(2,2): set([8, 9, 11, 2, 6, 14, 15]),
(3,2): set([8, 9, 10, 3, 7, 15, 14]),
(0,3): set([13, 14, 15, 0, 4, 8, 9]),
(1,3): set([12, 14, 15, 1, 5, 9, 8]),
(2,3): set([12, 13, 15, 2, 6, 10, 11]),
(3,3): set([12, 13, 14, 3, 7, 11, 10]),
}
for (coords, expeers) in expected_peers.items():
with self.subTest(coord=coords, ex=expeers):
peers = self.board.index_peers(*coords)
self.assertEqual(peers, expeers)
class Sudoku4SolvedTests(Sudoku4TestCase):
def test_that_an_empty_board_is_not_solved(self):
self.assertFalse(self.board.solved)
def test_simple_solution_is_solved(self):
board = (int(i) for i in '1234341221434321')
self.board = sudoku.Sudoku(2, board)
self.assertTrue(self.board.solved)

128
tests.py
View file

@ -1,128 +0,0 @@
#!/usr/bin/env python3
# Eryn Wells <eryn@erynwells.me>
'''
Tests for sudoku.
'''
import nose
import sudoku
import unittest
def test_9x9_dimensions():
'''
Test dimenions of a 9x9 Sudoku board.
'''
b = sudoku.Board()
assert len(b.keys()) == 81
assert b.size == 9
assert b.box_size == 3
for square, values in b.items():
assert len(values) == 9
def test_9x9_units():
'''
Test units of a 9x9 Sudoku board.
'''
b = sudoku.Board()
for r in range(b.size):
row = b.row(r)
assert len(row) == 9
for sq in row:
assert sq[1] == r
for c in range(b.size):
col = b.col(c)
assert len(col) == 9
for sq in col:
assert sq[0] == c
for x in range(0, b.size, b.box_size):
for y in range(0, b.size, b.box_size):
box = b.box(x, y)
assert len(box) == 9
# TODO: Finish this test
def test_9x9_row():
'''
A few tests on rows of a 9x9 Sudoku board.
'''
b = sudoku.Board()
row = b.row(1)
expected_keys = ((x, 1) for x in range(b.size))
for ekey in expected_keys:
assert ekey in row
# No negative numbers
assert (-1, 1) not in row
# Only squares with the right y-coordinate
assert (0, 0) not in row
# No keys above the size of the board
assert (b.size, 1) not in row
def test_9x9_col():
'''
A few tests on rows of a 9x9 Sudoku board.
'''
b = sudoku.Board()
col = b.col(3)
expected_keys = ((3, y) for y in range(b.size))
for ekey in expected_keys:
assert ekey in col
# No negative numbers
assert (3, -1) not in col
# Only squares with the right x-coordinate
assert (0, 0) not in col
# No keys above the size of the board
assert (3, b.size) not in col
def test_9x9_box():
'''
A few tests on boxes of a 9x9 Sudoku board.
'''
b = sudoku.Board()
assert True
def test_9x9_peers():
'''
Test peers.
'''
b = sudoku.Board()
peers = b.peers(3, 3)
expected_peers = set( [(x, 3) for x in range(b.size)]
+ [(3, y) for y in range(b.size)]
+ [(3, 3), (3, 4), (3, 5), (4, 3), (4, 4), (4, 5), (5, 3), (5, 4), (5, 5)])
expected_peers.remove((3, 3))
for epeer in expected_peers:
assert epeer in peers, '{} not in peers of (3, 3)'.format(epeer)
def test_9x9_str():
'''
Test string generation/printing.
'''
b = sudoku.Board()
expected_str = '\n'.join(['. . . | . . . | . . .',
'. . . | . . . | . . .',
'. . . | . . . | . . .',
'------+-------+------',
'. . . | . . . | . . .',
'. . . | . . . | . . .',
'. . . | . . . | . . .',
'------+-------+------',
'. . . | . . . | . . .',
'. . . | . . . | . . .',
'. . . | . . . | . . .'])
assert str(b) == expected_str
def main():
nose.main()
if __name__ == '__main__':
main()