"""This module contains the Board class. It is used to represent different elements for the whole board. It also contains methods to compute different possible values for each square on the board. """ from typing import List, Optional, Tuple from square import Square class Board: """The Board class contains different elements of the board. When the board is instantiated, it has the following attributes: self.string The original string that was passed in. self.debug (bool) Gives verbose output. The four following attributes are computed from self.string: self.lines List of 9 sets, each containing the values on the board at that line number. self.columns List of 9 sets, each containing the values on the board at that column number. self.grids List of 9 sets, each containing the values on the board at that grid number. self.squares List containing all 81 Squares of the board. -- """ def __init__(self, string: str, debug: bool = False): self.string: str = string self.debug: bool = debug self.lines: List[set] = self.__compute_line_values() self.__debug("Initialized lines:") for i, line in enumerate(self.lines): self.__debug(f"Line n°{i}: {line}") self.__debug_pause("Continue") self.columns: List[set] = self.__compute_column_values() self.__debug("Initialized columns:") for i, column in enumerate(self.columns): self.__debug(f"Column n°{i}: {column}") self.__debug_pause("Continue") self.grids: List[set] = self.__compute_grid_values() self.__debug("Initialized grids:") for i, grid in enumerate(self.grids): self.__debug(f"Grid n°{i}: {grid}") self.__debug_pause("Continue") self.squares: List[Square] = self.__compute_square_values() self.__debug("Initialized squares:") for i, square in enumerate(self.squares): self.__debug(f"Square n°{i} {square.coordinates}: {square.values}") self.__debug_pause("Continue") def __debug(self, string: str): if self.debug: print(string) def __debug_pause(self, string: str = ""): if self.debug: input(string) def __debug_print_board(self): if self.debug: print(self) def __str__(self) -> str: self.__debug(self.string) output: str = "" for square_nb, square in enumerate(self.squares): assert isinstance(square, Square) # square_nb // 9 gives us line number if (square_nb // 9) % 3 == 0 and square_nb % 9 == 0: output += "+-------+-------+-------+\n" # square_nb % 9 gives us column number if (square_nb % 9) % 3 == 0: output += '| ' if len(square.values) == 1: # example : "{'6'}" -> take away {''} output += str(square.values)[2:-2] else: output += '.' output += ' ' if square_nb % 9 == 8: output += '|\n' output += "+-------+-------+-------+\n" return output def __compute_line_values(self) -> List[set]: """Return a list of sets containing the values in each line.""" lines: List[str] = [""] * 9 i = 0 line_nb = 0 while line_nb < 9: if self.string[i] == '\n': line_nb += 1 elif self.string[i] != '.': lines[line_nb] += self.string[i] i += 1 output: List[set] = [set(values) for values in lines] # Make sure the sets reflect the strings for i in range(9): assert len(output[i]) == len(lines[i]) return output def __compute_column_values(self) -> List[set]: """Return a list of sets containing the values in each column.""" columns: List[str] = [""] * 9 string: str = self.string.replace('\n', '') assert len(string) == 81 i: int = 0 column_nb: int = i % 9 while i < 81: if string[i] != '.': columns[column_nb] += string[i] i += 1 column_nb = i % 9 output: List[set] = [set(values) for values in columns] # Make sure the sets reflect the strings for i in range(9): assert len(output[i]) == len(columns[i]) return output def __compute_grid_values(self) -> List[set]: """Return a list of sets containing the values in each grid.""" grids: List[str] = [""] * 9 lines: List[str] = self.string.split('\n')[:-1] assert len(lines) == 9 grid_mappings = [ (0, 0), # grid 0 (0, 1), # grid 1 (0, 2), # grid 2 (1, 0), # grid 3 (1, 1), # grid 4 (1, 2), # grid 5 (2, 0), # grid 6 (2, 1), # grid 7 (2, 2), # grid 8 ] for line_nb, line in enumerate(lines): for column_nb in range(9): if line[column_nb] != '.': grid_coordinates = (line_nb // 3, column_nb // 3) # Find the grid that the current character belongs to grid_nb: int = 0 while grid_mappings[grid_nb] != grid_coordinates: grid_nb += 1 if grid_nb == 9: raise AssertionError("Board is invalid") grids[grid_nb] += line[column_nb] output: List[set] = [set(values) for values in grids] # Make sure the sets reflect the strings for i in range(9): assert len(output[i]) == len(grids[i]) return output def __compute_square_values(self) -> List[Square]: """Return a list of Square objects.""" squares: List[Square] = [] lines: List[str] = self.string.split('\n')[:-1] assert len(lines) == 9 for line_nb, line in enumerate(lines): for column_nb in range(9): if line[column_nb] == '.': square = Square(line_nb, column_nb, set()) else: square = Square(line_nb, column_nb, set(line[column_nb])) squares.append(square) return squares def __compute_obligated_in_each_line(self): """Find obligated values per line for every number.""" self.__debug(">>>Compute obligated in each line") self.__debug_pause() for line_nb in range(9): self.__debug(f"Line n°{line_nb}") for column_nb in range(9): square, square_nb = self.get_square(line_nb, column_nb) self.__debug(f" square n°{square_nb}, {square.values}") # Create an empty set to store the other square's values. other_values = set() other_squares = [ self.get_square(line_nb, other_square_nb)[0] for other_square_nb in range(9) if other_square_nb != column_nb ] for other_square in other_squares: # update other_values with values of other squares other_values = other_values.union(other_square.values) self.__debug(f" other values: {other_values}") for number in range(1, 10): if str(number) in square.values and \ str(number) not in other_values: self.__debug(f" current number: {number}") self.squares[square_nb].values = {str(number)} self.__debug_print_board() def __compute_obligated_in_each_column(self): """Find obligated values per column for every number.""" self.__debug(">>>Compute obligated in each column") self.__debug_pause() for column_nb in range(9): self.__debug(f"Column n°{column_nb}") for line_nb in range(9): square, square_nb = self.get_square(line_nb, column_nb) self.__debug(f" square n°{square_nb}, {square.values}") # Create an empty set to store the other square's values. other_values = set() other_squares = [ self.get_square(other_square_nb, column_nb)[0] for other_square_nb in range(9) if other_square_nb != line_nb ] for other_square in other_squares: # update other_values with values of other squares other_values = other_values.union(other_square.values) self.__debug(f" other values: {other_values}") for number in range(1, 10): if str(number) in square.values and \ str(number) not in other_values: self.__debug(f" current number: {number}") self.squares[square_nb].values = {str(number)} self.__debug_print_board() def __compute_obligated_in_each_grid(self): """Find obligated values per grid for every number.""" self.__debug(">>>Compute obligated in each grid") self.__debug_pause() for grid_nb in range(9): self.__debug(f"Grid n°{grid_nb}") for square_nb, square in enumerate(self.squares): if square.grid == grid_nb: self.__debug(f" square n°{square_nb}, {square.values}") # Create an empty set to store the other square's values. other_values = set() other_squares = [ other_square for other_square in self.squares if other_square.coordinates != square.coordinates and other_square.grid == square.grid ] for other_square in other_squares: self.__debug(other_square.coordinates) # update other_values with values of other squares other_values = other_values.union(other_square.values) self.__debug(f" other values: {other_values}") for number in range(1, 10): if str(number) in square.values and \ str(number) not in other_values: self.__debug(f" current number: {number}") self.squares[square_nb].values = {str(number)} self.__debug_print_board() def compute_possible_values(self): """Compute possible values for each square in the board. This method should be run just after instantiation to update each square's values, or after the board is updated. """ self.__debug(">>>Compute possible values") self.__debug_pause() for square_nb, square in enumerate(self.squares): if len(square.values) == 0: for number in range(1, 10): if str(number) not in self.lines[square.line] and \ str(number) not in self.columns[square.column] and \ str(number) not in self.grids[square.grid]: self.squares[square_nb].values.add(str(number)) self.__debug( f"Square n°{square_nb} {square.coordinates}: {square.values}" ) self.__debug_print_board() def compute_obligated_values(self): """Find obligated values for every number. Go through each line. For each square, go through numbers 1 to 9. If the number is in the square's values but not in the values of the rest of the squares in the line then the square should take that number. Do the same for each column. Do the same for each grid. This method should be run after compute_possible_values to update each square's values. """ self.__debug(">>>Compute obligated values") self.__debug_pause() self.__compute_obligated_in_each_line() self.__compute_obligated_in_each_column() self.__compute_obligated_in_each_grid() def get_square(self, line: int, column: int) -> Tuple[Optional[Square], Optional[int]]: """Given a line and a column, return corresponding square and number. If the square wasn't found, i.e. the coordinates are not valid, return None. """ searched_coordinates: Tuple[int, int] = (line, column) for square_nb, square in enumerate(self.squares): if square.coordinates == searched_coordinates: return square, square_nb return None, None def update_board(self): """Update the board to reflect self.string's current state. This method should be run after updating the board's string. Otherwise it is just like resetting the board to its instantiation state. """ self.__debug(">>>Update board") self.__debug_pause() self.lines = self.__compute_line_values() self.columns = self.__compute_column_values() self.grids = self.__compute_grid_values() self.squares = self.__compute_square_values() def update_string(self) -> str: """Update self.string to reflect the board's current state.""" self.__debug(">>>Update board.string") self.__debug_pause() output: str = "" column = 0 for square in self.squares: if len(square.values) == 1: output += str(square.values)[2:-2] else: output += '.' column += 1 if column == 9: output += '\n' column = 0 if self.string == output and '.' in self.string: raise StopIteration("No update was made, will loop forever.") self.string = output return output