sudoku-solver/board.py

414 lines
14 KiB
Python

"""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