sudoku-solver/board.py

300 lines
10 KiB
Python

"""This module contains the Board class.
It is used to represent different elements for the whole board.
"""
from typing import List
from square import Square
class Board:
"""The Board class contains different elements of the board."""
def __init__(self, string: str, debug=False):
self.string = string
self.debug = 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):
output = ""
for lin_nb, line in enumerate(self.lines):
if (lin_nb) % 3 == 0:
output += "+-------+-------+-------+\n"
for col_nb, square in enumerate(line):
if (col_nb) % 3 == 0:
output += '| '
if isinstance(square, set):
if len(square) == 1:
output += str(square)[2:-2]
else:
output += '.'
elif isinstance(square, Square):
if len(square.values) == 1:
output += str(square.values)[2:-2]
else:
output += '.'
else:
output += square
output += ' '
output += '|\n'
output += "+-------+-------+-------+\n"
return output
def __compute_line_values(self):
"""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):
"""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):
"""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):
"""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_possible_values(self):
"""Compute possible values for each square in the board."""
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_in_each_line(self):
"""Find obligated values per line 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.
"""
self.__debug(">>>Compute obligated in each line")
self.__debug_pause()
for line_id, line in enumerate(self.lines):
self.__debug(f"Line n°{line_id}")
for square_id, square in enumerate(line):
self.__debug(f" square n°{square_id}, {square.values}")
# Create an empty set to store the other square's values.
other_values = set()
for other_square_id in range(9):
# ignore current square
if other_square_id != square_id:
# update other_values with values of other squares
other_values = other_values.union(
line[other_square_id].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" {number}")
self.lines[line_id][square_id].values = {str(number)}
self.__debug_print_board()
def compute_obligated_in_each_column(self):
"""Find obligated values per column for every number.
Go through each column. 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 column then the square should take that number.
"""
self.__debug(">>>Compute obligated in each column")
self.__debug_pause()
for col_id, column in enumerate(self.columns):
self.__debug(f"Column n°{col_id}")
for square_id, square in enumerate(column):
self.__debug(f" square n°{square_id}, {square.values}")
# Create an empty set to store the other square's values.
other_values = set()
for other_square_id in range(9):
# ignore current square
if other_square_id != square_id:
# update other_values with values of other squares
other_values = other_values.union(
column[other_square_id].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" {number}")
self.columns[col_id][square_id].values = {str(number)}
self.__debug_print_board()
def get_new_board(self) -> str:
"""Return the board in its current state as a string.
This methods let's us recreate a new board from a string so we can
reiterate throught the solving process.
"""
self.__debug(">>>Get new board")
self.__debug_pause()
output = ""
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
return output