Compare commits
53 commits
Author | SHA1 | Date | |
---|---|---|---|
2529533661 | |||
3689ac6974 | |||
c9a86d0fdc | |||
137e4e6436 | |||
fba52c7f09 | |||
1186305712 | |||
a7dda24015 | |||
3b380b17d2 | |||
a9a331df91 | |||
387ccacb8d | |||
d25a320c30 | |||
65f4453e78 | |||
189589c0e6 | |||
9ebabd4b56 | |||
7a0e31858f | |||
181bc7d61e | |||
f7b6fb053f | |||
1549269661 | |||
e329208db9 | |||
77456d5114 | |||
301a56904c | |||
0976ec8bb5 | |||
7809c38104 | |||
ed41930092 | |||
c0021ac2c2 | |||
f12263b197 | |||
b8eda77093 | |||
9534f04a08 | |||
6da7c2ecd6 | |||
a34987e3db | |||
e2eadf9da7 | |||
ab927ee41d | |||
a0594d1561 | |||
fb1066f716 | |||
e5d00debc7 | |||
35647fc8c9 | |||
a9a01d3fd4 | |||
cf43b02aa9 | |||
00225aca44 | |||
19a9bc9d37 | |||
93a4fecb26 | |||
3017b1e923 | |||
88f0e07c06 | |||
5a32743d7b | |||
eae05487b9 | |||
5696c087d8 | |||
b5ac48bc10 | |||
3fabd4b420 | |||
5ebe1f58ff | |||
01428e3972 | |||
f98a0f4f75 | |||
2feef04ad9 | |||
53629f4d56 |
10 changed files with 574 additions and 410 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*.pyc
|
||||
__pycache__/
|
93
puzzles.py
Normal file → Executable file
93
puzzles.py
Normal file → Executable 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())
|
||||
|
|
|
@ -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
237
sudoku.py
|
@ -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
230
sudoku/__init__.py
Normal 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
|
6
sudoku/solvers/__init__.py
Normal file
6
sudoku/solvers/__init__.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
# Eryn Wells <eryn@erynwells.me>
|
||||
'''
|
||||
A library of Sudoku solvers.
|
||||
'''
|
||||
|
||||
from . import backtracker, dlx
|
61
sudoku/solvers/backtracker.py
Normal file
61
sudoku/solvers/backtracker.py
Normal 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
137
sudoku/solvers/dlx.py
Normal 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
89
sudoku/test.py
Normal 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
128
tests.py
|
@ -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()
|
Loading…
Add table
Add a link
Reference in a new issue