Compare commits

..

22 commits
master ... old

Author SHA1 Message Date
e004d319e3 IDK what this WIP is 2017-10-06 20:27:01 -07:00
6cc3fed4b3 Implement search
- Remove __setitem__; it was getting in the way...
- deepcopy the board for each trial in search()
2013-08-30 17:09:41 -07:00
cc5e35ff81 Working on search() 2013-08-30 10:11:44 -07:00
81e3296f6f Create keys then assign values in __init__ 2013-08-29 22:46:38 -07:00
fdee9ed716 Couple tweaks to help with parsing Euler and Norvig puzzles
- Make xy_key and xy_kwargs_key classmethods
2013-08-29 22:32:15 -07:00
09c6c4287f Convert Euler puzzles into Norvig format 2013-08-29 22:32:05 -07:00
6a5e13e345 Add quick routines for parsing the puzzles in puzzles/ 2013-08-29 22:31:49 -07:00
3d69b67185 Fix printing -- had the x and y coordinates flipped 2013-08-29 22:05:16 -07:00
05976b51fb TODO and use self.possible_values instead of explicit range 2013-08-29 21:44:19 -07:00
Eryn Wells
9f4a775472 Add puzzles
Both via http://norvig.com/sudoku.html
- euler.txt: http://projecteuler.net/project/sudoku.txt
- norvig.txt: http://magictour.free.fr/top95
2013-08-29 16:32:24 -07:00
Eryn Wells
5a2df50e02 Add string test 2013-08-29 16:14:35 -07:00
Eryn Wells
7e43451eb5 Tests yay! 2013-08-29 15:58:43 -07:00
Eryn Wells
3c21549ee9 Bare bones tests module 2013-08-29 15:05:38 -07:00
Eryn Wells
a926bc5a99 Board.solve() and Board.assign() 2013-08-29 15:05:30 -07:00
Eryn Wells
d20a0bbdf7 Fix the divider printing logic (again) 2013-08-29 12:05:34 -07:00
cac812e46f Fix divider length computation 2013-08-29 11:57:31 -07:00
8540f60381 Use length of str(self.size) to compute size of each square in the printed grid 2013-08-29 11:49:34 -07:00
6a753a8dac Docstring updates 2013-08-29 09:52:52 -07:00
7fd051802c Implement the solved property 2013-08-29 09:42:06 -07:00
31415f8175 Ignore __pycache__/ 2013-08-28 23:10:27 -07:00
af92bb89f8 Ignore *.pyc 2013-08-28 23:10:15 -07:00
c120ddc4fa Add sudoku module -- work thus far 2013-08-28 23:09:55 -07:00
10 changed files with 410 additions and 574 deletions

2
.gitignore vendored Normal file
View file

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

93
puzzles.py Executable file → Normal file
View file

@ -1,67 +1,58 @@
#!env python3 #!/usr/bin/env python3
# Eryn Wells <eryn@erynwells.me> # Eryn Wells <eryn@erynwells.me>
''' '''
Parser for puzzles in the ./puzzles directory. Parser for puzzles in the ./puzzles directory.
''' '''
import argparse import sudoku
import os.path
import sys
from sudoku import Sudoku, solvers
euler = [] euler = []
norvig = [] 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 _get_puzzles(filename, quiet): def parse_euler(filename='./puzzles/euler.txt'):
with open(filename, 'r') as f: with open(filename, 'r') as f:
puzzles = f.readlines() puzzle_lines = f.readlines()
return (_parse_puzzle(p, quiet) for p in puzzles if p)
def _parse_puzzle(puzzle, quiet): for puzzle in puzzle_lines:
puzzle = puzzle.strip() # Chop the newline
if len(puzzle) == 81: if puzzle[-1] == '\n':
if not quiet: puzzle = puzzle[:-1]
print("Parsing '{}'".format(puzzle)) print('Parsing puzzle: {}'.format(puzzle))
board = (int('0' if x == '.' else x) for x in puzzle) if len(puzzle) != 81:
return Sudoku(board=board) continue
else: kwargs = {}
if not quiet: for idx in range(len(puzzle)):
print("Skipping '{}'".format(puzzle)) sq = puzzle[idx]
return None 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_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 main(): def parse_norvig(filename='./puzzles/norvig.txt'):
args = parse_args(sys.argv[1:]) with open(filename, 'r') as f:
puzzle_library = list(parse_puzzle_library(args.library, quiet=not args.verbose)) puzzle_lines = f.readlines()
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
if __name__ == '__main__': for puzzle in puzzle_lines:
sys.exit(main()) # 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))

View file

@ -93,3 +93,4 @@
.5..9....1.....6.....3.8.....8.4...9514.......3....2..........4.8...6..77..15..6. .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.......... .....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.. 3...8.......7....51..............36...2..4....7...........6.13..452...........8..


237
sudoku.py Normal file
View file

@ -0,0 +1,237 @@
#!/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)

View file

@ -1,230 +0,0 @@
#!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

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

View file

@ -1,61 +0,0 @@
# 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)

View file

@ -1,137 +0,0 @@
# 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

View file

@ -1,89 +0,0 @@
#!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 Normal file
View file

@ -0,0 +1,128 @@
#!/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()