From c120ddc4fa472b2430a431695e3ef58e12cb223b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 Aug 2013 23:09:55 -0700 Subject: [PATCH 01/75] Add sudoku module -- work thus far --- sudoku.py | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 sudoku.py diff --git a/sudoku.py b/sudoku.py new file mode 100644 index 0000000..5540925 --- /dev/null +++ b/sudoku.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# Eryn Wells + +''' +A Sudoku solver. +''' + +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. + possible_values = range(1, self.size + 1) + + def kwget(x, y): + ''' + Try to get an initial value for square ({x}, {y}) from the given kwargs. If none exists, return a list of + all possible values for the square. If an initial value was given, make sure it is one of the valid initial + values. Raise a ValueError if not. + ''' + initial_value = kwargs.get('x{}y{}'.format(x, y)) + if initial_value is None: + return list(possible_values) + if initial_value not in possible_values: + raise ValueError('Invalid initial value for square ({}, {}): {}'.format(x, y, initial_value)) + return [initial_value] + + # Make the grid. + super(Board, self).__init__([(self._xy_key(x, y), kwget(x, y)) + for x in range(self.size) + for y in range(self.size)]) + + 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)) + + 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 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[self._xy_key(x, y)] + return peers + + def clear(self): + ''' + Clear the board. + ''' + for square in self: + self[square] = list(range(1, self.size + 1)) + + def eliminate(self, square, value): + ''' + Eliminate {value} from square at ({x}, {y}). + ''' + if value not in self[square]: + LOG.debug('Value {} not in square {}; skipping'.format(value, square)) + return + + # 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)) + raise ValueError('Removed last value from square {}; board is now invalid'.format(square)) + 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])) + try: + for peer in self.peers(*square): + self.eliminate(peer, self[square][0]) + except ValueError: + raise + + # (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)) + raise ValueError('No place for value {} to go in unit {}; board is now invalid'.format(value, unit)) + elif len(places) == 1: + LOG.debug('One place for value {} to be in unit {}; setting'.format(value, unit)) + self[places[0]] = [value] + return True + + def __delitem__(self, key): + # Don't allow deleting keys from self. + pass + + def __setitem__(self, key, value): + #if key not in self: + # Don't allow adding new keys, only changes to existing ones. + #return + LOG.debug('Setting value {} at {}.'.format(value, key)) + removed_values = set(self[key]) - set(value) + for v in removed_values: + self.eliminate(key, v) + + def __str__(self): + lines = [] + box_lines = [] + for x in range(self.size): + row_squares = [] + box_squares = [] + for y in range(self.size): + square = self.get(self._xy_key(x, y)) + if len(square) == 1: + box_squares.append(str(square[0])) + else: + box_squares.append('.') + 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 x < self.size - 1: + box_dividers = ['-' * (2 * self.box_size - 1) for box in range(self.box_size)] + lines.append('\n{}\n'.format('-+-'.join(box_dividers))) + return ''.join(lines) + + From af92bb89f82591359be1ec6d97fac10aa465aed4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 Aug 2013 23:10:15 -0700 Subject: [PATCH 02/75] Ignore *.pyc --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From 31415f81754b466055161927372562aeeacb3f4b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 28 Aug 2013 23:10:27 -0700 Subject: [PATCH 03/75] Ignore __pycache__/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d20b64..d646835 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +__pycache__/ From 7fd051802c0719dfb56c240e3bd59404eda6035e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 09:42:06 -0700 Subject: [PATCH 04/75] Implement the solved property --- sudoku.py | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/sudoku.py b/sudoku.py index 5540925..e810099 100644 --- a/sudoku.py +++ b/sudoku.py @@ -21,7 +21,7 @@ class Board(dict): assert self.box_size == int(self.box_size), 'Invalid size; value must be a perfect square' # The range of possible values for a square. - possible_values = range(1, self.size + 1) + self.possible_values = range(1, self.size + 1) def kwget(x, y): ''' @@ -31,8 +31,8 @@ class Board(dict): ''' initial_value = kwargs.get('x{}y{}'.format(x, y)) if initial_value is None: - return list(possible_values) - if initial_value not in possible_values: + return list(self.possible_values) + if initial_value not in self.possible_values: raise ValueError('Invalid initial value for square ({}, {}): {}'.format(x, y, initial_value)) return [initial_value] @@ -42,11 +42,41 @@ class Board(dict): for y in range(self.size)]) 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. - ''' + '''Given {x} and {y}, generate a key to refer to the square at coordinate (x, y) in the grid.''' return (int(x), int(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. From 6a753a8dacc27ed0fcb566531242b1f69cf64fa3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 09:52:52 -0700 Subject: [PATCH 05/75] Docstring updates --- sudoku.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/sudoku.py b/sudoku.py index e810099..0eb3b34 100644 --- a/sudoku.py +++ b/sudoku.py @@ -105,6 +105,10 @@ class Board(dict): 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. @@ -113,14 +117,17 @@ class Board(dict): def clear(self): ''' - Clear the board. + Clear the board. Resets each square's possible value list to the full range of possible values for this board. ''' for square in self: self[square] = list(range(1, self.size + 1)) def eliminate(self, square, value): ''' - Eliminate {value} from square at ({x}, {y}). + 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)) @@ -160,9 +167,15 @@ class Board(dict): pass def __setitem__(self, key, value): - #if key not in self: + ''' + Set the square defined by {key}'s value to {value}. {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. + ''' + if key not in self: # Don't allow adding new keys, only changes to existing ones. - #return + raise KeyError('Key {} is not a valid coordinate pair.'.format(key)) + + # Remove all values other than the one given from the square. LOG.debug('Setting value {} at {}.'.format(value, key)) removed_values = set(self[key]) - set(value) for v in removed_values: From 8540f60381042265dc883e9e5e9f5efdbbb04586 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 11:49:34 -0700 Subject: [PATCH 06/75] Use length of str(self.size) to compute size of each square in the printed grid --- sudoku.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sudoku.py b/sudoku.py index 0eb3b34..6c4ec6c 100644 --- a/sudoku.py +++ b/sudoku.py @@ -184,15 +184,16 @@ class Board(dict): def __str__(self): lines = [] box_lines = [] + square_size = len(str(self.size)) for x in range(self.size): row_squares = [] box_squares = [] for y in range(self.size): square = self.get(self._xy_key(x, y)) if len(square) == 1: - box_squares.append(str(square[0])) + box_squares.append(str(square[0]).center(square_size)) else: - box_squares.append('.') + box_squares.append('.'.center(square_size)) if len(box_squares) == self.box_size: row_squares.append(' '.join(box_squares)) box_squares = [] @@ -202,7 +203,7 @@ class Board(dict): lines.append('\n'.join(box_lines)) box_lines = [] if x < self.size - 1: - box_dividers = ['-' * (2 * self.box_size - 1) for box in range(self.box_size)] + box_dividers = ['-' * (square_size * (2 * self.box_size - 1)) for box in range(self.box_size)] lines.append('\n{}\n'.format('-+-'.join(box_dividers))) return ''.join(lines) From cac812e46f2c0c097199f87a95fc0a7d5e60dd9d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 11:57:31 -0700 Subject: [PATCH 07/75] Fix divider length computation --- sudoku.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index 6c4ec6c..4376269 100644 --- a/sudoku.py +++ b/sudoku.py @@ -203,7 +203,7 @@ class Board(dict): lines.append('\n'.join(box_lines)) box_lines = [] if x < self.size - 1: - box_dividers = ['-' * (square_size * (2 * self.box_size - 1)) for box in range(self.box_size)] + box_dividers = ['-' * (square_size * self.box_size + square_size)) for box in range(self.box_size)] lines.append('\n{}\n'.format('-+-'.join(box_dividers))) return ''.join(lines) From d20a0bbdf74a2dea0601c5de116b93be9efaaa80 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 12:05:34 -0700 Subject: [PATCH 08/75] Fix the divider printing logic (again) --- sudoku.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index 4376269..5dfd47d 100644 --- a/sudoku.py +++ b/sudoku.py @@ -203,7 +203,8 @@ class Board(dict): lines.append('\n'.join(box_lines)) box_lines = [] if x < self.size - 1: - box_dividers = ['-' * (square_size * self.box_size + square_size)) for box in range(self.box_size)] + # 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) From a926bc5a99e0d69a485897ea70b50d5e9e636fca Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 15:05:30 -0700 Subject: [PATCH 09/75] Board.solve() and Board.assign() --- sudoku.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/sudoku.py b/sudoku.py index 5dfd47d..9da9aa5 100644 --- a/sudoku.py +++ b/sudoku.py @@ -122,6 +122,29 @@ class Board(dict): for square in self: self[square] = list(range(1, self.size + 1)) + def solve(self): + for square, values in self.items(): + if len(values) == 1: + try: + self.assign(square, values) + except ValueError: + raise + return self.solved + + 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: + try: + self.eliminate(square, v) + except ValueError: + raise + return True + def eliminate(self, square, value): ''' Eliminate {value} from {square}, propagating changes to the squares peers as necessary. This process involves @@ -131,7 +154,7 @@ class Board(dict): ''' if value not in self[square]: LOG.debug('Value {} not in square {}; skipping'.format(value, square)) - return + return True # Update peer value list. super(Board, self).__setitem__(square, [v for v in self[square] if v != value]) @@ -167,19 +190,10 @@ class Board(dict): pass def __setitem__(self, key, value): - ''' - Set the square defined by {key}'s value to {value}. {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. - ''' if key not in self: # Don't allow adding new keys, only changes to existing ones. raise KeyError('Key {} is not a valid coordinate pair.'.format(key)) - - # Remove all values other than the one given from the square. - LOG.debug('Setting value {} at {}.'.format(value, key)) - removed_values = set(self[key]) - set(value) - for v in removed_values: - self.eliminate(key, v) + self.assign(key, value) def __str__(self): lines = [] From 3c21549ee93d89b5b0c626249b3fa28a863459ac Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 15:05:38 -0700 Subject: [PATCH 10/75] Bare bones tests module --- tests.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests.py diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..cd64084 --- /dev/null +++ b/tests.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +# Eryn Wells + +''' +Tests for sudoku. +''' + +import nose +import sudoku +import unittest + + +def main(): + nose.main() + + +if __name__ == '__main__': + main() From 7e43451eb5037a1bfdec1b1272c23b71cfa19831 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 15:58:43 -0700 Subject: [PATCH 11/75] Tests yay! --- tests.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests.py b/tests.py index cd64084..d413bb1 100644 --- a/tests.py +++ b/tests.py @@ -10,6 +10,97 @@ 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 main(): nose.main() From 5a2df50e02b239e637c83c45eb1c4c2a59d62ddb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 16:14:35 -0700 Subject: [PATCH 12/75] Add string test --- tests.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests.py b/tests.py index d413bb1..32dabd1 100644 --- a/tests.py +++ b/tests.py @@ -101,6 +101,25 @@ def test_9x9_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() From 9f4a775472ebb7143c4c263171588d60ba52e0fc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 16:32:24 -0700 Subject: [PATCH 13/75] Add puzzles Both via http://norvig.com/sudoku.html - euler.txt: http://projecteuler.net/project/sudoku.txt - norvig.txt: http://magictour.free.fr/top95 --- puzzles/euler.txt | 500 +++++++++++++++++++++++++++++++++++++++++++++ puzzles/norvig.txt | 96 +++++++++ 2 files changed, 596 insertions(+) create mode 100644 puzzles/euler.txt create mode 100644 puzzles/norvig.txt diff --git a/puzzles/euler.txt b/puzzles/euler.txt new file mode 100644 index 0000000..be23f6a --- /dev/null +++ b/puzzles/euler.txt @@ -0,0 +1,500 @@ +Grid 01 +003020600 +900305001 +001806400 +008102900 +700000008 +006708200 +002609500 +800203009 +005010300 +Grid 02 +200080300 +060070084 +030500209 +000105408 +000000000 +402706000 +301007040 +720040060 +004010003 +Grid 03 +000000907 +000420180 +000705026 +100904000 +050000040 +000507009 +920108000 +034059000 +507000000 +Grid 04 +030050040 +008010500 +460000012 +070502080 +000603000 +040109030 +250000098 +001020600 +080060020 +Grid 05 +020810740 +700003100 +090002805 +009040087 +400208003 +160030200 +302700060 +005600008 +076051090 +Grid 06 +100920000 +524010000 +000000070 +050008102 +000000000 +402700090 +060000000 +000030945 +000071006 +Grid 07 +043080250 +600000000 +000001094 +900004070 +000608000 +010200003 +820500000 +000000005 +034090710 +Grid 08 +480006902 +002008001 +900370060 +840010200 +003704100 +001060049 +020085007 +700900600 +609200018 +Grid 09 +000900002 +050123400 +030000160 +908000000 +070000090 +000000205 +091000050 +007439020 +400007000 +Grid 10 +001900003 +900700160 +030005007 +050000009 +004302600 +200000070 +600100030 +042007006 +500006800 +Grid 11 +000125400 +008400000 +420800000 +030000095 +060902010 +510000060 +000003049 +000007200 +001298000 +Grid 12 +062340750 +100005600 +570000040 +000094800 +400000006 +005830000 +030000091 +006400007 +059083260 +Grid 13 +300000000 +005009000 +200504000 +020000700 +160000058 +704310600 +000890100 +000067080 +000005437 +Grid 14 +630000000 +000500008 +005674000 +000020000 +003401020 +000000345 +000007004 +080300902 +947100080 +Grid 15 +000020040 +008035000 +000070602 +031046970 +200000000 +000501203 +049000730 +000000010 +800004000 +Grid 16 +361025900 +080960010 +400000057 +008000471 +000603000 +259000800 +740000005 +020018060 +005470329 +Grid 17 +050807020 +600010090 +702540006 +070020301 +504000908 +103080070 +900076205 +060090003 +080103040 +Grid 18 +080005000 +000003457 +000070809 +060400903 +007010500 +408007020 +901020000 +842300000 +000100080 +Grid 19 +003502900 +000040000 +106000305 +900251008 +070408030 +800763001 +308000104 +000020000 +005104800 +Grid 20 +000000000 +009805100 +051907420 +290401065 +000000000 +140508093 +026709580 +005103600 +000000000 +Grid 21 +020030090 +000907000 +900208005 +004806500 +607000208 +003102900 +800605007 +000309000 +030020050 +Grid 22 +005000006 +070009020 +000500107 +804150000 +000803000 +000092805 +907006000 +030400010 +200000600 +Grid 23 +040000050 +001943600 +009000300 +600050002 +103000506 +800020007 +005000200 +002436700 +030000040 +Grid 24 +004000000 +000030002 +390700080 +400009001 +209801307 +600200008 +010008053 +900040000 +000000800 +Grid 25 +360020089 +000361000 +000000000 +803000602 +400603007 +607000108 +000000000 +000418000 +970030014 +Grid 26 +500400060 +009000800 +640020000 +000001008 +208000501 +700500000 +000090084 +003000600 +060003002 +Grid 27 +007256400 +400000005 +010030060 +000508000 +008060200 +000107000 +030070090 +200000004 +006312700 +Grid 28 +000000000 +079050180 +800000007 +007306800 +450708096 +003502700 +700000005 +016030420 +000000000 +Grid 29 +030000080 +009000500 +007509200 +700105008 +020090030 +900402001 +004207100 +002000800 +070000090 +Grid 30 +200170603 +050000100 +000006079 +000040700 +000801000 +009050000 +310400000 +005000060 +906037002 +Grid 31 +000000080 +800701040 +040020030 +374000900 +000030000 +005000321 +010060050 +050802006 +080000000 +Grid 32 +000000085 +000210009 +960080100 +500800016 +000000000 +890006007 +009070052 +300054000 +480000000 +Grid 33 +608070502 +050608070 +002000300 +500090006 +040302050 +800050003 +005000200 +010704090 +409060701 +Grid 34 +050010040 +107000602 +000905000 +208030501 +040070020 +901080406 +000401000 +304000709 +020060010 +Grid 35 +053000790 +009753400 +100000002 +090080010 +000907000 +080030070 +500000003 +007641200 +061000940 +Grid 36 +006080300 +049070250 +000405000 +600317004 +007000800 +100826009 +000702000 +075040190 +003090600 +Grid 37 +005080700 +700204005 +320000084 +060105040 +008000500 +070803010 +450000091 +600508007 +003010600 +Grid 38 +000900800 +128006400 +070800060 +800430007 +500000009 +600079008 +090004010 +003600284 +001007000 +Grid 39 +000080000 +270000054 +095000810 +009806400 +020403060 +006905100 +017000620 +460000038 +000090000 +Grid 40 +000602000 +400050001 +085010620 +038206710 +000000000 +019407350 +026040530 +900020007 +000809000 +Grid 41 +000900002 +050123400 +030000160 +908000000 +070000090 +000000205 +091000050 +007439020 +400007000 +Grid 42 +380000000 +000400785 +009020300 +060090000 +800302009 +000040070 +001070500 +495006000 +000000092 +Grid 43 +000158000 +002060800 +030000040 +027030510 +000000000 +046080790 +050000080 +004070100 +000325000 +Grid 44 +010500200 +900001000 +002008030 +500030007 +008000500 +600080004 +040100700 +000700006 +003004050 +Grid 45 +080000040 +000469000 +400000007 +005904600 +070608030 +008502100 +900000005 +000781000 +060000010 +Grid 46 +904200007 +010000000 +000706500 +000800090 +020904060 +040002000 +001607000 +000000030 +300005702 +Grid 47 +000700800 +006000031 +040002000 +024070000 +010030080 +000060290 +000800070 +860000500 +002006000 +Grid 48 +001007090 +590080001 +030000080 +000005800 +050060020 +004100000 +080000030 +100020079 +020700400 +Grid 49 +000003017 +015009008 +060000000 +100007000 +009000200 +000500004 +000000020 +500600340 +340200000 +Grid 50 +300200000 +000107000 +706030500 +070009080 +900020004 +010800050 +009040301 +000702000 +000008006 \ No newline at end of file diff --git a/puzzles/norvig.txt b/puzzles/norvig.txt new file mode 100644 index 0000000..c8429cc --- /dev/null +++ b/puzzles/norvig.txt @@ -0,0 +1,96 @@ +4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4...... +52...6.........7.13...........4..8..6......5...........418.........3..2...87..... +6.....8.3.4.7.................5.4.7.3..2.....1.6.......2.....5.....8.6......1.... +48.3............71.2.......7.5....6....2..8.............1.76...3.....4......5.... +....14....3....2...7..........9...3.6.1.............8.2.....1.4....5.6.....7.8... +......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3. +6.2.5.........3.4..........43...8....1....2........7..5..27...........81...6..... +.524.........7.1..............8.2...3.....6...9.5.....1.6.3...........897........ +6.2.5.........4.3..........43...8....1....2........7..5..27...........81...6..... +.923.........8.1...........1.7.4...........658.........6.5.2...4.....7.....9..... +6..3.2....5.....1..........7.26............543.........8.15........4.2........7.. +.6.5.1.9.1...9..539....7....4.8...7.......5.8.817.5.3.....5.2............76..8... +..5...987.4..5...1..7......2...48....9.1.....6..2.....3..6..2.......9.7.......5.. +3.6.7...........518.........1.4.5...7.....6.....2......2.....4.....8.3.....5..... +1.....3.8.7.4..............2.3.1...........958.........5.6...7.....8.2...4....... +6..3.2....4.....1..........7.26............543.........8.15........4.2........7.. +....3..9....2....1.5.9..............1.2.8.4.6.8.5...2..75......4.1..6..3.....4.6. +45.....3....8.1....9...........5..9.2..7.....8.........1..4..........7.2...6..8.. +.237....68...6.59.9.....7......4.97.3.7.96..2.........5..47.........2....8....... +..84...3....3.....9....157479...8........7..514.....2...9.6...2.5....4......9..56 +.98.1....2......6.............3.2.5..84.........6.........4.8.93..5...........1.. +..247..58..............1.4.....2...9528.9.4....9...1.........3.3....75..685..2... +4.....8.5.3..........7......2.....6.....5.4......1.......6.3.7.5..2.....1.9...... +.2.3......63.....58.......15....9.3....7........1....8.879..26......6.7...6..7..4 +1.....7.9.4...72..8.........7..1..6.3.......5.6..4..2.........8..53...7.7.2....46 +4.....3.....8.2......7........1...8734.......6........5...6........1.4...82...... +.......71.2.8........4.3...7...6..5....2..3..9........6...7.....8....4......5.... +6..3.2....4.....8..........7.26............543.........8.15........8.2........7.. +.47.8...1............6..7..6....357......5....1..6....28..4.....9.1...4.....2.69. +......8.17..2........5.6......7...5..1....3...8.......5......2..4..8....6...3.... +38.6.......9.......2..3.51......5....3..1..6....4......17.5..8.......9.......7.32 +...5...........5.697.....2...48.2...25.1...3..8..3.........4.7..13.5..9..2...31.. +.2.......3.5.62..9.68...3...5..........64.8.2..47..9....3.....1.....6...17.43.... +.8..4....3......1........2...5...4.69..1..8..2...........3.9....6....5.....2..... +..8.9.1...6.5...2......6....3.1.7.5.........9..4...3...5....2...7...3.8.2..7....4 +4.....5.8.3..........7......2.....6.....5.8......1.......6.3.7.5..2.....1.8...... +1.....3.8.6.4..............2.3.1...........958.........5.6...7.....8.2...4....... +1....6.8..64..........4...7....9.6...7.4..5..5...7.1...5....32.3....8...4........ +249.6...3.3....2..8.......5.....6......2......1..4.82..9.5..7....4.....1.7...3... +...8....9.873...4.6..7.......85..97...........43..75.......3....3...145.4....2..1 +...5.1....9....8...6.......4.1..........7..9........3.8.....1.5...2..4.....36.... +......8.16..2........7.5......6...2..1....3...8.......2......7..3..8....5...4.... +.476...5.8.3.....2.....9......8.5..6...1.....6.24......78...51...6....4..9...4..7 +.....7.95.....1...86..2.....2..73..85......6...3..49..3.5...41724................ +.4.5.....8...9..3..76.2.....146..........9..7.....36....1..4.5..6......3..71..2.. +.834.........7..5...........4.1.8..........27...3.....2.6.5....5.....8........1.. +..9.....3.....9...7.....5.6..65..4.....3......28......3..75.6..6...........12.3.8 +.26.39......6....19.....7.......4..9.5....2....85.....3..2..9..4....762.........4 +2.3.8....8..7...........1...6.5.7...4......3....1............82.5....6...1....... +6..3.2....1.....5..........7.26............843.........8.15........8.2........7.. +1.....9...64..1.7..7..4.......3.....3.89..5....7....2.....6.7.9.....4.1....129.3. +.........9......84.623...5....6...453...1...6...9...7....1.....4.5..2....3.8....9 +.2....5938..5..46.94..6...8..2.3.....6..8.73.7..2.........4.38..7....6..........5 +9.4..5...25.6..1..31......8.7...9...4..26......147....7.......2...3..8.6.4.....9. +...52.....9...3..4......7...1.....4..8..453..6...1...87.2........8....32.4..8..1. +53..2.9...24.3..5...9..........1.827...7.........981.............64....91.2.5.43. +1....786...7..8.1.8..2....9........24...1......9..5...6.8..........5.9.......93.4 +....5...11......7..6.....8......4.....9.1.3.....596.2..8..62..7..7......3.5.7.2.. +.47.2....8....1....3....9.2.....5...6..81..5.....4.....7....3.4...9...1.4..27.8.. +......94.....9...53....5.7..8.4..1..463...........7.8.8..7.....7......28.5.26.... +.2......6....41.....78....1......7....37.....6..412....1..74..5..8.5..7......39.. +1.....3.8.6.4..............2.3.1...........758.........7.5...6.....8.2...4....... +2....1.9..1..3.7..9..8...2.......85..6.4.........7...3.2.3...6....5.....1.9...2.5 +..7..8.....6.2.3...3......9.1..5..6.....1.....7.9....2........4.83..4...26....51. +...36....85.......9.4..8........68.........17..9..45...1.5...6.4....9..2.....3... +34.6.......7.......2..8.57......5....7..1..2....4......36.2..1.......9.......7.82 +......4.18..2........6.7......8...6..4....3...1.......6......2..5..1....7...3.... +.4..5..67...1...4....2.....1..8..3........2...6...........4..5.3.....8..2........ +.......4...2..4..1.7..5..9...3..7....4..6....6..1..8...2....1..85.9...6.....8...3 +8..7....4.5....6............3.97...8....43..5....2.9....6......2...6...7.71..83.2 +.8...4.5....7..3............1..85...6.....2......4....3.26............417........ +....7..8...6...5...2...3.61.1...7..2..8..534.2..9.......2......58...6.3.4...1.... +......8.16..2........7.5......6...2..1....3...8.......2......7..4..8....5...3.... +.2..........6....3.74.8.........3..2.8..4..1.6..5.........1.78.5....9..........4. +.52..68.......7.2.......6....48..9..2..41......1.....8..61..38.....9...63..6..1.9 +....1.78.5....9..........4..2..........6....3.74.8.........3..2.8..4..1.6..5..... +1.......3.6.3..7...7...5..121.7...9...7........8.1..2....8.64....9.2..6....4..... +4...7.1....19.46.5.....1......7....2..2.3....847..6....14...8.6.2....3..6...9.... +......8.17..2........5.6......7...5..1....3...8.......5......2..3..8....6...4.... +963......1....8......2.5....4.8......1....7......3..257......3...9.2.4.7......9.. +15.3......7..4.2....4.72.....8.........9..1.8.1..8.79......38...........6....7423 +..........5724...98....947...9..3...5..9..12...3.1.9...6....25....56.....7......6 +....75....1..2.....4...3...5.....3.2...8...1.......6.....1..48.2........7........ +6.....7.3.4.8.................5.4.8.7..2.....1.3.......2.....5.....7.9......1.... +....6...4..6.3....1..4..5.77.....8.5...8.....6.8....9...2.9....4....32....97..1.. +.32.....58..3.....9.428...1...4...39...6...5.....1.....2...67.8.....4....95....6. +...5.3.......6.7..5.8....1636..2.......4.1.......3...567....2.8..4.7.......2..5.. +.5.3.7.4.1.........3.......5.8.3.61....8..5.9.6..1........4...6...6927....2...9.. +..5..8..18......9.......78....4.....64....9......53..2.6.........138..5....9.714. +..........72.6.1....51...82.8...13..4.........37.9..1.....238..5.4..9.........79. +...658.....4......12............96.7...3..5....2.8...3..19..8..3.6.....4....473.. +.2.3.......6..8.9.83.5........2...8.7.9..5........6..4.......1...1...4.22..7..8.9 +.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.. + \ No newline at end of file From 05976b51fbd1a53047d5eaa53790e268b55b051d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 21:44:19 -0700 Subject: [PATCH 14/75] TODO and use self.possible_values instead of explicit range --- sudoku.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index 9da9aa5..b5ca47a 100644 --- a/sudoku.py +++ b/sudoku.py @@ -120,7 +120,7 @@ class Board(dict): Clear the board. Resets each square's possible value list to the full range of possible values for this board. ''' for square in self: - self[square] = list(range(1, self.size + 1)) + self[square] = list(self.possible_values) def solve(self): for square, values in self.items(): @@ -143,6 +143,8 @@ class Board(dict): self.eliminate(square, v) except ValueError: raise + # TODO: To make this work right, I should probably also do the same as above for the values added *back* into + # the values for {square}. return True def eliminate(self, square, value): From 3d69b671851c12a9b2b16f82f1a41dca51acb102 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 22:05:16 -0700 Subject: [PATCH 15/75] Fix printing -- had the x and y coordinates flipped --- sudoku.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sudoku.py b/sudoku.py index b5ca47a..e47228d 100644 --- a/sudoku.py +++ b/sudoku.py @@ -201,10 +201,10 @@ class Board(dict): lines = [] box_lines = [] square_size = len(str(self.size)) - for x in range(self.size): + for y in range(self.size): row_squares = [] box_squares = [] - for y in range(self.size): + for x in range(self.size): square = self.get(self._xy_key(x, y)) if len(square) == 1: box_squares.append(str(square[0]).center(square_size)) @@ -218,7 +218,7 @@ class Board(dict): if len(box_lines) == self.box_size: lines.append('\n'.join(box_lines)) box_lines = [] - if x < self.size - 1: + 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))) From 6a5e13e345454af84b4ab92b870ab3c542914d4d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 22:31:49 -0700 Subject: [PATCH 16/75] Add quick routines for parsing the puzzles in puzzles/ --- puzzles.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 puzzles.py diff --git a/puzzles.py b/puzzles.py new file mode 100644 index 0000000..62f5199 --- /dev/null +++ b/puzzles.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Eryn Wells + +''' +Parser for puzzles in the ./puzzles directory. +''' + +import sudoku + + +euler = [] +norvig = [] + + +def parse_euler(filename='./puzzles/euler.txt'): + with open(filename, 'r') as f: + puzzle_lines = f.readlines() + + 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_norvig(filename='./puzzles/norvig.txt'): + with open(filename, 'r') as f: + puzzle_lines = f.readlines() + + 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)) From 09c6c4287f3be99a7bef1f94d6da50b6f865adf8 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 22:32:05 -0700 Subject: [PATCH 17/75] Convert Euler puzzles into Norvig format --- puzzles/euler.txt | 550 +++++----------------------------------------- 1 file changed, 50 insertions(+), 500 deletions(-) diff --git a/puzzles/euler.txt b/puzzles/euler.txt index be23f6a..b0bb576 100644 --- a/puzzles/euler.txt +++ b/puzzles/euler.txt @@ -1,500 +1,50 @@ -Grid 01 -003020600 -900305001 -001806400 -008102900 -700000008 -006708200 -002609500 -800203009 -005010300 -Grid 02 -200080300 -060070084 -030500209 -000105408 -000000000 -402706000 -301007040 -720040060 -004010003 -Grid 03 -000000907 -000420180 -000705026 -100904000 -050000040 -000507009 -920108000 -034059000 -507000000 -Grid 04 -030050040 -008010500 -460000012 -070502080 -000603000 -040109030 -250000098 -001020600 -080060020 -Grid 05 -020810740 -700003100 -090002805 -009040087 -400208003 -160030200 -302700060 -005600008 -076051090 -Grid 06 -100920000 -524010000 -000000070 -050008102 -000000000 -402700090 -060000000 -000030945 -000071006 -Grid 07 -043080250 -600000000 -000001094 -900004070 -000608000 -010200003 -820500000 -000000005 -034090710 -Grid 08 -480006902 -002008001 -900370060 -840010200 -003704100 -001060049 -020085007 -700900600 -609200018 -Grid 09 -000900002 -050123400 -030000160 -908000000 -070000090 -000000205 -091000050 -007439020 -400007000 -Grid 10 -001900003 -900700160 -030005007 -050000009 -004302600 -200000070 -600100030 -042007006 -500006800 -Grid 11 -000125400 -008400000 -420800000 -030000095 -060902010 -510000060 -000003049 -000007200 -001298000 -Grid 12 -062340750 -100005600 -570000040 -000094800 -400000006 -005830000 -030000091 -006400007 -059083260 -Grid 13 -300000000 -005009000 -200504000 -020000700 -160000058 -704310600 -000890100 -000067080 -000005437 -Grid 14 -630000000 -000500008 -005674000 -000020000 -003401020 -000000345 -000007004 -080300902 -947100080 -Grid 15 -000020040 -008035000 -000070602 -031046970 -200000000 -000501203 -049000730 -000000010 -800004000 -Grid 16 -361025900 -080960010 -400000057 -008000471 -000603000 -259000800 -740000005 -020018060 -005470329 -Grid 17 -050807020 -600010090 -702540006 -070020301 -504000908 -103080070 -900076205 -060090003 -080103040 -Grid 18 -080005000 -000003457 -000070809 -060400903 -007010500 -408007020 -901020000 -842300000 -000100080 -Grid 19 -003502900 -000040000 -106000305 -900251008 -070408030 -800763001 -308000104 -000020000 -005104800 -Grid 20 -000000000 -009805100 -051907420 -290401065 -000000000 -140508093 -026709580 -005103600 -000000000 -Grid 21 -020030090 -000907000 -900208005 -004806500 -607000208 -003102900 -800605007 -000309000 -030020050 -Grid 22 -005000006 -070009020 -000500107 -804150000 -000803000 -000092805 -907006000 -030400010 -200000600 -Grid 23 -040000050 -001943600 -009000300 -600050002 -103000506 -800020007 -005000200 -002436700 -030000040 -Grid 24 -004000000 -000030002 -390700080 -400009001 -209801307 -600200008 -010008053 -900040000 -000000800 -Grid 25 -360020089 -000361000 -000000000 -803000602 -400603007 -607000108 -000000000 -000418000 -970030014 -Grid 26 -500400060 -009000800 -640020000 -000001008 -208000501 -700500000 -000090084 -003000600 -060003002 -Grid 27 -007256400 -400000005 -010030060 -000508000 -008060200 -000107000 -030070090 -200000004 -006312700 -Grid 28 -000000000 -079050180 -800000007 -007306800 -450708096 -003502700 -700000005 -016030420 -000000000 -Grid 29 -030000080 -009000500 -007509200 -700105008 -020090030 -900402001 -004207100 -002000800 -070000090 -Grid 30 -200170603 -050000100 -000006079 -000040700 -000801000 -009050000 -310400000 -005000060 -906037002 -Grid 31 -000000080 -800701040 -040020030 -374000900 -000030000 -005000321 -010060050 -050802006 -080000000 -Grid 32 -000000085 -000210009 -960080100 -500800016 -000000000 -890006007 -009070052 -300054000 -480000000 -Grid 33 -608070502 -050608070 -002000300 -500090006 -040302050 -800050003 -005000200 -010704090 -409060701 -Grid 34 -050010040 -107000602 -000905000 -208030501 -040070020 -901080406 -000401000 -304000709 -020060010 -Grid 35 -053000790 -009753400 -100000002 -090080010 -000907000 -080030070 -500000003 -007641200 -061000940 -Grid 36 -006080300 -049070250 -000405000 -600317004 -007000800 -100826009 -000702000 -075040190 -003090600 -Grid 37 -005080700 -700204005 -320000084 -060105040 -008000500 -070803010 -450000091 -600508007 -003010600 -Grid 38 -000900800 -128006400 -070800060 -800430007 -500000009 -600079008 -090004010 -003600284 -001007000 -Grid 39 -000080000 -270000054 -095000810 -009806400 -020403060 -006905100 -017000620 -460000038 -000090000 -Grid 40 -000602000 -400050001 -085010620 -038206710 -000000000 -019407350 -026040530 -900020007 -000809000 -Grid 41 -000900002 -050123400 -030000160 -908000000 -070000090 -000000205 -091000050 -007439020 -400007000 -Grid 42 -380000000 -000400785 -009020300 -060090000 -800302009 -000040070 -001070500 -495006000 -000000092 -Grid 43 -000158000 -002060800 -030000040 -027030510 -000000000 -046080790 -050000080 -004070100 -000325000 -Grid 44 -010500200 -900001000 -002008030 -500030007 -008000500 -600080004 -040100700 -000700006 -003004050 -Grid 45 -080000040 -000469000 -400000007 -005904600 -070608030 -008502100 -900000005 -000781000 -060000010 -Grid 46 -904200007 -010000000 -000706500 -000800090 -020904060 -040002000 -001607000 -000000030 -300005702 -Grid 47 -000700800 -006000031 -040002000 -024070000 -010030080 -000060290 -000800070 -860000500 -002006000 -Grid 48 -001007090 -590080001 -030000080 -000005800 -050060020 -004100000 -080000030 -100020079 -020700400 -Grid 49 -000003017 -015009008 -060000000 -100007000 -009000200 -000500004 -000000020 -500600340 -340200000 -Grid 50 -300200000 -000107000 -706030500 -070009080 -900020004 -010800050 -009040301 -000702000 -000008006 \ No newline at end of file +003020600900305001001806400008102900700000008006708200002609500800203009005010300 +200080300060070084030500209000105408000000000402706000301007040720040060004010003 +000000907000420180000705026100904000050000040000507009920108000034059000507000000 +030050040008010500460000012070502080000603000040109030250000098001020600080060020 +020810740700003100090002805009040087400208003160030200302700060005600008076051090 +100920000524010000000000070050008102000000000402700090060000000000030945000071006 +043080250600000000000001094900004070000608000010200003820500000000000005034090710 +480006902002008001900370060840010200003704100001060049020085007700900600609200018 +000900002050123400030000160908000000070000090000000205091000050007439020400007000 +001900003900700160030005007050000009004302600200000070600100030042007006500006800 +000125400008400000420800000030000095060902010510000060000003049000007200001298000 +062340750100005600570000040000094800400000006005830000030000091006400007059083260 +300000000005009000200504000020000700160000058704310600000890100000067080000005437 +630000000000500008005674000000020000003401020000000345000007004080300902947100080 +000020040008035000000070602031046970200000000000501203049000730000000010800004000 +361025900080960010400000057008000471000603000259000800740000005020018060005470329 +050807020600010090702540006070020301504000908103080070900076205060090003080103040 +080005000000003457000070809060400903007010500408007020901020000842300000000100080 +003502900000040000106000305900251008070408030800763001308000104000020000005104800 +000000000009805100051907420290401065000000000140508093026709580005103600000000000 +020030090000907000900208005004806500607000208003102900800605007000309000030020050 +005000006070009020000500107804150000000803000000092805907006000030400010200000600 +040000050001943600009000300600050002103000506800020007005000200002436700030000040 +004000000000030002390700080400009001209801307600200008010008053900040000000000800 +360020089000361000000000000803000602400603007607000108000000000000418000970030014 +500400060009000800640020000000001008208000501700500000000090084003000600060003002 +007256400400000005010030060000508000008060200000107000030070090200000004006312700 +000000000079050180800000007007306800450708096003502700700000005016030420000000000 +030000080009000500007509200700105008020090030900402001004207100002000800070000090 +200170603050000100000006079000040700000801000009050000310400000005000060906037002 +000000080800701040040020030374000900000030000005000321010060050050802006080000000 +000000085000210009960080100500800016000000000890006007009070052300054000480000000 +608070502050608070002000300500090006040302050800050003005000200010704090409060701 +050010040107000602000905000208030501040070020901080406000401000304000709020060010 +053000790009753400100000002090080010000907000080030070500000003007641200061000940 +006080300049070250000405000600317004007000800100826009000702000075040190003090600 +005080700700204005320000084060105040008000500070803010450000091600508007003010600 +000900800128006400070800060800430007500000009600079008090004010003600284001007000 +000080000270000054095000810009806400020403060006905100017000620460000038000090000 +000602000400050001085010620038206710000000000019407350026040530900020007000809000 +000900002050123400030000160908000000070000090000000205091000050007439020400007000 +380000000000400785009020300060090000800302009000040070001070500495006000000000092 +000158000002060800030000040027030510000000000046080790050000080004070100000325000 +010500200900001000002008030500030007008000500600080004040100700000700006003004050 +080000040000469000400000007005904600070608030008502100900000005000781000060000010 +904200007010000000000706500000800090020904060040002000001607000000000030300005702 +000700800006000031040002000024070000010030080000060290000800070860000500002006000 +001007090590080001030000080000005800050060020004100000080000030100020079020700400 +000003017015009008060000000100007000009000200000500004000000020500600340340200000 +300200000000107000706030500070009080900020004010800050009040301000702000000008006 From fdee9ed716f8bfbbfcf833fa41e7849be7ce6d83 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 22:32:15 -0700 Subject: [PATCH 18/75] Couple tweaks to help with parsing Euler and Norvig puzzles - Make xy_key and xy_kwargs_key classmethods --- sudoku.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/sudoku.py b/sudoku.py index e47228d..95dee07 100644 --- a/sudoku.py +++ b/sudoku.py @@ -29,7 +29,7 @@ class Board(dict): all possible values for the square. If an initial value was given, make sure it is one of the valid initial values. Raise a ValueError if not. ''' - initial_value = kwargs.get('x{}y{}'.format(x, y)) + initial_value = kwargs.get(Board.xy_kwargs_key(x, y)) if initial_value is None: return list(self.possible_values) if initial_value not in self.possible_values: @@ -37,14 +37,23 @@ class Board(dict): return [initial_value] # Make the grid. - super(Board, self).__init__([(self._xy_key(x, y), kwget(x, y)) + super(Board, self).__init__([(Board.xy_key(x, y), kwget(x, y)) for x in range(self.size) for y in range(self.size)]) - def _xy_key(self, x, y): + @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): ''' @@ -205,7 +214,7 @@ class Board(dict): row_squares = [] box_squares = [] for x in range(self.size): - square = self.get(self._xy_key(x, y)) + square = self.get(Board.xy_key(x, y)) if len(square) == 1: box_squares.append(str(square[0]).center(square_size)) else: @@ -223,5 +232,3 @@ class Board(dict): 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) - - From 81e3296f6ff4302f3e2ea1c6aade7c5812d1630e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 29 Aug 2013 22:46:38 -0700 Subject: [PATCH 19/75] Create keys then assign values in __init__ --- sudoku.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/sudoku.py b/sudoku.py index 95dee07..ea78cc2 100644 --- a/sudoku.py +++ b/sudoku.py @@ -23,23 +23,18 @@ class Board(dict): # The range of possible values for a square. self.possible_values = range(1, self.size + 1) - def kwget(x, y): - ''' - Try to get an initial value for square ({x}, {y}) from the given kwargs. If none exists, return a list of - all possible values for the square. If an initial value was given, make sure it is one of the valid initial - values. Raise a ValueError if not. - ''' - initial_value = kwargs.get(Board.xy_kwargs_key(x, y)) - if initial_value is None: - return list(self.possible_values) - if initial_value not in self.possible_values: - raise ValueError('Invalid initial value for square ({}, {}): {}'.format(x, y, initial_value)) - return [initial_value] - # Make the grid. - super(Board, self).__init__([(Board.xy_key(x, y), kwget(x, y)) + 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[square] = [initial_value] @classmethod def xy_key(self, x, y): @@ -121,7 +116,7 @@ class Board(dict): # 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[self._xy_key(x, y)] + del peers[Board.xy_key(x, y)] return peers def clear(self): From cc5e35ff8194ee29390b86a14255b95a11348f5b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 30 Aug 2013 10:11:44 -0700 Subject: [PATCH 20/75] Working on search() --- sudoku.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sudoku.py b/sudoku.py index ea78cc2..ea1bbd8 100644 --- a/sudoku.py +++ b/sudoku.py @@ -135,6 +135,14 @@ class Board(dict): raise return self.solved + def search(self): + if self.solved: + return True + + # 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. + 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) From 6cc3fed4b3e4c6d60ed3153d42ae1ceb6612f931 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 30 Aug 2013 17:09:41 -0700 Subject: [PATCH 21/75] Implement search - Remove __setitem__; it was getting in the way... - deepcopy the board for each trial in search() --- sudoku.py | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/sudoku.py b/sudoku.py index ea1bbd8..efdabc3 100644 --- a/sudoku.py +++ b/sudoku.py @@ -5,6 +5,7 @@ A Sudoku solver. ''' +import copy import logging import math @@ -34,7 +35,7 @@ class Board(dict): 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[square] = [initial_value] + self.assign(square, [initial_value]) @classmethod def xy_key(self, x, y): @@ -119,29 +120,36 @@ class Board(dict): del peers[Board.xy_key(x, y)] return peers - def clear(self): + 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: - self[square] = list(self.possible_values) + for square in (self if squares is None else squares): + self.assign(square, list(self.possible_values)) def solve(self): - for square, values in self.items(): - if len(values) == 1: - try: - self.assign(square, values) - except ValueError: - raise - return self.solved + 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 True + 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. + for v in self[smallest]: + trial_board = copy.deepcopy(self) + try: + trial_board.assign(smallest, [v]) + if trial_board.search(): + return trial_board + except ValueError: + continue + raise ValueError('No possible solution found.') + def assign(self, square, value): ''' @@ -196,19 +204,13 @@ class Board(dict): raise ValueError('No place for value {} to go in unit {}; board is now invalid'.format(value, unit)) elif len(places) == 1: LOG.debug('One place for value {} to be in unit {}; setting'.format(value, unit)) - self[places[0]] = [value] + self.assign(places[0], [value]) return True def __delitem__(self, key): # Don't allow deleting keys from self. pass - def __setitem__(self, key, value): - if key not in self: - # Don't allow adding new keys, only changes to existing ones. - raise KeyError('Key {} is not a valid coordinate pair.'.format(key)) - self.assign(key, value) - def __str__(self): lines = [] box_lines = [] From e004d319e31c6f9b642bac5ded4047869e9b19bb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Fri, 6 Oct 2017 20:27:01 -0700 Subject: [PATCH 22/75] IDK what this WIP is --- sudoku.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/sudoku.py b/sudoku.py index efdabc3..964e6be 100644 --- a/sudoku.py +++ b/sudoku.py @@ -140,15 +140,18 @@ class Board(dict): # 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) - try: - trial_board.assign(smallest, [v]) - if trial_board.search(): - return trial_board - except ValueError: - continue - raise ValueError('No possible solution found.') + 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): @@ -159,12 +162,8 @@ class Board(dict): LOG.debug('Assigning values {} to square {}.'.format(value, square)) removed_values = set(self[square]) - set(value) for v in removed_values: - try: - self.eliminate(square, v) - except ValueError: - raise - # TODO: To make this work right, I should probably also do the same as above for the values added *back* into - # the values for {square}. + if not self.eliminate(square, v): + return False return True def eliminate(self, square, value): @@ -186,25 +185,24 @@ class Board(dict): if len(self[square]) == 0: # Whoops. Removed the last value... We have a contradiction now. LOG.error('Removed last value from square {}'.format(square)) - raise ValueError('Removed last value from square {}; board is now invalid'.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])) - try: - for peer in self.peers(*square): - self.eliminate(peer, self[square][0]) - except ValueError: - raise + 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)) - raise ValueError('No place for value {} to go in unit {}; board is now invalid'.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): From 53629f4d566101c6c1b890d1e604648a12a579ba Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Oct 2017 09:20:49 -0700 Subject: [PATCH 23/75] Empty Sudoku board, with a pretty printing __str__() --- sudoku.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 sudoku.py diff --git a/sudoku.py b/sudoku.py new file mode 100644 index 0000000..4354bf2 --- /dev/null +++ b/sudoku.py @@ -0,0 +1,37 @@ +#!env python3 +# Eryn Wells +''' +A Sudoku puzzle solver. +''' + +import math + +class Sudoku: + def __init__(self, size=9): + dim = math.sqrt(size) + if dim != int(dim): + raise ValueError('Board size must have an integral square root.') + self.dimension = int(dim) + self.size = size + self.board = bytearray(b'\x00' * (size * size)) + + def __str__(self): + field_width = len(str(self.size)) + dim = int(math.sqrt(self.size)) + lines = [] + spacer = '+' + '+'.join(['-' * (field_width * dim) for _ in range(dim)]) + '+' + for line in range(self.size): + chunks = [] + for i in range(dim): + fields = [] + for j in range(dim): + idx = line * self.size + i * dim + j + fields.append('{{board[{i}]:^{width}}}'.format(i=idx, width=field_width)) + chunks.append(''.join(fields)) + if (line % dim) == 0: + lines.append(spacer) + lines.append('|' + '|'.join(chunks) + '|') + lines.append(spacer) + fmt = '\n'.join(lines) + out = fmt.format(board=[str(n) if n != 0 else ' ' for n in self.board]) + return out From 2feef04ad9e03c39e1addd02ba42dfa5b7f2041c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Oct 2017 10:39:19 -0700 Subject: [PATCH 24/75] Working on the solved property --- sudoku.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index 4354bf2..d07c787 100644 --- a/sudoku.py +++ b/sudoku.py @@ -15,6 +15,21 @@ class Sudoku: self.size = size self.board = bytearray(b'\x00' * (size * size)) + @property + def solved(self): + def _check_group(group): + values = sorted([self.board[i] for i in group]) + is_complete = values == list(range(1, self.size+1)) + return is_complete + + sz = self.size + sz2 = sz ** 2 + dim = int(math.sqrt(self.size)) + + rows = [range(i * sz, i * sz + sz) for i in range(sz)] + cols = [range(i, sz2, sz) for i in range(sz)] + # TODO: WIP + def __str__(self): field_width = len(str(self.size)) dim = int(math.sqrt(self.size)) @@ -33,5 +48,6 @@ class Sudoku: lines.append('|' + '|'.join(chunks) + '|') lines.append(spacer) fmt = '\n'.join(lines) - out = fmt.format(board=[str(n) if n != 0 else ' ' for n in self.board]) + str_board = [str(n) if n != 0 else ' ' for n in self.board] + out = fmt.format(board=str_board) return out From f98a0f4f75acba5fb05823efca5e253e55aa0838 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sat, 7 Oct 2017 12:24:41 -0700 Subject: [PATCH 25/75] Quick implement rows, columns, and square --- sudoku.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/sudoku.py b/sudoku.py index d07c787..fc8411a 100644 --- a/sudoku.py +++ b/sudoku.py @@ -4,6 +4,7 @@ A Sudoku puzzle solver. ''' +import itertools import math class Sudoku: @@ -15,21 +16,41 @@ class Sudoku: self.size = size self.board = bytearray(b'\x00' * (size * size)) + @property + def rows(self): + sz = self.size + return [range(i * sz, i * sz + sz) for i in range(sz)] + + @property + def columns(self): + sz = self.size + sz2 = sz ** 2 + return [range(i, sz2, sz) for i in range(sz)] + @property def solved(self): def _check_group(group): values = sorted([self.board[i] for i in group]) is_complete = values == list(range(1, self.size+1)) return is_complete - + sz = self.size sz2 = sz ** 2 dim = int(math.sqrt(self.size)) - - rows = [range(i * sz, i * sz + sz) for i in range(sz)] - cols = [range(i, sz2, sz) for i in range(sz)] # TODO: WIP + def square(self, x, y): + dim = int(math.sqrt(self.size)) + if (x < 0 or x >= dim) or (y < 0 or y >= dim): + raise IndexError('Invalid coordinates for square: ({}, {})'.format(x, y)) + offset_x = x * dim + offset_y = y * dim + def _range(i): + start = offset_x + i * self.size + return range(start, start + dim) + ranges = itertools.chain(*[_range(i) for i in range(dim)]) + return ranges + def __str__(self): field_width = len(str(self.size)) dim = int(math.sqrt(self.size)) From 01428e39728c65c3868c03a53daa82581bdd86b4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 07:23:03 -0700 Subject: [PATCH 26/75] Fix squares; add dimension property; add some tests --- sudoku.py | 21 ++++++++++++++++----- test.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 test.py diff --git a/sudoku.py b/sudoku.py index fc8411a..8f05749 100644 --- a/sudoku.py +++ b/sudoku.py @@ -12,10 +12,14 @@ class Sudoku: dim = math.sqrt(size) if dim != int(dim): raise ValueError('Board size must have an integral square root.') - self.dimension = int(dim) + self._dimension = int(dim) self.size = size self.board = bytearray(b'\x00' * (size * size)) + @property + def dimension(self): + return self._dimension + @property def rows(self): sz = self.size @@ -27,6 +31,11 @@ class Sudoku: sz2 = sz ** 2 return [range(i, sz2, sz) for i in range(sz)] + @property + def squares(self): + dim = self.dimension + return [self.square(x, y) for y in range(dim) for x in range(dim)] + @property def solved(self): def _check_group(group): @@ -40,14 +49,16 @@ class Sudoku: # TODO: WIP def square(self, x, y): - dim = int(math.sqrt(self.size)) + dim = self.dimension if (x < 0 or x >= dim) or (y < 0 or y >= dim): raise IndexError('Invalid coordinates for square: ({}, {})'.format(x, y)) - offset_x = x * dim - offset_y = y * dim + + offset = (x * dim, y * dim * self.size) + def _range(i): - start = offset_x + i * self.size + start = (offset[1] + i * self.size) + offset[0] return range(start, start + dim) + ranges = itertools.chain(*[_range(i) for i in range(dim)]) return ranges diff --git a/test.py b/test.py new file mode 100644 index 0000000..38805d6 --- /dev/null +++ b/test.py @@ -0,0 +1,53 @@ +#!env python3 +# Eryn Wells +''' +Unit tests for the Sudoku module. +''' + +import unittest +import sudoku + +class Sudoku4Tests(unittest.TestCase): + def setUp(self): + self.board = sudoku.Sudoku(size=4) + + def test_that_board_is_sane(self): + self.assertEqual(self.board.size, 4) + self.assertEqual(len(self.board.board), 4**2) + self.assertEqual(self.board.dimension, 2) + + 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.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.columns, expected_columns): + col_list = list(col) + with self.subTest(col=col_list, ex=excol): + self.assertEqual(col_list, excol) + + def test_squares(self): + expected_squares = [ + [ 0, 1, 4, 5], + [ 2, 3, 6, 7], + [ 8, 9, 12, 13], + [10, 11, 14, 15] + ] + for (sq, exsq) in zip(self.board.squares, expected_squares): + sq_list = list(sq) + with self.subTest(sq=sq_list, ex=exsq): + self.assertEqual(sq_list, exsq) From 5ebe1f58ffbecf3f8fecdbf96c518c44c244cb26 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 07:56:23 -0700 Subject: [PATCH 27/75] Add index_* properties for returning ranges of indexes --- sudoku.py | 46 ++++++++++++++++++++++++++++++++-------------- test.py | 9 +++++---- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/sudoku.py b/sudoku.py index 8f05749..f664846 100644 --- a/sudoku.py +++ b/sudoku.py @@ -22,31 +22,49 @@ class Sudoku: @property def rows(self): - sz = self.size - return [range(i * sz, i * sz + sz) for i in range(sz)] + return self._apply_index_range_list(self.index_rows) @property def columns(self): - sz = self.size - sz2 = sz ** 2 - return [range(i, sz2, sz) for i in range(sz)] + return self._apply_index_range_list(self.index_columns) @property def squares(self): - dim = self.dimension - return [self.square(x, y) for y in range(dim) for x in range(dim)] + return self._apply_index_range_list(self.index_squares) + + def _apply_index_range_list(self, ranges): + return (self._apply_index_range(r) for r in ranges) + + def _apply_index_range(self, rng): + return (self.board[i] for i in rng) @property - def solved(self): - def _check_group(group): - values = sorted([self.board[i] for i in group]) - is_complete = values == list(range(1, self.size+1)) - return is_complete + def index_rows(self): + ''' + Return a list of ranges of indexes into the board, each + defining a row. + ''' + sz = self.size + return (range(i * sz, i * sz + sz) for i in range(sz)) + @property + def index_columns(self): + ''' + Return a list of ranges of indexes into the board, each + defining a column. + ''' sz = self.size sz2 = sz ** 2 - dim = int(math.sqrt(self.size)) - # TODO: WIP + return (range(i, sz2, sz) for i in range(sz)) + + @property + def index_squares(self): + ''' + Return a list of ranges of indexes into the board, each + defining a square. + ''' + dim = self.dimension + return (self.square(x, y) for y in range(dim) for x in range(dim)) def square(self, x, y): dim = self.dimension diff --git a/test.py b/test.py index 38805d6..ab827b4 100644 --- a/test.py +++ b/test.py @@ -7,10 +7,11 @@ Unit tests for the Sudoku module. import unittest import sudoku -class Sudoku4Tests(unittest.TestCase): +class Sudoku4TestCase(unittest.TestCase): def setUp(self): self.board = sudoku.Sudoku(size=4) +class Sudoku4BasicTests(Sudoku4TestCase): def test_that_board_is_sane(self): self.assertEqual(self.board.size, 4) self.assertEqual(len(self.board.board), 4**2) @@ -23,7 +24,7 @@ class Sudoku4Tests(unittest.TestCase): [ 8, 9, 10, 11], [12, 13, 14, 15] ] - for (row, exrow) in zip(self.board.rows, expected_rows): + 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) @@ -35,7 +36,7 @@ class Sudoku4Tests(unittest.TestCase): [2, 6, 10, 14], [3, 7, 11, 15] ] - for (col, excol) in zip(self.board.columns, expected_columns): + 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) @@ -47,7 +48,7 @@ class Sudoku4Tests(unittest.TestCase): [ 8, 9, 12, 13], [10, 11, 14, 15] ] - for (sq, exsq) in zip(self.board.squares, expected_squares): + for (sq, exsq) in zip(self.board.index_squares, expected_squares): sq_list = list(sq) with self.subTest(sq=sq_list, ex=exsq): self.assertEqual(sq_list, exsq) From 3fabd4b42014c1cae66eced4b9534b084c08a098 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 08:00:45 -0700 Subject: [PATCH 28/75] Rough, probably incomplete solved property --- sudoku.py | 9 +++++++++ test.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/sudoku.py b/sudoku.py index f664846..c97e071 100644 --- a/sudoku.py +++ b/sudoku.py @@ -66,6 +66,15 @@ class Sudoku: dim = self.dimension return (self.square(x, y) for y in range(dim) for x in range(dim)) + @property + def solved(self): + expected = set(range(self.size)) + return all([ + all(expected == set(row) for row in self.rows), + all(expected == set(col) for col in self.columns), + all(expected == set(sqr) for sqr in self.squares) + ]) + def square(self, x, y): dim = self.dimension if (x < 0 or x >= dim) or (y < 0 or y >= dim): diff --git a/test.py b/test.py index ab827b4..cfe6354 100644 --- a/test.py +++ b/test.py @@ -52,3 +52,7 @@ class Sudoku4BasicTests(Sudoku4TestCase): sq_list = list(sq) with self.subTest(sq=sq_list, ex=exsq): self.assertEqual(sq_list, exsq) + +class Sudoku4SolvedTests(Sudoku4TestCase): + def test_that_an_empty_board_is_not_solved(self): + self.assertFalse(self.board.solved) From b5ac48bc1095bcc2066a12e45c567f1a1154afcc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 08:04:29 -0700 Subject: [PATCH 29/75] Little bit o' cleanup --- sudoku.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/sudoku.py b/sudoku.py index c97e071..f6239ab 100644 --- a/sudoku.py +++ b/sudoku.py @@ -22,27 +22,20 @@ class Sudoku: @property def rows(self): - return self._apply_index_range_list(self.index_rows) + return self._apply_index_ranges(self.index_rows) @property def columns(self): - return self._apply_index_range_list(self.index_columns) + return self._apply_index_ranges(self.index_columns) @property def squares(self): - return self._apply_index_range_list(self.index_squares) - - def _apply_index_range_list(self, ranges): - return (self._apply_index_range(r) for r in ranges) - - def _apply_index_range(self, rng): - return (self.board[i] for i in rng) + return self._apply_index_ranges(self.index_squares) @property def index_rows(self): ''' - Return a list of ranges of indexes into the board, each - defining a row. + Return an iterable of ranges of indexes into the board, each defining a row. ''' sz = self.size return (range(i * sz, i * sz + sz) for i in range(sz)) @@ -50,8 +43,7 @@ class Sudoku: @property def index_columns(self): ''' - Return a list of ranges of indexes into the board, each - defining a column. + Return an iterable of ranges of indexes into the board, each defining a column. ''' sz = self.size sz2 = sz ** 2 @@ -60,8 +52,7 @@ class Sudoku: @property def index_squares(self): ''' - Return a list of ranges of indexes into the board, each - defining a square. + Return an iterable of ranges of indexes into the board, each defining a square. ''' dim = self.dimension return (self.square(x, y) for y in range(dim) for x in range(dim)) @@ -89,6 +80,9 @@ class Sudoku: ranges = itertools.chain(*[_range(i) for i in range(dim)]) return ranges + def _apply_index_ranges(self, ranges): + return ((self.board[i] for i in rng) for r in ranges) + def __str__(self): field_width = len(str(self.size)) dim = int(math.sqrt(self.size)) From 5696c087d8ec2ac543be64124679ce3d19dda05f Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 08:06:34 -0700 Subject: [PATCH 30/75] Add a TODO --- sudoku.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sudoku.py b/sudoku.py index f6239ab..985b295 100644 --- a/sudoku.py +++ b/sudoku.py @@ -57,6 +57,8 @@ class Sudoku: dim = self.dimension return (self.square(x, y) for y in range(dim) for x in range(dim)) + # TODO: Break the above into helper methods that produce a single thing given an index. + @property def solved(self): expected = set(range(self.size)) From eae05487b9fbd9ac39f164e3b2c5e7613261a481 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 08:26:18 -0700 Subject: [PATCH 31/75] Add puzzles and puzzles.py from the old branch --- puzzles.py | 34 +++++++++++++++++ puzzles/euler.txt | 50 ++++++++++++++++++++++++ puzzles/norvig.txt | 95 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 puzzles.py create mode 100644 puzzles/euler.txt create mode 100644 puzzles/norvig.txt diff --git a/puzzles.py b/puzzles.py new file mode 100644 index 0000000..4a9b788 --- /dev/null +++ b/puzzles.py @@ -0,0 +1,34 @@ +#!env python3 +# Eryn Wells +''' +Parser for puzzles in the ./puzzles directory. +''' + +import sudoku + +euler = [] +norvig = [] + +def parse_puzzle_files(): + global euler, norvig + + print('Parsing Euler puzzles') + euler = list(_get_puzzles('puzzles/euler.txt')) + + print('Parsing Norvig puzzles') + norvig = list(_get_puzzles('puzzles/norvig.txt')) + +def _get_puzzles(filename): + with open(filename, 'r') as f: + puzzles = f.readlines() + return (_parse_puzzle(p) for p in puzzles if p) + +def _parse_puzzle(puzzle): + puzzle = puzzle.strip() + if len(puzzle) == 81: + print("Parsing '{}'".format(puzzle)) + board = (int('0' if x == '.' else x) for x in puzzle) + return sudoku.Sudoku(initial=board) + else: + print("Skipping '{}'".format(puzzle)) + return None diff --git a/puzzles/euler.txt b/puzzles/euler.txt new file mode 100644 index 0000000..b0bb576 --- /dev/null +++ b/puzzles/euler.txt @@ -0,0 +1,50 @@ +003020600900305001001806400008102900700000008006708200002609500800203009005010300 +200080300060070084030500209000105408000000000402706000301007040720040060004010003 +000000907000420180000705026100904000050000040000507009920108000034059000507000000 +030050040008010500460000012070502080000603000040109030250000098001020600080060020 +020810740700003100090002805009040087400208003160030200302700060005600008076051090 +100920000524010000000000070050008102000000000402700090060000000000030945000071006 +043080250600000000000001094900004070000608000010200003820500000000000005034090710 +480006902002008001900370060840010200003704100001060049020085007700900600609200018 +000900002050123400030000160908000000070000090000000205091000050007439020400007000 +001900003900700160030005007050000009004302600200000070600100030042007006500006800 +000125400008400000420800000030000095060902010510000060000003049000007200001298000 +062340750100005600570000040000094800400000006005830000030000091006400007059083260 +300000000005009000200504000020000700160000058704310600000890100000067080000005437 +630000000000500008005674000000020000003401020000000345000007004080300902947100080 +000020040008035000000070602031046970200000000000501203049000730000000010800004000 +361025900080960010400000057008000471000603000259000800740000005020018060005470329 +050807020600010090702540006070020301504000908103080070900076205060090003080103040 +080005000000003457000070809060400903007010500408007020901020000842300000000100080 +003502900000040000106000305900251008070408030800763001308000104000020000005104800 +000000000009805100051907420290401065000000000140508093026709580005103600000000000 +020030090000907000900208005004806500607000208003102900800605007000309000030020050 +005000006070009020000500107804150000000803000000092805907006000030400010200000600 +040000050001943600009000300600050002103000506800020007005000200002436700030000040 +004000000000030002390700080400009001209801307600200008010008053900040000000000800 +360020089000361000000000000803000602400603007607000108000000000000418000970030014 +500400060009000800640020000000001008208000501700500000000090084003000600060003002 +007256400400000005010030060000508000008060200000107000030070090200000004006312700 +000000000079050180800000007007306800450708096003502700700000005016030420000000000 +030000080009000500007509200700105008020090030900402001004207100002000800070000090 +200170603050000100000006079000040700000801000009050000310400000005000060906037002 +000000080800701040040020030374000900000030000005000321010060050050802006080000000 +000000085000210009960080100500800016000000000890006007009070052300054000480000000 +608070502050608070002000300500090006040302050800050003005000200010704090409060701 +050010040107000602000905000208030501040070020901080406000401000304000709020060010 +053000790009753400100000002090080010000907000080030070500000003007641200061000940 +006080300049070250000405000600317004007000800100826009000702000075040190003090600 +005080700700204005320000084060105040008000500070803010450000091600508007003010600 +000900800128006400070800060800430007500000009600079008090004010003600284001007000 +000080000270000054095000810009806400020403060006905100017000620460000038000090000 +000602000400050001085010620038206710000000000019407350026040530900020007000809000 +000900002050123400030000160908000000070000090000000205091000050007439020400007000 +380000000000400785009020300060090000800302009000040070001070500495006000000000092 +000158000002060800030000040027030510000000000046080790050000080004070100000325000 +010500200900001000002008030500030007008000500600080004040100700000700006003004050 +080000040000469000400000007005904600070608030008502100900000005000781000060000010 +904200007010000000000706500000800090020904060040002000001607000000000030300005702 +000700800006000031040002000024070000010030080000060290000800070860000500002006000 +001007090590080001030000080000005800050060020004100000080000030100020079020700400 +000003017015009008060000000100007000009000200000500004000000020500600340340200000 +300200000000107000706030500070009080900020004010800050009040301000702000000008006 diff --git a/puzzles/norvig.txt b/puzzles/norvig.txt new file mode 100644 index 0000000..a68d542 --- /dev/null +++ b/puzzles/norvig.txt @@ -0,0 +1,95 @@ +4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4...... +52...6.........7.13...........4..8..6......5...........418.........3..2...87..... +6.....8.3.4.7.................5.4.7.3..2.....1.6.......2.....5.....8.6......1.... +48.3............71.2.......7.5....6....2..8.............1.76...3.....4......5.... +....14....3....2...7..........9...3.6.1.............8.2.....1.4....5.6.....7.8... +......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3. +6.2.5.........3.4..........43...8....1....2........7..5..27...........81...6..... +.524.........7.1..............8.2...3.....6...9.5.....1.6.3...........897........ +6.2.5.........4.3..........43...8....1....2........7..5..27...........81...6..... +.923.........8.1...........1.7.4...........658.........6.5.2...4.....7.....9..... +6..3.2....5.....1..........7.26............543.........8.15........4.2........7.. +.6.5.1.9.1...9..539....7....4.8...7.......5.8.817.5.3.....5.2............76..8... +..5...987.4..5...1..7......2...48....9.1.....6..2.....3..6..2.......9.7.......5.. +3.6.7...........518.........1.4.5...7.....6.....2......2.....4.....8.3.....5..... +1.....3.8.7.4..............2.3.1...........958.........5.6...7.....8.2...4....... +6..3.2....4.....1..........7.26............543.........8.15........4.2........7.. +....3..9....2....1.5.9..............1.2.8.4.6.8.5...2..75......4.1..6..3.....4.6. +45.....3....8.1....9...........5..9.2..7.....8.........1..4..........7.2...6..8.. +.237....68...6.59.9.....7......4.97.3.7.96..2.........5..47.........2....8....... +..84...3....3.....9....157479...8........7..514.....2...9.6...2.5....4......9..56 +.98.1....2......6.............3.2.5..84.........6.........4.8.93..5...........1.. +..247..58..............1.4.....2...9528.9.4....9...1.........3.3....75..685..2... +4.....8.5.3..........7......2.....6.....5.4......1.......6.3.7.5..2.....1.9...... +.2.3......63.....58.......15....9.3....7........1....8.879..26......6.7...6..7..4 +1.....7.9.4...72..8.........7..1..6.3.......5.6..4..2.........8..53...7.7.2....46 +4.....3.....8.2......7........1...8734.......6........5...6........1.4...82...... +.......71.2.8........4.3...7...6..5....2..3..9........6...7.....8....4......5.... +6..3.2....4.....8..........7.26............543.........8.15........8.2........7.. +.47.8...1............6..7..6....357......5....1..6....28..4.....9.1...4.....2.69. +......8.17..2........5.6......7...5..1....3...8.......5......2..4..8....6...3.... +38.6.......9.......2..3.51......5....3..1..6....4......17.5..8.......9.......7.32 +...5...........5.697.....2...48.2...25.1...3..8..3.........4.7..13.5..9..2...31.. +.2.......3.5.62..9.68...3...5..........64.8.2..47..9....3.....1.....6...17.43.... +.8..4....3......1........2...5...4.69..1..8..2...........3.9....6....5.....2..... +..8.9.1...6.5...2......6....3.1.7.5.........9..4...3...5....2...7...3.8.2..7....4 +4.....5.8.3..........7......2.....6.....5.8......1.......6.3.7.5..2.....1.8...... +1.....3.8.6.4..............2.3.1...........958.........5.6...7.....8.2...4....... +1....6.8..64..........4...7....9.6...7.4..5..5...7.1...5....32.3....8...4........ +249.6...3.3....2..8.......5.....6......2......1..4.82..9.5..7....4.....1.7...3... +...8....9.873...4.6..7.......85..97...........43..75.......3....3...145.4....2..1 +...5.1....9....8...6.......4.1..........7..9........3.8.....1.5...2..4.....36.... +......8.16..2........7.5......6...2..1....3...8.......2......7..3..8....5...4.... +.476...5.8.3.....2.....9......8.5..6...1.....6.24......78...51...6....4..9...4..7 +.....7.95.....1...86..2.....2..73..85......6...3..49..3.5...41724................ +.4.5.....8...9..3..76.2.....146..........9..7.....36....1..4.5..6......3..71..2.. +.834.........7..5...........4.1.8..........27...3.....2.6.5....5.....8........1.. +..9.....3.....9...7.....5.6..65..4.....3......28......3..75.6..6...........12.3.8 +.26.39......6....19.....7.......4..9.5....2....85.....3..2..9..4....762.........4 +2.3.8....8..7...........1...6.5.7...4......3....1............82.5....6...1....... +6..3.2....1.....5..........7.26............843.........8.15........8.2........7.. +1.....9...64..1.7..7..4.......3.....3.89..5....7....2.....6.7.9.....4.1....129.3. +.........9......84.623...5....6...453...1...6...9...7....1.....4.5..2....3.8....9 +.2....5938..5..46.94..6...8..2.3.....6..8.73.7..2.........4.38..7....6..........5 +9.4..5...25.6..1..31......8.7...9...4..26......147....7.......2...3..8.6.4.....9. +...52.....9...3..4......7...1.....4..8..453..6...1...87.2........8....32.4..8..1. +53..2.9...24.3..5...9..........1.827...7.........981.............64....91.2.5.43. +1....786...7..8.1.8..2....9........24...1......9..5...6.8..........5.9.......93.4 +....5...11......7..6.....8......4.....9.1.3.....596.2..8..62..7..7......3.5.7.2.. +.47.2....8....1....3....9.2.....5...6..81..5.....4.....7....3.4...9...1.4..27.8.. +......94.....9...53....5.7..8.4..1..463...........7.8.8..7.....7......28.5.26.... +.2......6....41.....78....1......7....37.....6..412....1..74..5..8.5..7......39.. +1.....3.8.6.4..............2.3.1...........758.........7.5...6.....8.2...4....... +2....1.9..1..3.7..9..8...2.......85..6.4.........7...3.2.3...6....5.....1.9...2.5 +..7..8.....6.2.3...3......9.1..5..6.....1.....7.9....2........4.83..4...26....51. +...36....85.......9.4..8........68.........17..9..45...1.5...6.4....9..2.....3... +34.6.......7.......2..8.57......5....7..1..2....4......36.2..1.......9.......7.82 +......4.18..2........6.7......8...6..4....3...1.......6......2..5..1....7...3.... +.4..5..67...1...4....2.....1..8..3........2...6...........4..5.3.....8..2........ +.......4...2..4..1.7..5..9...3..7....4..6....6..1..8...2....1..85.9...6.....8...3 +8..7....4.5....6............3.97...8....43..5....2.9....6......2...6...7.71..83.2 +.8...4.5....7..3............1..85...6.....2......4....3.26............417........ +....7..8...6...5...2...3.61.1...7..2..8..534.2..9.......2......58...6.3.4...1.... +......8.16..2........7.5......6...2..1....3...8.......2......7..4..8....5...3.... +.2..........6....3.74.8.........3..2.8..4..1.6..5.........1.78.5....9..........4. +.52..68.......7.2.......6....48..9..2..41......1.....8..61..38.....9...63..6..1.9 +....1.78.5....9..........4..2..........6....3.74.8.........3..2.8..4..1.6..5..... +1.......3.6.3..7...7...5..121.7...9...7........8.1..2....8.64....9.2..6....4..... +4...7.1....19.46.5.....1......7....2..2.3....847..6....14...8.6.2....3..6...9.... +......8.17..2........5.6......7...5..1....3...8.......5......2..3..8....6...4.... +963......1....8......2.5....4.8......1....7......3..257......3...9.2.4.7......9.. +15.3......7..4.2....4.72.....8.........9..1.8.1..8.79......38...........6....7423 +..........5724...98....947...9..3...5..9..12...3.1.9...6....25....56.....7......6 +....75....1..2.....4...3...5.....3.2...8...1.......6.....1..48.2........7........ +6.....7.3.4.8.................5.4.8.7..2.....1.3.......2.....5.....7.9......1.... +....6...4..6.3....1..4..5.77.....8.5...8.....6.8....9...2.9....4....32....97..1.. +.32.....58..3.....9.428...1...4...39...6...5.....1.....2...67.8.....4....95....6. +...5.3.......6.7..5.8....1636..2.......4.1.......3...567....2.8..4.7.......2..5.. +.5.3.7.4.1.........3.......5.8.3.61....8..5.9.6..1........4...6...6927....2...9.. +..5..8..18......9.......78....4.....64....9......53..2.6.........138..5....9.714. +..........72.6.1....51...82.8...13..4.........37.9..1.....238..5.4..9.........79. +...658.....4......12............96.7...3..5....2.8...3..19..8..3.6.....4....473.. +.2.3.......6..8.9.83.5........2...8.7.9..5........6..4.......1...1...4.22..7..8.9 +.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.. From 5a32743d7bc7a3526119a506fe4994328b7dc344 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 8 Oct 2017 08:26:34 -0700 Subject: [PATCH 32/75] Add initial arg to Sudoku.__init__() --- sudoku.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sudoku.py b/sudoku.py index 985b295..a10cfd2 100644 --- a/sudoku.py +++ b/sudoku.py @@ -8,13 +8,16 @@ import itertools import math class Sudoku: - def __init__(self, size=9): + def __init__(self, size=9, initial=None): dim = math.sqrt(size) if dim != int(dim): raise ValueError('Board size must have an integral square root.') self._dimension = int(dim) self.size = size - self.board = bytearray(b'\x00' * (size * size)) + if initial: + self.board = bytearray(initial)[:9**2] + else: + self.board = bytearray(b'\x00' * (size * size)) @property def dimension(self): From 88f0e07c06a16f87a52fd2608688722b87984499 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 9 Oct 2017 15:03:33 -0700 Subject: [PATCH 33/75] Copy pasta error --- sudoku.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index a10cfd2..84c967f 100644 --- a/sudoku.py +++ b/sudoku.py @@ -86,7 +86,7 @@ class Sudoku: return ranges def _apply_index_ranges(self, ranges): - return ((self.board[i] for i in rng) for r in ranges) + return ((self.board[i] for i in r) for r in ranges) def __str__(self): field_width = len(str(self.size)) From 3017b1e92386ccf370335c0fabbe76308025d28b Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 09:53:55 -0700 Subject: [PATCH 34/75] Rename initial init attribute to board; make self.board -> _board --- puzzles.py | 2 +- sudoku.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/puzzles.py b/puzzles.py index 4a9b788..a2e1fb3 100644 --- a/puzzles.py +++ b/puzzles.py @@ -28,7 +28,7 @@ def _parse_puzzle(puzzle): if len(puzzle) == 81: print("Parsing '{}'".format(puzzle)) board = (int('0' if x == '.' else x) for x in puzzle) - return sudoku.Sudoku(initial=board) + return sudoku.Sudoku(board=board) else: print("Skipping '{}'".format(puzzle)) return None diff --git a/sudoku.py b/sudoku.py index 84c967f..aa25fb6 100644 --- a/sudoku.py +++ b/sudoku.py @@ -8,16 +8,16 @@ import itertools import math class Sudoku: - def __init__(self, size=9, initial=None): + def __init__(self, size=9, board=None): dim = math.sqrt(size) if dim != int(dim): raise ValueError('Board size must have an integral square root.') self._dimension = int(dim) self.size = size - if initial: - self.board = bytearray(initial)[:9**2] + if board: + self._board = bytearray(board)[:size**2] else: - self.board = bytearray(b'\x00' * (size * size)) + self._board = bytearray(b'\x00' * (size * size)) @property def dimension(self): @@ -86,7 +86,7 @@ class Sudoku: return ranges def _apply_index_ranges(self, ranges): - return ((self.board[i] for i in r) for r in ranges) + return ((self._board[i] for i in r) for r in ranges) def __str__(self): field_width = len(str(self.size)) @@ -106,6 +106,6 @@ class Sudoku: lines.append('|' + '|'.join(chunks) + '|') lines.append(spacer) fmt = '\n'.join(lines) - str_board = [str(n) if n != 0 else ' ' for n in self.board] + str_board = [str(n) if n != 0 else ' ' for n in self._board] out = fmt.format(board=str_board) return out From 93a4fecb2667983d9f437c09119f63c8583f03fc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 09:55:09 -0700 Subject: [PATCH 35/75] Add helpers to get rows, cols, and squares for particular coordinates --- sudoku.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/sudoku.py b/sudoku.py index aa25fb6..7115db5 100644 --- a/sudoku.py +++ b/sudoku.py @@ -41,7 +41,7 @@ class Sudoku: Return an iterable of ranges of indexes into the board, each defining a row. ''' sz = self.size - return (range(i * sz, i * sz + sz) for i in range(sz)) + return (self._row(i, size) for i in range(sz)) @property def index_columns(self): @@ -50,7 +50,7 @@ class Sudoku: ''' sz = self.size sz2 = sz ** 2 - return (range(i, sz2, sz) for i in range(sz)) + return (self._column(c, sz, sz2) for i in range(sz)) @property def index_squares(self): @@ -58,21 +58,16 @@ class Sudoku: Return an iterable of ranges of indexes into the board, each defining a square. ''' dim = self.dimension - return (self.square(x, y) for y in range(dim) for x in range(dim)) + return (self._square(x, y, dim) for y in range(dim) for x in range(dim)) # TODO: Break the above into helper methods that produce a single thing given an index. + def _row(self, r, size): + return range(r * size, r * size + size) - @property - def solved(self): - expected = set(range(self.size)) - return all([ - all(expected == set(row) for row in self.rows), - all(expected == set(col) for col in self.columns), - all(expected == set(sqr) for sqr in self.squares) - ]) + def _column(self, c, size, size2): + return range(c, size2, size) - def square(self, x, y): - dim = self.dimension + def _square(self, x, y, dim): if (x < 0 or x >= dim) or (y < 0 or y >= dim): raise IndexError('Invalid coordinates for square: ({}, {})'.format(x, y)) @@ -85,6 +80,15 @@ class Sudoku: ranges = itertools.chain(*[_range(i) for i in range(dim)]) return ranges + @property + def solved(self): + expected = set(range(self.size)) + return all([ + all(expected == set(row) for row in self.rows), + all(expected == set(col) for col in self.columns), + all(expected == set(sqr) for sqr in self.squares) + ]) + def _apply_index_ranges(self, ranges): return ((self._board[i] for i in r) for r in ranges) From 19a9bc9d3720d5f2936de0eb2112bb588fdaf8cc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 09:55:34 -0700 Subject: [PATCH 36/75] Tweak the str printing a bit --- sudoku.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sudoku.py b/sudoku.py index 7115db5..0888b56 100644 --- a/sudoku.py +++ b/sudoku.py @@ -94,9 +94,9 @@ class Sudoku: def __str__(self): field_width = len(str(self.size)) - dim = int(math.sqrt(self.size)) + dim = self.dimension lines = [] - spacer = '+' + '+'.join(['-' * (field_width * dim) for _ in range(dim)]) + '+' + spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * dim) for _ in range(dim)])) for line in range(self.size): chunks = [] for i in range(dim): @@ -107,7 +107,7 @@ class Sudoku: chunks.append(''.join(fields)) if (line % dim) == 0: lines.append(spacer) - lines.append('|' + '|'.join(chunks) + '|') + lines.append('{0}{1}{0}'.format('|', '|'.join(chunks))) lines.append(spacer) fmt = '\n'.join(lines) str_board = [str(n) if n != 0 else ' ' for n in self._board] From 00225aca444cd86f4a5a4589418ce6295af776ea Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 09:55:51 -0700 Subject: [PATCH 37/75] Add peers and index_peers for a given (x,y) --- sudoku.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sudoku.py b/sudoku.py index 0888b56..2d3904c 100644 --- a/sudoku.py +++ b/sudoku.py @@ -60,6 +60,16 @@ class Sudoku: dim = self.dimension return (self._square(x, y, dim) for y in range(dim) for x in range(dim)) + def peers(self, x, y): + return {self._board[i] for i in self.index_peers(x, y) if self._board[i] != 0} + + def index_peers(self, x, y): + dim = self.dimension + sz = self.size + sz2 = sz ** 2 + sq_x, sq_y = int(x / dim), int(y / dim) + return set(self._row(y, sz)) | set(self._column(x, sz, sz2)) | set(self._square(sq_x, sq_y, dim)) + # TODO: Break the above into helper methods that produce a single thing given an index. def _row(self, r, size): return range(r * size, r * size + size) From cf43b02aa933455b94f6b98e59085c3c816cc919 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 11:43:02 -0700 Subject: [PATCH 38/75] Add test_peers(); fix up some errors found --- sudoku.py | 5 ++--- test.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/sudoku.py b/sudoku.py index 2d3904c..da25383 100644 --- a/sudoku.py +++ b/sudoku.py @@ -40,8 +40,7 @@ class Sudoku: ''' Return an iterable of ranges of indexes into the board, each defining a row. ''' - sz = self.size - return (self._row(i, size) for i in range(sz)) + return (self._row(i, self.size) for i in range(self.size)) @property def index_columns(self): @@ -50,7 +49,7 @@ class Sudoku: ''' sz = self.size sz2 = sz ** 2 - return (self._column(c, sz, sz2) for i in range(sz)) + return (self._column(i, sz, sz2) for i in range(sz)) @property def index_squares(self): diff --git a/test.py b/test.py index cfe6354..0ee539b 100644 --- a/test.py +++ b/test.py @@ -14,7 +14,7 @@ class Sudoku4TestCase(unittest.TestCase): class Sudoku4BasicTests(Sudoku4TestCase): def test_that_board_is_sane(self): self.assertEqual(self.board.size, 4) - self.assertEqual(len(self.board.board), 4**2) + self.assertEqual(len(self.board._board), 4**2) self.assertEqual(self.board.dimension, 2) def test_rows(self): @@ -53,6 +53,33 @@ class Sudoku4BasicTests(Sudoku4TestCase): with self.subTest(sq=sq_list, ex=exsq): self.assertEqual(sq_list, exsq) + def test_peers(self): + expected_peers = { + (0,0): set([0, 1, 2, 3, 4, 8, 12, 5]), + (1,0): set([0, 1, 2, 3, 5, 9, 13, 4]), + (2,0): set([0, 1, 2, 3, 6, 10, 14, 7]), + (3,0): set([0, 1, 2, 3, 7, 11, 15, 6]), + + (0,1): set([4, 5, 6, 7, 0, 8, 12, 1]), + (1,1): set([4, 5, 6, 7, 1, 9, 13, 0]), + (2,1): set([4, 5, 6, 7, 2, 10, 14, 3]), + (3,1): set([4, 5, 6, 7, 3, 11, 15, 2]), + + (0,2): set([8, 9, 10, 11, 0, 4, 12, 13]), + (1,2): set([8, 9, 10, 11, 1, 5, 13, 12]), + (2,2): set([8, 9, 10, 11, 2, 6, 14, 15]), + (3,2): set([8, 9, 10, 11, 3, 7, 15, 14]), + + (0,3): set([12, 13, 14, 15, 0, 4, 8, 9]), + (1,3): set([12, 13, 14, 15, 1, 5, 9, 8]), + (2,3): set([12, 13, 14, 15, 2, 6, 10, 11]), + (3,3): set([12, 13, 14, 15, 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) From a9a01d3fd4fa544ce87ef9e12f7e546afc349888 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 11:59:29 -0700 Subject: [PATCH 39/75] Fix solved property --- sudoku.py | 9 +++------ test.py | 5 +++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/sudoku.py b/sudoku.py index da25383..8fa2937 100644 --- a/sudoku.py +++ b/sudoku.py @@ -91,12 +91,9 @@ class Sudoku: @property def solved(self): - expected = set(range(self.size)) - return all([ - all(expected == set(row) for row in self.rows), - all(expected == set(col) for col in self.columns), - all(expected == set(sqr) for sqr in self.squares) - ]) + expected = set(range(1, self.size + 1)) + all_groups = itertools.chain(self.rows, self.columns, self.squares) + return all(expected == set(g) for g in all_groups) def _apply_index_ranges(self, ranges): return ((self._board[i] for i in r) for r in ranges) diff --git a/test.py b/test.py index 0ee539b..a9695dc 100644 --- a/test.py +++ b/test.py @@ -83,3 +83,8 @@ class Sudoku4BasicTests(Sudoku4TestCase): 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(4, board) + self.assertTrue(self.board.solved) From 35647fc8c92ad1dedbc54b755f7fc18a7469f51c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 12:43:18 -0700 Subject: [PATCH 40/75] 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 --- sudoku.py | 93 +++++++++++++++++++++++++++++++------------------------ test.py | 27 ++++++++-------- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/sudoku.py b/sudoku.py index 8fa2937..225f60b 100644 --- a/sudoku.py +++ b/sudoku.py @@ -8,20 +8,37 @@ import itertools import math class Sudoku: - def __init__(self, size=9, board=None): - dim = math.sqrt(size) - if dim != int(dim): - raise ValueError('Board size must have an integral square root.') - self._dimension = int(dim) - self.size = size + def __init__(self, size=3, board=None): + self._size = size + sz4 = size ** 4 if board: - self._board = bytearray(board)[:size**2] + self._board = bytes(board)[:sz4] else: - self._board = bytearray(b'\x00' * (size * size)) + self._board = bytes(sz4) @property - def dimension(self): - return self._dimension + def size(self): + return self._size + + @property + def row_size(self): + return self.size ** 2 + + @property + def grid_size(self): + return self.size ** 4 + + @property + def all_squares(self): + return itertools.product(range(self.size), repeat=2) + + @property + def possible_values(self): + ''' + The set of valid values for any grid square, not accounting for values made invalid by already being + present in a peer of that square. + ''' + return set(range(1, self.row_size + 1)) @property def rows(self): @@ -40,58 +57,52 @@ class Sudoku: ''' Return an iterable of ranges of indexes into the board, each defining a row. ''' - return (self._row(i, self.size) for i in range(self.size)) + 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. ''' - sz = self.size - sz2 = sz ** 2 - return (self._column(i, sz, sz2) for i in range(sz)) + return (self._column(i) for i in range(self.row_size)) @property def index_squares(self): ''' Return an iterable of ranges of indexes into the board, each defining a square. ''' - dim = self.dimension - return (self._square(x, y, dim) for y in range(dim) for x in range(dim)) + return (self._square(x, y) for (x,y) in self.all_squares) def peers(self, x, y): return {self._board[i] for i in self.index_peers(x, y) if self._board[i] != 0} def index_peers(self, x, y): - dim = self.dimension sz = self.size - sz2 = sz ** 2 - sq_x, sq_y = int(x / dim), int(y / dim) - return set(self._row(y, sz)) | set(self._column(x, sz, sz2)) | set(self._square(sq_x, sq_y, dim)) + sqx, sqy = int(x / sz), int(y / sz) + return set(self._row(y)) | set(self._column(x)) | set(self._square(sqx, sqy)) - # TODO: Break the above into helper methods that produce a single thing given an index. - def _row(self, r, size): - return range(r * size, r * size + size) + def _row(self, r): + row_size = self.row_size + return range(r * row_size, r * row_size + row_size) - def _column(self, c, size, size2): - return range(c, size2, size) + def _column(self, c): + return range(c, self.grid_size, self.row_size) - def _square(self, x, y, dim): - if (x < 0 or x >= dim) or (y < 0 or y >= dim): - raise IndexError('Invalid coordinates for square: ({}, {})'.format(x, y)) - - offset = (x * dim, y * dim * self.size) + def _square(self, x, y): + size = self.size + row_size = self.row_size + offx, offy = (x * size, y * size * row_size) def _range(i): - start = (offset[1] + i * self.size) + offset[0] - return range(start, start + dim) + start = (offy + i * row_size) + offx + return range(start, start + size) - ranges = itertools.chain(*[_range(i) for i in range(dim)]) + ranges = itertools.chain(*[_range(i) for i in range(size)]) return ranges @property def solved(self): - expected = set(range(1, self.size + 1)) + expected = self.possible_values all_groups = itertools.chain(self.rows, self.columns, self.squares) return all(expected == set(g) for g in all_groups) @@ -99,19 +110,19 @@ class Sudoku: return ((self._board[i] for i in r) for r in ranges) def __str__(self): - field_width = len(str(self.size)) - dim = self.dimension + field_width = len(str(max(self.possible_values))) + sz = self.size lines = [] - spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * dim) for _ in range(dim)])) + spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * sz) for _ in range(sz)])) for line in range(self.size): chunks = [] - for i in range(dim): + for i in range(sz): fields = [] - for j in range(dim): - idx = line * self.size + i * dim + j + for j in range(sz): + idx = line * self.size + i * sz + j fields.append('{{board[{i}]:^{width}}}'.format(i=idx, width=field_width)) chunks.append(''.join(fields)) - if (line % dim) == 0: + if (line % sz) == 0: lines.append(spacer) lines.append('{0}{1}{0}'.format('|', '|'.join(chunks))) lines.append(spacer) diff --git a/test.py b/test.py index a9695dc..559998f 100644 --- a/test.py +++ b/test.py @@ -9,13 +9,12 @@ import sudoku class Sudoku4TestCase(unittest.TestCase): def setUp(self): - self.board = sudoku.Sudoku(size=4) + self.board = sudoku.Sudoku(size=2) class Sudoku4BasicTests(Sudoku4TestCase): def test_that_board_is_sane(self): - self.assertEqual(self.board.size, 4) - self.assertEqual(len(self.board._board), 4**2) - self.assertEqual(self.board.dimension, 2) + self.assertEqual(self.board.size, 2) + self.assertEqual(len(self.board._board), 2**4) def test_rows(self): expected_rows = [ @@ -42,16 +41,16 @@ class Sudoku4BasicTests(Sudoku4TestCase): self.assertEqual(col_list, excol) def test_squares(self): - expected_squares = [ - [ 0, 1, 4, 5], - [ 2, 3, 6, 7], - [ 8, 9, 12, 13], - [10, 11, 14, 15] - ] - for (sq, exsq) in zip(self.board.index_squares, expected_squares): - sq_list = list(sq) - with self.subTest(sq=sq_list, ex=exsq): - self.assertEqual(sq_list, exsq) + expected_squares = { + (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, exsq) in expected_squares.items(): + with self.subTest(sq=coord, ex=exsq): + sq = set(self.board._square(*coord)) + self.assertEqual(sq, exsq) def test_peers(self): expected_peers = { From e5d00debc711bc020bc9d372ce0693a09d314fe0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 12:51:11 -0700 Subject: [PATCH 41/75] Rename square -> box --- sudoku.py | 23 +++++++++++++---------- test.py | 12 ++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/sudoku.py b/sudoku.py index 225f60b..a394b25 100644 --- a/sudoku.py +++ b/sudoku.py @@ -29,7 +29,10 @@ class Sudoku: return self.size ** 4 @property - def all_squares(self): + def all_boxes(self): + ''' + Iterator of xy-coordinates for every box in the grid. + ''' return itertools.product(range(self.size), repeat=2) @property @@ -49,8 +52,8 @@ class Sudoku: return self._apply_index_ranges(self.index_columns) @property - def squares(self): - return self._apply_index_ranges(self.index_squares) + def boxes(self): + return self._apply_index_ranges(self.index_boxes) @property def index_rows(self): @@ -67,19 +70,19 @@ class Sudoku: return (self._column(i) for i in range(self.row_size)) @property - def index_squares(self): + def index_boxes(self): ''' - Return an iterable of ranges of indexes into the board, each defining a square. + Return an iterable of ranges of indexes into the board, each defining a box. ''' - return (self._square(x, y) for (x,y) in self.all_squares) + return (self._box(x, y) for (x,y) in self.all_boxes) def peers(self, x, y): return {self._board[i] for i in self.index_peers(x, y) if self._board[i] != 0} def index_peers(self, x, y): sz = self.size - sqx, sqy = int(x / sz), int(y / sz) - return set(self._row(y)) | set(self._column(x)) | set(self._square(sqx, sqy)) + box = int(x / sz), int(y / sz) + return set(self._row(y)) | set(self._column(x)) | set(self._box(*box)) def _row(self, r): row_size = self.row_size @@ -88,7 +91,7 @@ class Sudoku: def _column(self, c): return range(c, self.grid_size, self.row_size) - def _square(self, x, y): + def _box(self, x, y): size = self.size row_size = self.row_size offx, offy = (x * size, y * size * row_size) @@ -103,7 +106,7 @@ class Sudoku: @property def solved(self): expected = self.possible_values - all_groups = itertools.chain(self.rows, self.columns, self.squares) + all_groups = itertools.chain(self.rows, self.columns, self.boxes) return all(expected == set(g) for g in all_groups) def _apply_index_ranges(self, ranges): diff --git a/test.py b/test.py index 559998f..09239ec 100644 --- a/test.py +++ b/test.py @@ -40,17 +40,17 @@ class Sudoku4BasicTests(Sudoku4TestCase): with self.subTest(col=col_list, ex=excol): self.assertEqual(col_list, excol) - def test_squares(self): - expected_squares = { + 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, exsq) in expected_squares.items(): - with self.subTest(sq=coord, ex=exsq): - sq = set(self.board._square(*coord)) - self.assertEqual(sq, exsq) + 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 = { From fb1066f71649260fe402a257c6056f0f39c79bf6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 12:51:27 -0700 Subject: [PATCH 42/75] Fix the board size parameter in a test --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 09239ec..763fc45 100644 --- a/test.py +++ b/test.py @@ -85,5 +85,5 @@ class Sudoku4SolvedTests(Sudoku4TestCase): def test_simple_solution_is_solved(self): board = (int(i) for i in '1234341221434321') - self.board = sudoku.Sudoku(4, board) + self.board = sudoku.Sudoku(2, board) self.assertTrue(self.board.solved) From a0594d1561fcc0e7c6cb1458ff848041d3f3a6d0 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 12:51:33 -0700 Subject: [PATCH 43/75] Add some doc strings --- sudoku.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sudoku.py b/sudoku.py index a394b25..1575002 100644 --- a/sudoku.py +++ b/sudoku.py @@ -18,14 +18,23 @@ class Sudoku: @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 @@ -38,8 +47,8 @@ class Sudoku: @property def possible_values(self): ''' - The set of valid values for any grid square, not accounting for values made invalid by already being - present in a peer of that square. + 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. ''' return set(range(1, self.row_size + 1)) From ab927ee41d574489a93f29f974fa3c47beb81024 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Tue, 10 Oct 2017 12:54:33 -0700 Subject: [PATCH 44/75] Move some stuff around; add some docstrings --- sudoku.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/sudoku.py b/sudoku.py index 1575002..13c56ef 100644 --- a/sudoku.py +++ b/sudoku.py @@ -64,6 +64,12 @@ class Sudoku: 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): ''' @@ -85,11 +91,10 @@ class Sudoku: ''' return (self._box(x, y) for (x,y) in self.all_boxes) - def peers(self, x, y): - return {self._board[i] for i in self.index_peers(x, y) if self._board[i] != 0} - def index_peers(self, x, y): - sz = self.size + ''' + Return a set of the peers, indexes into the board, for a given square. + ''' box = int(x / sz), int(y / sz) return set(self._row(y)) | set(self._column(x)) | set(self._box(*box)) From e2eadf9da76a986143a81e3d96b9ff8478489ccb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:49:18 -0700 Subject: [PATCH 45/75] Allow selecting and printing puzzles from the library to the command line --- puzzles.py | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/puzzles.py b/puzzles.py index a2e1fb3..0c8431a 100644 --- a/puzzles.py +++ b/puzzles.py @@ -4,31 +4,54 @@ Parser for puzzles in the ./puzzles directory. ''' +import argparse import sudoku +import sys euler = [] norvig = [] -def parse_puzzle_files(): +def parse_puzzle_files(quiet=True): global euler, norvig - print('Parsing Euler puzzles') - euler = list(_get_puzzles('puzzles/euler.txt')) + if not quiet: + print('Parsing Euler puzzles') + euler.extend(_get_puzzles('puzzles/euler.txt', quiet)) - print('Parsing Norvig puzzles') - norvig = list(_get_puzzles('puzzles/norvig.txt')) + if not quiet: + print('Parsing Norvig puzzles') + norvig.extend(_get_puzzles('puzzles/norvig.txt', quiet)) -def _get_puzzles(filename): +def _get_puzzles(filename, quiet): with open(filename, 'r') as f: puzzles = f.readlines() - return (_parse_puzzle(p) for p in puzzles if p) + return (_parse_puzzle(p, quiet) for p in puzzles if p) -def _parse_puzzle(puzzle): +def _parse_puzzle(puzzle, quiet): puzzle = puzzle.strip() if len(puzzle) == 81: - print("Parsing '{}'".format(puzzle)) + if not quiet: + print("Parsing '{}'".format(puzzle)) board = (int('0' if x == '.' else x) for x in puzzle) return sudoku.Sudoku(board=board) else: - print("Skipping '{}'".format(puzzle)) + if not quiet: + print("Skipping '{}'".format(puzzle)) return None + +def parse_args(args): + parser = argparse.ArgumentParser() + parser.add_argument('--euler', '-e', dest='library', action='store_const', const=euler, default=None) + parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) + parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('index', default=0, type=int) + return parser.parse_args(args) + +def main(): + args = parse_args(sys.argv[1:]) + parse_puzzle_files(quiet=not args.verbose) + print(args.library[args.index]) + return 0 + +if __name__ == '__main__': + sys.exit(main()) From a34987e3db7616ebf6ec7eac75fbd12c7bc4adef Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:51:55 -0700 Subject: [PATCH 46/75] Print multiple indexes --- puzzles.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/puzzles.py b/puzzles.py index 0c8431a..2a15d18 100644 --- a/puzzles.py +++ b/puzzles.py @@ -44,13 +44,14 @@ def parse_args(args): parser.add_argument('--euler', '-e', dest='library', action='store_const', const=euler, default=None) parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) parser.add_argument('--verbose', '-v', action='store_true', default=False) - parser.add_argument('index', default=0, type=int) + parser.add_argument('indexes', metavar='N', nargs='+', type=int) return parser.parse_args(args) def main(): args = parse_args(sys.argv[1:]) parse_puzzle_files(quiet=not args.verbose) - print(args.library[args.index]) + for i in args.indexes: + print(args.library[i]) return 0 if __name__ == '__main__': From 6da7c2ecd6c9863d5030da5f0c750fd86b5f5576 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:52:27 -0700 Subject: [PATCH 47/75] Store indexes of clues as a set --- sudoku.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sudoku.py b/sudoku.py index 13c56ef..083ce86 100644 --- a/sudoku.py +++ b/sudoku.py @@ -13,8 +13,10 @@ class Sudoku: sz4 = size ** 4 if board: self._board = bytes(board)[:sz4] + self._clues = set(i for i in range(len(self._board)) if self._board[i] != 0) else: self._board = bytes(sz4) + self._clues = set() @property def size(self): From 9534f04a0892a43e176dd3722feeee15869ea3c9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:52:53 -0700 Subject: [PATCH 48/75] Print the whole grid -- fallout from changing the meaning of size --- sudoku.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku.py b/sudoku.py index 083ce86..c8d6093 100644 --- a/sudoku.py +++ b/sudoku.py @@ -133,7 +133,7 @@ class Sudoku: sz = self.size lines = [] spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * sz) for _ in range(sz)])) - for line in range(self.size): + for line in range(self.row_size): chunks = [] for i in range(sz): fields = [] From b8eda7709358a0ec88c2a8bf3fe3a670c6262433 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:53:17 -0700 Subject: [PATCH 49/75] Bold clues when printing the board --- sudoku.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sudoku.py b/sudoku.py index c8d6093..cd8d7cf 100644 --- a/sudoku.py +++ b/sudoku.py @@ -7,6 +7,9 @@ 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 @@ -139,7 +142,12 @@ class Sudoku: fields = [] for j in range(sz): idx = line * self.size + i * sz + j - fields.append('{{board[{i}]:^{width}}}'.format(i=idx, width=field_width)) + if idx in self._clues: + bold = BOLD_SEQUENCE + unbold = UNBOLD_SEQUENCE + else: + bold = unbold = '' + fields.append('{bold}{{board[{i}]:^{{width}}}}{unbold}'.format(i=idx, bold=bold, unbold=unbold)) chunks.append(''.join(fields)) if (line % sz) == 0: lines.append(spacer) @@ -147,5 +155,5 @@ class Sudoku: lines.append(spacer) fmt = '\n'.join(lines) str_board = [str(n) if n != 0 else ' ' for n in self._board] - out = fmt.format(board=str_board) + out = fmt.format(board=str_board, width=field_width) return out From f12263b1976ee23fca5932d591c6d9c87ce8f885 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:53:25 -0700 Subject: [PATCH 50/75] Stub of a solve() method --- sudoku.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sudoku.py b/sudoku.py index cd8d7cf..ebea6fb 100644 --- a/sudoku.py +++ b/sudoku.py @@ -128,6 +128,9 @@ class Sudoku: 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.solve(self) + def _apply_index_ranges(self, ranges): return ((self._board[i] for i in r) for r in ranges) From c0021ac2c210a911fa85de7874ed779bb6080e05 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:58:31 -0700 Subject: [PATCH 51/75] Make a sudoku package --- sudoku.py => sudoku/__init__.py | 0 puzzles.py => sudoku/puzzles.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename sudoku.py => sudoku/__init__.py (100%) rename puzzles.py => sudoku/puzzles.py (100%) diff --git a/sudoku.py b/sudoku/__init__.py similarity index 100% rename from sudoku.py rename to sudoku/__init__.py diff --git a/puzzles.py b/sudoku/puzzles.py similarity index 100% rename from puzzles.py rename to sudoku/puzzles.py From ed419300925f3a80487713f6dfedc2c318c2583e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 15:59:14 -0700 Subject: [PATCH 52/75] Oops broke the tests --- sudoku/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index ebea6fb..30a3536 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -100,7 +100,7 @@ class Sudoku: ''' Return a set of the peers, indexes into the board, for a given square. ''' - box = int(x / sz), int(y / sz) + box = int(x / self.size), int(y / self.size) return set(self._row(y)) | set(self._column(x)) | set(self._box(*box)) def _row(self, r): From 7809c38104e4b0286ce4b6880b909944fe943bd9 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 16:03:45 -0700 Subject: [PATCH 53/75] Allow specifying path to puzzles library By default look in the current directory for a puzzles/ directory. --- sudoku/puzzles.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sudoku/puzzles.py b/sudoku/puzzles.py index 2a15d18..78404ef 100644 --- a/sudoku/puzzles.py +++ b/sudoku/puzzles.py @@ -5,22 +5,22 @@ Parser for puzzles in the ./puzzles directory. ''' import argparse -import sudoku +import os.path import sys euler = [] norvig = [] -def parse_puzzle_files(quiet=True): +def parse_puzzle_files(path, quiet=True): global euler, norvig if not quiet: print('Parsing Euler puzzles') - euler.extend(_get_puzzles('puzzles/euler.txt', quiet)) + euler.extend(_get_puzzles(os.path.join(path, 'euler.txt'), quiet)) if not quiet: print('Parsing Norvig puzzles') - norvig.extend(_get_puzzles('puzzles/norvig.txt', quiet)) + norvig.extend(_get_puzzles(os.path.join(path, 'norvig.txt'), quiet)) def _get_puzzles(filename, quiet): with open(filename, 'r') as f: @@ -33,7 +33,7 @@ def _parse_puzzle(puzzle, quiet): if not quiet: print("Parsing '{}'".format(puzzle)) board = (int('0' if x == '.' else x) for x in puzzle) - return sudoku.Sudoku(board=board) + return Sudoku(board=board) else: if not quiet: print("Skipping '{}'".format(puzzle)) @@ -44,12 +44,13 @@ def parse_args(args): parser.add_argument('--euler', '-e', dest='library', action='store_const', const=euler, default=None) parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) parser.add_argument('--verbose', '-v', action='store_true', default=False) + parser.add_argument('--library', '-l', dest='path', default='./puzzles') parser.add_argument('indexes', metavar='N', nargs='+', type=int) return parser.parse_args(args) def main(): args = parse_args(sys.argv[1:]) - parse_puzzle_files(quiet=not args.verbose) + parse_puzzle_files(args.path, quiet=not args.verbose) for i in args.indexes: print(args.library[i]) return 0 From 0976ec8bb57916e2e7df7d41cf9b4744725c1dc6 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 16:04:14 -0700 Subject: [PATCH 54/75] Move puzzles back up a level --- sudoku/puzzles.py => puzzles.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename sudoku/puzzles.py => puzzles.py (100%) diff --git a/sudoku/puzzles.py b/puzzles.py similarity index 100% rename from sudoku/puzzles.py rename to puzzles.py From 301a56904cffefc564cd6904c8096cd37304cafb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 16:05:10 -0700 Subject: [PATCH 55/75] Relative import Sudoku symbol --- puzzles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/puzzles.py b/puzzles.py index 78404ef..cb229ba 100644 --- a/puzzles.py +++ b/puzzles.py @@ -8,6 +8,8 @@ import argparse import os.path import sys +from sudoku import Sudoku + euler = [] norvig = [] From 77456d51142e819c68247dcdd81eae50654d1e0c Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 16:07:33 -0700 Subject: [PATCH 56/75] Add solvers package --- sudoku/solvers/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 sudoku/solvers/__init__.py diff --git a/sudoku/solvers/__init__.py b/sudoku/solvers/__init__.py new file mode 100644 index 0000000..de3672c --- /dev/null +++ b/sudoku/solvers/__init__.py @@ -0,0 +1,8 @@ +# Eryn Wells +''' +''' + +class Solver: + def solve(self, sudoku): + # TODO: Any subclass of Solver should implement this. + raise NotImplementedError From e329208db9cb3e77cd5eb66931d27440599aa1c1 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:40:39 -0700 Subject: [PATCH 57/75] Rewrite the printer to be cleaner --- sudoku/__init__.py | 61 ++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 30a3536..30cd255 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -42,6 +42,13 @@ class Sudoku: ''' 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): ''' @@ -131,32 +138,38 @@ class Sudoku: def solve(self, solver): return solver.solve(self) + 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 __str__(self): field_width = len(str(max(self.possible_values))) - sz = self.size - lines = [] - spacer = '{0}{1}{0}'.format('+', '+'.join(['-' * (field_width * sz) for _ in range(sz)])) - for line in range(self.row_size): - chunks = [] - for i in range(sz): - fields = [] - for j in range(sz): - idx = line * self.size + i * sz + j - if idx in self._clues: - bold = BOLD_SEQUENCE - unbold = UNBOLD_SEQUENCE - else: - bold = unbold = '' - fields.append('{bold}{{board[{i}]:^{{width}}}}{unbold}'.format(i=idx, bold=bold, unbold=unbold)) - chunks.append(''.join(fields)) - if (line % sz) == 0: - lines.append(spacer) - lines.append('{0}{1}{0}'.format('|', '|'.join(chunks))) - lines.append(spacer) - fmt = '\n'.join(lines) - str_board = [str(n) if n != 0 else ' ' for n in self._board] - out = fmt.format(board=str_board, width=field_width) - return out + 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) From 15492696619c7925d5d213998da1e7ede0c90961 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:41:32 -0700 Subject: [PATCH 58/75] Mutable squares, immutable clues set --- sudoku/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 30cd255..701ba39 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -15,11 +15,11 @@ class Sudoku: self._size = size sz4 = size ** 4 if board: - self._board = bytes(board)[:sz4] - self._clues = set(i for i in range(len(self._board)) if self._board[i] != 0) + self._board = bytearray(board)[:sz4] + self._clues = frozenset(i for i in range(len(self._board)) if self._board[i] != 0) else: - self._board = bytes(sz4) - self._clues = set() + self._board = bytearray(sz4) + self._clues = frozenset() @property def size(self): From f7b6fb053f64f05cc8de756ef1327f3614f45022 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:42:11 -0700 Subject: [PATCH 59/75] Memoize possible_values set --- sudoku/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 701ba39..fd29b5f 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -20,6 +20,7 @@ class Sudoku: else: self._board = bytearray(sz4) self._clues = frozenset() + self._possible_values = None @property def size(self): @@ -62,7 +63,9 @@ class Sudoku: 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. ''' - return set(range(1, self.row_size + 1)) + if not self._possible_values: + self._possible_values = set(range(1, self.row_size + 1)) + return self._possible_values @property def rows(self): From 181bc7d61e79949ea1ca2e063b6d9053babcc8b4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:43:15 -0700 Subject: [PATCH 60/75] Remove idx from peer set --- sudoku/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index fd29b5f..cbe5b38 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -110,8 +110,9 @@ class Sudoku: ''' 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)) + return (set(self._row(y)) | set(self._column(x)) | set(self._box(*box))) - {idx} def _row(self, r): row_size = self.row_size From 7a0e31858fdc622f3a5be88fac8d54fdc863151d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:43:42 -0700 Subject: [PATCH 61/75] Add __repr__ to Sudoku --- sudoku/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index cbe5b38..659739c 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -148,6 +148,11 @@ class Sudoku: 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)])) From 9ebabd4b56c637abf1d04ce5f3475fd80a07323d Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:44:19 -0700 Subject: [PATCH 62/75] Add square setter to Sudoku --- sudoku/__init__.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 659739c..d9f4311 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -142,6 +142,22 @@ class Sudoku: def solve(self, solver): return solver.solve(self) + 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)) + + 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._board[idx] = value + def _xy_to_idx(self, x, y): return y * self.row_size + x @@ -182,3 +198,15 @@ class Sudoku: 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 From 189589c0e614268cd990ecce1922d88563a152b5 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:44:41 -0700 Subject: [PATCH 63/75] solve() takes a function that solves the puzzle --- sudoku/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index d9f4311..1c52a20 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -140,7 +140,7 @@ class Sudoku: return all(expected == set(g) for g in all_groups) def solve(self, solver): - return solver.solve(self) + return solver(self) def set(self, x, y, value): idx = self._xy_to_idx(x, y) From 65f4453e786dea7361b051a84d85df127cffe9fb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:45:00 -0700 Subject: [PATCH 64/75] Attempt #1 at a backtracking solver --- sudoku/solvers/__init__.py | 6 ++--- sudoku/solvers/backtracker.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 sudoku/solvers/backtracker.py diff --git a/sudoku/solvers/__init__.py b/sudoku/solvers/__init__.py index de3672c..becaaba 100644 --- a/sudoku/solvers/__init__.py +++ b/sudoku/solvers/__init__.py @@ -1,8 +1,6 @@ # Eryn Wells ''' +A library of Sudoku solvers. ''' -class Solver: - def solve(self, sudoku): - # TODO: Any subclass of Solver should implement this. - raise NotImplementedError +from . import backtracker diff --git a/sudoku/solvers/backtracker.py b/sudoku/solvers/backtracker.py new file mode 100644 index 0000000..62cd01e --- /dev/null +++ b/sudoku/solvers/backtracker.py @@ -0,0 +1,48 @@ +# Eryn Wells + +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): + for value in sudoku.possible_values: + try: + print('({},{}) trying {}'.format(x, y, value)) + sudoku.set(x, y, value) + except SquareIsClue: + # Do nothing with this square; continue on to the next square below. + print('({},{}) square is clue'.format(x,y)) + pass + except ValueExistsInPeers: + print('({},{}) value exists in peer set'.format(x,y)) + # Try next value. + continue + except NoPossibleValues: + # Need to backtrack. + print('({},{}) backtracking'.format(x,y)) + raise Backtrack() + + next_coord = _next_coord(sudoku, x, y) + if not next_coord: + break + + try: + _solve_square(sudoku, *next_coord) + except Backtrack: + continue + 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) From d25a320c30d026bf1f0bb8ac32b92e42866bb2b4 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:45:46 -0700 Subject: [PATCH 65/75] puzzle script takes an algorithm to use to solve the given puzzles --- puzzles.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/puzzles.py b/puzzles.py index cb229ba..3db054f 100644 --- a/puzzles.py +++ b/puzzles.py @@ -9,6 +9,7 @@ import os.path import sys from sudoku import Sudoku +from sudoku.solvers import backtracker euler = [] norvig = [] @@ -44,9 +45,10 @@ def _parse_puzzle(puzzle, quiet): def parse_args(args): parser = argparse.ArgumentParser() parser.add_argument('--euler', '-e', dest='library', action='store_const', const=euler, default=None) - parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) - parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument('--library', '-l', dest='path', default='./puzzles') + parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) + parser.add_argument('--solver', '-s', default=None) + parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument('indexes', metavar='N', nargs='+', type=int) return parser.parse_args(args) @@ -54,7 +56,12 @@ def main(): args = parse_args(sys.argv[1:]) parse_puzzle_files(args.path, quiet=not args.verbose) for i in args.indexes: - print(args.library[i]) + puzzle = args.library[i] + print(puzzle) + if args.solver is not None: + if args.solver == 'backtracking': + puzzle.solve(backtracker.solve) + print(puzzle) return 0 if __name__ == '__main__': From 387ccacb8de0ce95528c94135103df626edef2fc Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 19:46:16 -0700 Subject: [PATCH 66/75] Print some stuff after setting a value --- sudoku/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 1c52a20..9d94255 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -157,6 +157,7 @@ class Sudoku: raise ValueExistsInPeers('{} already exists in the peer set for ({},{})'.format(value, x, y)) self._board[idx] = value + print('({},{}) <- {} (p:{}) {!r}'.format(x, y, value, peers, self)) def _xy_to_idx(self, x, y): return y * self.row_size + x From a9a331df91df44b871f3f0251add02270bc89feb Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 20:24:09 -0700 Subject: [PATCH 67/75] Move actual square setter to _set --- sudoku/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 9d94255..0865e3f 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -143,10 +143,6 @@ class Sudoku: return solver(self) 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)) - if value not in self.possible_values: raise ValueError('{} not in set of possible values {}'.format(value, self.possible_values)) @@ -156,8 +152,17 @@ class Sudoku: 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 - print('({},{}) <- {} (p:{}) {!r}'.format(x, y, value, peers, self)) + print('({},{}) <- {} {!r}'.format(x, y, value, self)) def _xy_to_idx(self, x, y): return y * self.row_size + x From 3b380b17d281cd60465a55a39dd9047a6a915c2e Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 20:25:11 -0700 Subject: [PATCH 68/75] Take care of some edge cases in the backtracker --- sudoku/solvers/backtracker.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sudoku/solvers/backtracker.py b/sudoku/solvers/backtracker.py index 62cd01e..fd0f6a0 100644 --- a/sudoku/solvers/backtracker.py +++ b/sudoku/solvers/backtracker.py @@ -12,21 +12,21 @@ def solve(sudoku): 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: - print('({},{}) trying {}'.format(x, y, value)) sudoku.set(x, y, value) except SquareIsClue: # Do nothing with this square; continue on to the next square below. - print('({},{}) square is clue'.format(x,y)) pass except ValueExistsInPeers: - print('({},{}) value exists in peer set'.format(x,y)) # Try next value. + should_backtrack = True continue except NoPossibleValues: # Need to backtrack. - print('({},{}) backtracking'.format(x,y)) + sudoku.unset(x, y) raise Backtrack() next_coord = _next_coord(sudoku, x, y) @@ -34,9 +34,19 @@ def _solve_square(sudoku, x, y): break try: - _solve_square(sudoku, *next_coord) + return _solve_square(sudoku, *next_coord) except Backtrack: + should_backtrack = True continue + + if should_backtrack: + # Unhandled backtrack. Pop out of this one too. + try: + sudoku.unset(x, y) + except SquareIsClue: + pass + raise Backtrack() + return sudoku def _next_coord(sudoku, x, y): From a7dda24015c5c6c8f5d99459e0020d063366fa19 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Wed, 11 Oct 2017 20:53:10 -0700 Subject: [PATCH 69/75] Print stuff in backtracking module only; clean up the \r stuff --- sudoku/__init__.py | 1 - sudoku/solvers/backtracker.py | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 0865e3f..92d92ae 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -162,7 +162,6 @@ class Sudoku: if idx in self._clues: raise SquareIsClue('Cannot set clue square ({},{})'.format(x, y)) self._board[idx] = value - print('({},{}) <- {} {!r}'.format(x, y, value, self)) def _xy_to_idx(self, x, y): return y * self.row_size + x diff --git a/sudoku/solvers/backtracker.py b/sudoku/solvers/backtracker.py index fd0f6a0..d58fad2 100644 --- a/sudoku/solvers/backtracker.py +++ b/sudoku/solvers/backtracker.py @@ -26,8 +26,10 @@ def _solve_square(sudoku, x, y): continue except NoPossibleValues: # Need to backtrack. - sudoku.unset(x, y) - raise Backtrack() + should_backtrack = True + break + + print('\r{!r}'.format(sudoku), end='', flush=True) next_coord = _next_coord(sudoku, x, y) if not next_coord: @@ -40,13 +42,14 @@ def _solve_square(sudoku, x, y): continue if should_backtrack: - # Unhandled backtrack. Pop out of this one too. try: sudoku.unset(x, y) except SquareIsClue: pass raise Backtrack() + print() + return sudoku def _next_coord(sudoku, x, y): From 1186305712b30c7ffa634009246420fc984d2896 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Thu, 12 Oct 2017 12:29:49 -0700 Subject: [PATCH 70/75] Clean up puzzles.py interface --- puzzles.py | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) mode change 100644 => 100755 puzzles.py diff --git a/puzzles.py b/puzzles.py old mode 100644 new mode 100755 index 3db054f..1d7dc00 --- a/puzzles.py +++ b/puzzles.py @@ -8,22 +8,16 @@ import argparse import os.path import sys -from sudoku import Sudoku -from sudoku.solvers import backtracker +from sudoku import Sudoku, solvers euler = [] norvig = [] -def parse_puzzle_files(path, quiet=True): - global euler, norvig - +def parse_puzzle_library(path, quiet=True): if not quiet: - print('Parsing Euler puzzles') - euler.extend(_get_puzzles(os.path.join(path, 'euler.txt'), quiet)) - - if not quiet: - print('Parsing Norvig puzzles') - norvig.extend(_get_puzzles(os.path.join(path, 'norvig.txt'), quiet)) + print('Parsing puzzles in {}'.format(path)) + puzzles = _get_puzzles(path, quiet) + return puzzles def _get_puzzles(filename, quiet): with open(filename, 'r') as f: @@ -44,23 +38,28 @@ def _parse_puzzle(puzzle, quiet): def parse_args(args): parser = argparse.ArgumentParser() - parser.add_argument('--euler', '-e', dest='library', action='store_const', const=euler, default=None) - parser.add_argument('--library', '-l', dest='path', default='./puzzles') - parser.add_argument('--norvig', '-n', dest='library', action='store_const', const=norvig, default=None) - parser.add_argument('--solver', '-s', default=None) - parser.add_argument('--verbose', '-v', action='store_true', default=False) - parser.add_argument('indexes', metavar='N', nargs='+', type=int) + 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(): args = parse_args(sys.argv[1:]) - parse_puzzle_files(args.path, quiet=not args.verbose) + puzzle_library = list(parse_puzzle_library(args.library, quiet=not args.verbose)) for i in args.indexes: - puzzle = args.library[i] + puzzle = puzzle_library[i] print(puzzle) if args.solver is not None: - if args.solver == 'backtracking': - puzzle.solve(backtracker.solve) + try: + solver = getattr(solvers, args.solver) + puzzle.solve(solver.solve) + except AttributeError: + print('No solver named {}'.format(args.solver)) print(puzzle) return 0 From fba52c7f090518420819dc127c80e06148af2de3 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Oct 2017 12:23:42 -0700 Subject: [PATCH 71/75] Add Sudoku.possible_values_for_square() --- sudoku/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 92d92ae..a693408 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -67,6 +67,10 @@ class Sudoku: self._possible_values = set(range(1, self.row_size + 1)) return self._possible_values + def possible_values_for_square(self, x, y): + peers = self.peers(x, y) + return self.possible_values - peers + @property def rows(self): return self._apply_index_ranges(self.index_rows) From 137e4e643691899e21932412e367e4c51ea30850 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Oct 2017 12:24:02 -0700 Subject: [PATCH 72/75] Add Sudoku.get() -> value of board at (x,y) --- sudoku/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index a693408..130b175 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -146,6 +146,11 @@ class Sudoku: 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)) From c9a86d0fdc4e0f6607873cac4c447c46c2dca524 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Oct 2017 12:25:30 -0700 Subject: [PATCH 73/75] 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. --- sudoku/solvers/__init__.py | 2 +- sudoku/solvers/dlx.py | 137 +++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 sudoku/solvers/dlx.py diff --git a/sudoku/solvers/__init__.py b/sudoku/solvers/__init__.py index becaaba..2f2ffed 100644 --- a/sudoku/solvers/__init__.py +++ b/sudoku/solvers/__init__.py @@ -3,4 +3,4 @@ A library of Sudoku solvers. ''' -from . import backtracker +from . import backtracker, dlx diff --git a/sudoku/solvers/dlx.py b/sudoku/solvers/dlx.py new file mode 100644 index 0000000..e3657e0 --- /dev/null +++ b/sudoku/solvers/dlx.py @@ -0,0 +1,137 @@ +# Eryn Wells + +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 = {board_value} if board_value else 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 From 3689ac69748b8540f659e71db2109663d97803a2 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Oct 2017 12:40:00 -0700 Subject: [PATCH 74/75] Move existing test file alongside the sudoku __init__.py --- test.py => sudoku/test.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) rename test.py => sudoku/test.py (72%) diff --git a/test.py b/sudoku/test.py similarity index 72% rename from test.py rename to sudoku/test.py index 763fc45..b8fccd2 100644 --- a/test.py +++ b/sudoku/test.py @@ -54,25 +54,25 @@ class Sudoku4BasicTests(Sudoku4TestCase): def test_peers(self): expected_peers = { - (0,0): set([0, 1, 2, 3, 4, 8, 12, 5]), - (1,0): set([0, 1, 2, 3, 5, 9, 13, 4]), - (2,0): set([0, 1, 2, 3, 6, 10, 14, 7]), - (3,0): set([0, 1, 2, 3, 7, 11, 15, 6]), + (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([4, 5, 6, 7, 0, 8, 12, 1]), - (1,1): set([4, 5, 6, 7, 1, 9, 13, 0]), - (2,1): set([4, 5, 6, 7, 2, 10, 14, 3]), - (3,1): set([4, 5, 6, 7, 3, 11, 15, 2]), + (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([8, 9, 10, 11, 0, 4, 12, 13]), - (1,2): set([8, 9, 10, 11, 1, 5, 13, 12]), - (2,2): set([8, 9, 10, 11, 2, 6, 14, 15]), - (3,2): set([8, 9, 10, 11, 3, 7, 15, 14]), + (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([12, 13, 14, 15, 0, 4, 8, 9]), - (1,3): set([12, 13, 14, 15, 1, 5, 9, 8]), - (2,3): set([12, 13, 14, 15, 2, 6, 10, 11]), - (3,3): set([12, 13, 14, 15, 3, 7, 11, 10]), + (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): From 2529533661da0d229d00651f3599c30cf8d57b79 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Mon, 16 Oct 2017 12:40:29 -0700 Subject: [PATCH 75/75] Adjust possible_values_for_square to behave like dlx expects --- sudoku/__init__.py | 8 ++++++-- sudoku/solvers/dlx.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sudoku/__init__.py b/sudoku/__init__.py index 130b175..3cb0572 100644 --- a/sudoku/__init__.py +++ b/sudoku/__init__.py @@ -68,8 +68,12 @@ class Sudoku: return self._possible_values def possible_values_for_square(self, x, y): - peers = self.peers(x, y) - return self.possible_values - peers + value = self.get(x, y) + if value: + return {value} + else: + peers = self.peers(x, y) + return self.possible_values - peers @property def rows(self): diff --git a/sudoku/solvers/dlx.py b/sudoku/solvers/dlx.py index e3657e0..39e4b98 100644 --- a/sudoku/solvers/dlx.py +++ b/sudoku/solvers/dlx.py @@ -124,7 +124,7 @@ def _build_headers(sudoku): def _build_rows(sudoku, headers): for (index, coords) in enumerate(sudoku.all_squares): board_value = sudoku.get(*coords) - possibilities = {board_value} if board_value else sudoku.possible_values_for_square(*coords) + possibilities = sudoku.possible_values_for_square(*coords) for value in possibilities: cur = None for col in headers.iterate_row():