#!/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)