pax_global_header00006660000000000000000000000064150475536730014531gustar00rootroot0000000000000052 comment=53cac393b5eed614e27b67d3b0b03f203bd334b5 python-sudoku-2.0.0/000077500000000000000000000000001504755367300143615ustar00rootroot00000000000000python-sudoku-2.0.0/.github/000077500000000000000000000000001504755367300157215ustar00rootroot00000000000000python-sudoku-2.0.0/.github/FUNDING.yml000066400000000000000000000014711504755367300175410ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry polar: # Replace with a single Polar username buy_me_a_coffee: jeffsieu custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] python-sudoku-2.0.0/.gitignore000066400000000000000000000034061504755367300163540ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/python-sudoku-2.0.0/LICENSE000066400000000000000000000020511504755367300153640ustar00rootroot00000000000000MIT License Copyright (c) 2019 Jeff Sieu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.python-sudoku-2.0.0/README.md000066400000000000000000000173031504755367300156440ustar00rootroot00000000000000# py-sudoku A simple Python package that generates and solves m x n Sudoku puzzles. ## Install ```sh # Python 2 pip install py-sudoku # Python 3 pip3 install py-sudoku ``` ## Usage ### Basic usage ```py from sudoku import Sudoku # Initializes a Sudoku puzzle with 3 x 3 sub-grid and # generates a puzzle with half of the cells empty puzzle = Sudoku(3).difficulty(0.5) puzzle.show() # +-------+-------+-------+ # | 4 1 | 3 | 7 6 | # | 9 3 | 7 | 4 1 | # | 2 | 1 4 | 8 3 | # +-------+-------+-------+ # | 9 5 8 | | 7 | # | 3 4 | 7 | 1 | # | 7 2 | 8 9 3 | 5 4 | # +-------+-------+-------+ # | 8 | 2 | 3 7 4 | # | 4 | | 1 9 5 | # | | 5 | 6 | # +-------+-------+-------+ solution = puzzle.solve() solution.show() # +-------+-------+-------+ # | 4 1 5 | 3 8 9 | 7 6 2 | # | 8 9 3 | 6 7 2 | 4 5 1 | # | 2 6 7 | 1 4 5 | 9 8 3 | # +-------+-------+-------+ # | 9 5 8 | 4 1 6 | 2 3 7 | # | 3 4 6 | 5 2 7 | 8 1 9 | # | 1 7 2 | 8 9 3 | 5 4 6 | # +-------+-------+-------+ # | 5 8 9 | 2 6 1 | 3 7 4 | # | 6 2 4 | 7 3 8 | 1 9 5 | # | 7 3 1 | 9 5 4 | 6 2 8 | # +-------+-------+-------+ solution.board # [[4, 1, 5, 3, 8, 9, 7, 6, 2], # [8, 9, 3, 6, 7, 2, 4, 5, 1], # [2, 6, 7, 1, 4, 5, 9, 8, 3], # [9, 5, 8, 4, 1, 6, 2, 3, 7], # [3, 4, 6, 5, 2, 7, 8, 1, 9], # [1, 7, 2, 8, 9, 3, 5, 4, 6], # [5, 8, 9, 2, 6, 1, 3, 7, 4], # [6, 2, 4, 7, 3, 8, 1, 9, 5], # [7, 3, 1, 9, 5, 4, 6, 2, 8]] solution.width # 3 solution.height # 3 ``` ### Creating puzzles m x n rectangular puzzles can be initialized using the `Sudoku(width)` or `Sudoku(width, height)` constructors. ```py # Initializes a 3 x 5 puzzle puzzle = Sudoku(3, 5) # Initializes a 4 x 4 puzzle puzzle = Sudoku(4) puzzle = Sudoku(4, 4) ``` Use ```solve()``` to get a solved puzzle, or ```difficulty(x)``` to create a problem. ```py # Create a 3 x 5 sub-grid problem with 0.4 difficulty (40% of cells empty) puzzle = Sudoku(3, 5).difficulty(0.4) # Create a solved 4 x 4 problem puzzle = Sudoku(4).solve() ``` ### Displaying puzzles ```py solution = Sudoku(5, 3).solve() # Shows the puzzle only solution.show() # +----------------+----------------+----------------+ # | 09 10 11 04 06 | 05 01 03 12 13 | 08 14 15 02 07 | # | 03 05 07 08 01 | 02 14 15 09 04 | 06 10 11 12 13 | # | 12 02 13 14 15 | 07 10 06 11 08 | 01 03 04 05 09 | # +----------------+----------------+----------------+ # | 13 14 06 11 08 | 15 07 09 02 12 | 10 01 05 03 04 | # | 10 03 15 05 02 | 13 04 08 14 01 | 12 09 07 11 06 | # | 01 07 04 09 12 | 03 05 10 06 11 | 13 02 08 15 14 | # +----------------+----------------+----------------+ # | 07 13 08 15 05 | 12 11 04 10 03 | 14 06 09 01 02 | # | 06 01 12 03 09 | 08 02 07 15 14 | 11 13 10 04 05 | # | 04 11 10 02 14 | 06 09 01 13 05 | 15 08 12 07 03 | # +----------------+----------------+----------------+ # | 08 12 02 06 10 | 01 13 11 05 07 | 03 04 14 09 15 | # | 05 15 09 13 11 | 14 03 12 04 10 | 02 07 06 08 01 | # | 14 04 01 07 03 | 09 06 02 08 15 | 05 11 13 10 12 | # +----------------+----------------+----------------+ # | 11 09 03 12 13 | 10 15 14 07 02 | 04 05 01 06 08 | # | 15 06 14 01 04 | 11 08 05 03 09 | 07 12 02 13 10 | # | 02 08 05 10 07 | 04 12 13 01 06 | 09 15 03 14 11 | # +----------------+----------------+----------------+ # Use print or show_full to display more information print(solution) solution.show_full() # --------------------------- # 15x15 (5x3) SUDOKU PUZZLE # Difficulty: SOLVED # --------------------------- # +----------------+----------------+----------------+ # | 09 10 11 04 06 | 05 01 03 12 13 | 08 14 15 02 07 | # | 03 05 07 08 01 | 02 14 15 09 04 | 06 10 11 12 13 | # | 12 02 13 14 15 | 07 10 06 11 08 | 01 03 04 05 09 | # +----------------+----------------+----------------+ # | 13 14 06 11 08 | 15 07 09 02 12 | 10 01 05 03 04 | # | 10 03 15 05 02 | 13 04 08 14 01 | 12 09 07 11 06 | # | 01 07 04 09 12 | 03 05 10 06 11 | 13 02 08 15 14 | # +----------------+----------------+----------------+ # | 07 13 08 15 05 | 12 11 04 10 03 | 14 06 09 01 02 | # | 06 01 12 03 09 | 08 02 07 15 14 | 11 13 10 04 05 | # | 04 11 10 02 14 | 06 09 01 13 05 | 15 08 12 07 03 | # +----------------+----------------+----------------+ # | 08 12 02 06 10 | 01 13 11 05 07 | 03 04 14 09 15 | # | 05 15 09 13 11 | 14 03 12 04 10 | 02 07 06 08 01 | # | 14 04 01 07 03 | 09 06 02 08 15 | 05 11 13 10 12 | # +----------------+----------------+----------------+ # | 11 09 03 12 13 | 10 15 14 07 02 | 04 05 01 06 08 | # | 15 06 14 01 04 | 11 08 05 03 09 | 07 12 02 13 10 | # | 02 08 05 10 07 | 04 12 13 01 06 | 09 15 03 14 11 | # +----------------+----------------+----------------+ ``` ### Seeds Problems can be generated with a certain seed. ```py # Generates a 3x2 puzzle with a given seed Sudoku(3, 2, seed=100).solve().show() # +-------+-------+ # | 5 6 3 | 1 2 4 | # | 2 1 4 | 5 3 6 | # +-------+-------+ # | 1 5 2 | 6 4 3 | # | 3 4 6 | 2 5 1 | # +-------+-------+ # | 6 3 5 | 4 1 2 | # | 4 2 1 | 3 6 5 | # +-------+-------+ ``` ### Importing boards Puzzle boards can also be imported. ```py board = [ [0,0,7,0,4,0,0,0,0], [0,0,0,0,0,8,0,0,6], [0,4,1,0,0,0,9,0,0], [0,0,0,0,0,0,1,7,0], [0,0,0,0,0,6,0,0,0], [0,0,8,7,0,0,2,0,0], [3,0,0,0,0,0,0,0,0], [0,0,0,1,2,0,0,0,0], [8,6,0,0,7,0,0,0,5] ] puzzle = Sudoku(3, 3, board=board) print(puzzle) # --------------------------- # 9x9 (3x3) SUDOKU PUZZLE # Difficulty: 0.74 # --------------------------- # +-------+-------+-------+ # | 7 | 4 | | # | | 8 | 6 | # | 4 1 | | 9 | # +-------+-------+-------+ # | | | 1 7 | # | | 6 | | # | 8 | 7 | 2 | # +-------+-------+-------+ # | 3 | | | # | | 1 2 | | # | 8 6 | 7 0 | 5 | # +-------+-------+-------+ puzzle.solve().show_full() # --------------------------- # 9x9 (3x3) SUDOKU PUZZLE # Difficulty: SOLVED # --------------------------- # +-------+-------+-------+ # | 9 8 7 | 6 4 2 | 5 3 1 | # | 2 3 5 | 9 1 8 | 7 4 6 | # | 6 4 1 | 5 3 7 | 9 8 2 | # +-------+-------+-------+ # | 5 2 6 | 3 8 4 | 1 7 9 | # | 1 7 3 | 2 9 6 | 8 5 4 | # | 4 9 8 | 7 5 1 | 2 6 3 | # +-------+-------+-------+ # | 3 1 9 | 8 6 5 | 4 2 7 | # | 7 5 4 | 1 2 3 | 6 9 8 | # | 8 6 2 | 4 7 9 | 3 1 5 | # +-------+-------+-------+ ``` ### Invalid boards Invalid boards give errors when attempted to be solved. ```py board = [ [0,0,7,0,4,0,0,0,0], [0,0,0,0,0,8,0,0,6], [0,4,1,0,0,0,9,0,0], [0,0,0,0,0,0,1,7,0], [0,0,0,0,0,6,0,0,0], [0,0,8,7,0,0,2,0,0], [3,0,0,0,0,0,0,0,0], [0,0,0,1,2,0,0,0,0], [8,6,0,0,7,6,0,0,5] ] puzzle = Sudoku(3, 3, board=board) puzzle.show_full() # --------------------------- # 9x9 (3x3) SUDOKU PUZZLE # Difficulty: 0.74 # --------------------------- # +-------+-------+-------+ # | 7 | 4 | | # | | 8 | 6 | # | 4 1 | | 9 | # +-------+-------+-------+ # | | | 1 7 | # | | 6 | | # | 8 | 7 | 2 | # +-------+-------+-------+ # | 3 | | | # | | 1 2 | | # | 8 6 | 7 6 | 5 | # +-------+-------+-------+ puzzle.solve().show_full() # --------------------------- # 9x9 (3x3) SUDOKU PUZZLE # Difficulty: INVALID PUZZLE (GIVEN PUZZLE HAS NO SOLUTION) # --------------------------- # +-------+-------+-------+ # | | | | # | | | | # | | | | # +-------+-------+-------+ # | | | | # | | | | # | | | | # +-------+-------+-------+ # | | | | # | | | | # | | | | # +-------+-------+-------+ ``` If you wish to raise an `UnsolvableSudoku` error when the board is invalid pass a `raising=True` parameter: ```py puzzle.solve(raising=True) ``` python-sudoku-2.0.0/main.py000066400000000000000000000005511504755367300156600ustar00rootroot00000000000000from sudoku import Sudoku board = [ [0,0,7,0,4,0,0,0,0], [0,0,0,0,0,8,0,0,6], [0,4,1,0,0,0,9,0,0], [0,0,0,0,0,0,1,7,0], [0,0,0,0,0,6,0,0,0], [0,0,8,7,0,0,2,0,0], [3,0,0,0,0,0,0,0,0], [0,0,0,1,2,0,0,0,0], [8,6,0,0,7,0,0,0,5] ] puzzle = Sudoku(3, board=board) print(puzzle) puzzle.solve().show_full() # puzzle.solve().display()python-sudoku-2.0.0/pyproject.toml000066400000000000000000000012211504755367300172710ustar00rootroot00000000000000[project] name = "py-sudoku" version = "2.0.0" requires-python = ">=2.7" authors = [ {name = "Jeff Sieu", email = "jeffsieu@gmail.com"}, ] description = "A simple Python package that generates and solves m x n Sudoku puzzles." readme = "README.md" license = {file = "LICENSE"} keywords = ["SUDOKU"] classifiers = [ "Programming Language :: Python :: 2", "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] [project.urls] Homepage = "https://github.com/jeffsieu/py-sudoku" Issues = "https://github.com/jeffsieu/py-sudoku/issues" GitHub = "https://github.com/jeffsieu/py-sudoku" python-sudoku-2.0.0/sudoku/000077500000000000000000000000001504755367300156735ustar00rootroot00000000000000python-sudoku-2.0.0/sudoku/__init__.py000066400000000000000000000001571504755367300200070ustar00rootroot00000000000000import sys if sys.version_info[0] < 3: from sudoku import Sudoku else: from sudoku.sudoku import Sudokupython-sudoku-2.0.0/sudoku/sudoku.py000066400000000000000000000730451504755367300175700ustar00rootroot00000000000000from random import shuffle, seed as random_seed, randrange import sys try: from typing import Iterable, List, Optional, Tuple, Union, cast, TYPE_CHECKING except ImportError: TYPE_CHECKING = False if not TYPE_CHECKING: # Stubs for Python 2 cast = lambda _, value: value # type: ignore class FakeList(): def __getitem__(self, key): return FakeList() List = FakeList() # type: ignore class UnsolvableSudoku(Exception): pass class _SudokuSolver: def __init__(self, sudoku): # type: (Sudoku) -> None self.width = sudoku.width self.height = sudoku.height self.size = sudoku.size self.sudoku = sudoku def _solve(self): # type: () -> Optional[Sudoku] blanks = self.__get_blanks() blank_count = len(blanks) are_blanks_filled = [False for _ in range(blank_count)] blank_fillers = self.__calculate_blank_cell_fillers(blanks) solution_board = self.__get_solution( Sudoku._copy_board(self.sudoku.board), blanks, blank_fillers, are_blanks_filled) solution_difficulty = 0 if not solution_board: return None return Sudoku(self.width, self.height, board=solution_board, difficulty=solution_difficulty) def _has_multiple_solutions(self): # type: () -> bool blanks = self.__get_blanks() blank_count = len(blanks) are_blanks_filled = [False for _ in range(blank_count)] blank_fillers = self.__calculate_blank_cell_fillers(blanks) solution_board = self.__get_solution( Sudoku._copy_board(self.sudoku.board), blanks, blank_fillers, are_blanks_filled) are_blanks_filled = [False for _ in range(blank_count)] blank_fillers = self.__calculate_blank_cell_fillers(blanks) solution_board2 = self.__get_solution( Sudoku._copy_board(self.sudoku.board), blanks, blank_fillers, are_blanks_filled, reverse=True) if not solution_board: return False return solution_board != solution_board2 def __calculate_blank_cell_fillers(self, blanks): # type: (List[Tuple[int, int]]) -> List[List[List[bool]]] sudoku = self.sudoku valid_fillers = [[[True for _ in range(self.size)] for _ in range( self.size)] for _ in range(self.size)] for row, col in blanks: for i in range(self.size): same_row = sudoku.board[row][i] same_col = sudoku.board[i][col] if same_row and i != col: valid_fillers[row][col][same_row - 1] = False if same_col and i != row: valid_fillers[row][col][same_col - 1] = False grid_row, grid_col = row // sudoku.height, col // sudoku.width grid_row_start = grid_row * sudoku.height grid_col_start = grid_col * sudoku.width for y_offset in range(sudoku.height): for x_offset in range(sudoku.width): if grid_row_start + y_offset == row and grid_col_start + x_offset == col: continue cell = sudoku.board[grid_row_start + y_offset][grid_col_start + x_offset] if cell: valid_fillers[row][col][cell - 1] = False return valid_fillers def __get_blanks(self): # type: () -> List[Tuple[int, int]] blanks = [] for i, row in enumerate(self.sudoku.board): for j, cell in enumerate(row): if cell == Sudoku._empty_cell_value: blanks += [(i, j)] return blanks def __is_neighbor(self, blank1, blank2): # type: (Tuple[int, int], Tuple[int, int]) -> bool row1, col1 = blank1 row2, col2 = blank2 if row1 == row2 or col1 == col2: return True grid_row1, grid_col1 = row1 // self.height, col1 // self.width grid_row2, grid_col2 = row2 // self.height, col2 // self.width return grid_row1 == grid_row2 and grid_col1 == grid_col2 # Optimized version of above def __get_solution(self, board, blanks, blank_fillers, are_blanks_filled, reverse=False): # type: (List[List[Optional[int]]], List[Tuple[int, int]], List[List[List[bool]]], List[bool], bool) -> Optional[List[List[int]]] min_filler_count = None chosen_blank = None for i, blank in enumerate(blanks): x, y = blank if are_blanks_filled[i]: continue valid_filler_count = sum(blank_fillers[x][y]) if valid_filler_count == 0: # Blank cannot be filled with any number, no solution return None if not min_filler_count or valid_filler_count < min_filler_count: min_filler_count = valid_filler_count chosen_blank = blank chosen_blank_index = i if not chosen_blank: # All blanks have been filled with valid values, return this board as the solution return cast(List[List[int]], board) row, col = chosen_blank # Declare chosen blank as filled are_blanks_filled[chosen_blank_index] = True # Save list of neighbors affected by the filling of current cell revert_list = [False for _ in range(len(blanks))] if reverse: foo = range(self.size - 1, -1, -1) else: foo = range(self.size) for number in foo: # Only try filling this cell with numbers its neighbors aren't already filled with if not blank_fillers[row][col][number]: continue # Test number in this cell, number + 1 is used because number is zero-indexed board[row][col] = number + 1 for i, blank in enumerate(blanks): blank_row, blank_col = blank if blank == chosen_blank: continue if self.__is_neighbor(blank, chosen_blank) and blank_fillers[blank_row][blank_col][number]: blank_fillers[blank_row][blank_col][number] = False revert_list[i] = True else: revert_list[i] = False solution_board = self.__get_solution( board, blanks, blank_fillers, are_blanks_filled, reverse=reverse) if solution_board: return solution_board # No solution found by having tested number in this cell # So we reallow neighbor cells to have this number filled in them for i, blank in enumerate(blanks): if revert_list[i]: blank_row, blank_col = blank blank_fillers[blank_row][blank_col][number] = True # If this point is reached, there is no solution with the initial board state, # a mistake must have been made in earlier steps # Declare chosen cell as empty once again are_blanks_filled[chosen_blank_index] = False board[row][col] = Sudoku._empty_cell_value return None # Optimized version of above class Sudoku: _empty_cell_value = None # type: None __difficulty = None # type: float def __init__(self, width = 3, height = None, board = None, difficulty = None, seed = randrange(sys.maxsize)): # type: (int, Optional[int], Optional[Iterable[Iterable[Optional[int]]]], Optional[float], int) -> None """ Initializes a Sudoku board :param width: Integer representing the width of the Sudoku grid. Defaults to 3. :param height: Optional integer representing the height of the Sudoku grid. If not provided, defaults to the value of `width`. :param board: Optional iterable for a the initial state of the Sudoku board. :param difficulty: Optional float representing the difficulty level of the Sudoku puzzle. If provided, sets the difficulty level based on the number of empty cells. Defaults to None. :param seed: Integer representing the seed for the random number generator used to generate the board. Defaults to a random seed within the system's maximum size. :raises AssertionError: If the width, height, or size of the board is invalid. """ self.width = width self.height = height if height else width self.size = self.width * self.height assert self.width > 0, 'Width cannot be less than 1' assert self.height > 0, 'Height cannot be less than 1' assert self.size > 1, 'Board size cannot be 1 x 1' if difficulty is not None: self.__difficulty = difficulty if board: blank_count = 0 self.board = [[cell for cell in row] for row in board] # type: List[List[Optional[int]]] for row in self.board: for i in range(len(row)): if not row[i] in range(1, self.size + 1): row[i] = Sudoku._empty_cell_value blank_count += 1 if difficulty == None: if self.validate(): self.__difficulty = blank_count / \ (self.size * self.size) else: self.__difficulty = -2 else: positions = list(range(self.size)) random_seed(seed) shuffle(positions) self.board = [[(i + 1) if i == positions[j] else Sudoku._empty_cell_value for i in range(self.size)] for j in range(self.size)] def solve(self, assert_solvable = False): # type: (bool) -> Sudoku """ Solves the given Sudoku board :param assert_solvable: Boolean for if you wish to raise an UnsolvableSodoku error when the board is invalid. Defaults to `false`. :raises UnsolvableSudoku: """ solution = _SudokuSolver(self)._solve() if self.validate() else None if solution: return solution elif assert_solvable: raise UnsolvableSudoku('No solution found') else: solution_board = Sudoku.empty(self.width, self.height).board solution_difficulty = -2 return Sudoku(board=solution_board, difficulty=solution_difficulty) def has_multiple_solutions(self): # type: () -> bool """ Returns if the Sudoku board has multiple solutions. Solves the Sudoku board via backtracking: - once by filling the cells with increasing numbers - once by filling the cells with decreasing numbers If the two solutions are different, the board has multiple solutions (and vice versa). """ return _SudokuSolver(self)._has_multiple_solutions() def validate(self): # type: () -> bool row_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] col_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] box_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] for row in range(self.size): for col in range(self.size): cell = self.board[row][col] box = (row // self.height) * self.height + (col // self.width) if cell == Sudoku._empty_cell_value: continue elif isinstance(cell, int): if row_numbers[row][cell - 1]: return False elif col_numbers[col][cell - 1]: return False elif box_numbers[box][cell - 1]: return False row_numbers[row][cell - 1] = True col_numbers[col][cell - 1] = True box_numbers[box][cell - 1] = True return True @ staticmethod def _copy_board(board): # type: (Iterable[Iterable[Optional[int]]]) -> List[List[Optional[int]]] return [[cell for cell in row] for row in board] @ staticmethod def empty(width, height): # type: (int, int) -> Sudoku size = width * height board = [[Sudoku._empty_cell_value] * size] * size return Sudoku(width, height, board, 0) def difficulty(self, difficulty): # type: (float) -> Sudoku """ Sets the difficulty of the Sudoku board by removing cells. This method modifies the current Sudoku instance by removing cells from the solved puzzle to achieve the desired difficulty level. The difficulty is specified as a float value between 0 and 1, where 0 represents the easiest puzzle (fully solved) and 1 represents the most difficult puzzle (almost empty). :param difficulty: A float value between 0 and 1 representing the desired difficulty level of the Sudoku puzzle. :return: A new Sudoku instance representing the puzzle with adjusted difficulty. :raises AssertionError: If the provided difficulty value is not within the range of 0 to 1. """ assert 0 < difficulty < 1, 'Difficulty must be between 0 and 1' indices = list(range(self.size * self.size)) shuffle(indices) problem_board = self.solve().board for index in indices[:int(difficulty * self.size * self.size)]: row_index = index // self.size col_index = index % self.size problem_board[row_index][col_index] = Sudoku._empty_cell_value # check for multiple solutions puzzle = Sudoku(self.width, self.height, problem_board, difficulty) if puzzle.has_multiple_solutions(): return Sudoku(self.width, self.height, problem_board, -3) return Sudoku(self.width, self.height, problem_board, difficulty) def get_difficulty(self): # type: () -> float return self.__difficulty def show(self): # type: () -> None """ Prints the puzzle to the terminal """ if self.__difficulty == -3: print('Puzzle has multiple solutions') elif self.__difficulty == -2: print('Puzzle has no solution') elif self.__difficulty == -1: print('Invalid puzzle. Please solve the puzzle (puzzle.solve()), or set a difficulty (puzzle.difficulty())') elif not self.board: print('No solution') else: print('Puzzle has exactly one solution') print(self.__format_board_ascii()) def show_full(self): # type: () -> None """ Prints the puzzle to the terminal, with more information """ print(self.__str__()) def __format_board_ascii(self): # type: () -> str table = '' cell_length = len(str(self.size)) format_int = '{0:0' + str(cell_length) + 'd}' for i, row in enumerate(self.board): if i == 0: table += ('+-' + '-' * (cell_length + 1) * self.width) * self.height + '+' + '\n' table += (('| ' + '{} ' * self.width) * self.height + '|').format(*[format_int.format( x) if x != Sudoku._empty_cell_value else ' ' * cell_length for x in row]) + '\n' if i == self.size - 1 or i % self.height == self.height - 1: table += ('+-' + '-' * (cell_length + 1) * self.width) * self.height + '+' + '\n' return table def __str__(self): # type: () -> str if self.__difficulty == -2: difficulty_str = 'INVALID PUZZLE (GIVEN PUZZLE HAS NO SOLUTION)' elif self.__difficulty == -1: difficulty_str = 'INVALID PUZZLE' elif self.__difficulty == -3: difficulty_str = 'INVALID PUZZLE (MULTIPLE SOLUTIONS)' elif self.__difficulty == 0: difficulty_str = 'SOLVED' else: difficulty_str = '{:.2f}'.format(self.__difficulty) return ''' --------------------------- {}x{} ({}x{}) SUDOKU PUZZLE Difficulty: {} --------------------------- {} '''.format(self.size, self.size, self.width, self.height, difficulty_str, self.__format_board_ascii()) class DiagonalSudoku(Sudoku): __difficulty = None # type: float def __init__(self, size = 3, board = None, difficulty = None, seed = randrange(sys.maxsize)): # type: (int, Optional[Iterable[Iterable[Optional[int]]]], Optional[float], int) -> None self.width = size self.height = size self.size = size * size self.diagonal_left_to_right = [(i, i) for i in range(self.size)] self.diagonal_right_to_left = [ (i, j) for i, j in enumerate(range(self.size-1, -1, -1))] assert self.width > 0, 'Width cannot be less than 1' assert self.height > 0, 'Height cannot be less than 1' assert self.size > 1, 'Board size cannot be 1 x 1' if difficulty is not None: self.__difficulty = difficulty if board: blank_count = 0 self.board = [[cell for cell in row] for row in board] # type: List[List[Union[int, None]]] for _row in self.board: for i in range(len(_row)): if _row[i] not in range(1, self.size + 1): _row[i] = Sudoku._empty_cell_value blank_count += 1 for row, col in self.diagonal_left_to_right: if self.board[row][col] not in range(1, self.size+1): self.board[row][col] = Sudoku._empty_cell_value blank_count += 1 for row, col in self.diagonal_right_to_left: if self.board[row][col] not in range(1, self.size+1): self.board[row][col] = Sudoku._empty_cell_value blank_count += 1 if difficulty == None: if self.validate(): self.__difficulty = blank_count / \ (self.size * self.size) else: self.__difficulty = -2 else: positions = list(range(1, self.size+1)) random_seed(seed) shuffle(positions) self.board = [[positions[j] if i == j else Sudoku._empty_cell_value for i in range( self.size)] for j in range(self.size)] def difficulty(self, difficulty): # type: (float) -> DiagonalSudoku assert 0 < difficulty < 1, 'Difficulty must be between 0 and 1' indices = list(range(self.size * self.size)) shuffle(indices) problem_board = self.solve().board for index in indices[:int(difficulty * self.size * self.size)]: row_index = index // self.size col_index = index % self.size problem_board[row_index][col_index] = Sudoku._empty_cell_value return DiagonalSudoku(self.width, problem_board, difficulty) def validate(self): # type: () -> bool row_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] col_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] box_numbers = [[False for _ in range(self.size)] for _ in range(self.size)] diagonal_numbers = [ [False for _ in range(self.size)] for _ in range(2)] for row in range(self.size): for col in range(self.size): cell = self.board[row][col] box = (row // self.height) * self.height + (col // self.width) if cell == Sudoku._empty_cell_value: continue elif isinstance(cell, int): if row_numbers[row][cell - 1] or col_numbers[col][cell - 1] or box_numbers[box][cell - 1]: return False row_numbers[row][cell - 1] = True col_numbers[col][cell - 1] = True box_numbers[box][cell - 1] = True for i in self.diagonal_left_to_right: cell = self.board[i[0]][i[1]] if cell == Sudoku._empty_cell_value: continue elif isinstance(cell, int): if diagonal_numbers[0][cell - 1]: return False diagonal_numbers[0][cell - 1] = True for i in self.diagonal_right_to_left: cell = self.board[i[0]][i[1]] if cell == Sudoku._empty_cell_value: continue elif isinstance(cell, int): if diagonal_numbers[1][cell - 1]: return False diagonal_numbers[1][cell - 1] = True return True def solve(self, raising = False): # type: (bool) -> DiagonalSudoku solution = _DiagonalSudokuSolver( self)._solve() if self.validate() else None if solution: return solution elif raising: raise UnsolvableSudoku('No solution found') else: solution_board = DiagonalSudoku.empty( self.width, self.height).board solution_difficulty = -2 return DiagonalSudoku(board=solution_board, difficulty=solution_difficulty) def show(self): # type: () -> None if self.__difficulty == -2: print('Puzzle has no solution') if self.__difficulty == -1: print('Invalid puzzle. Please solve the puzzle (puzzle.solve()), or set a difficulty (puzzle.difficulty())') if not self.board: print('No solution') print(self.__format_board_ascii()) def show_full(self): # type: () -> None print(self.__str__()) def __format_board_ascii(self): # type: () -> str table = '' cell_length = len(str(self.size)) row_square = [] format_int = '{0:0' + str(cell_length) + 'd}' for i, row in enumerate(self.board): if i == 0: table += ('+-' + '-' * (cell_length + 1) * self.width) * self.height + '+' + '\n' for x in range(len(row)): if x != Sudoku._empty_cell_value: if i == x: row_square.append("\033[1m\033[4m{}\033[0m".format( format_int.format(row[x]))) elif self.diagonal_right_to_left[i][1] == x: row_square.append("\033[1m\033[4m{}\033[0m".format( format_int.format(row[x]))) else: row_square.append(format_int.format(row[x])) else: row_square.append(' ' * cell_length) table += (('| ' + '{} ' * self.width) * self.height + '|').format(*row_square) + '\n' row_square = [] if i == self.size - 1 or i % self.height == self.height - 1: table += ('+-' + '-' * (cell_length + 1) * self.width) * self.height + '+' + '\n' return table def __str__(self): # type: () -> str if self.__difficulty == -2: difficulty_str = 'INVALID PUZZLE (GIVEN PUZZLE HAS NO SOLUTION)' elif self.__difficulty == -1: difficulty_str = 'INVALID PUZZLE' elif self.__difficulty == 0: difficulty_str = 'SOLVED' else: difficulty_str = '{:.2f}'.format(self.__difficulty) return ''' ------------------------------------ {}x{} ({}x{}) DIAGONAL SUDOKU PUZZLE Difficulty: {} ------------------------------------ {} '''.format(self.size, self.size, self.width, self.height, difficulty_str, self.__format_board_ascii()) class _DiagonalSudokuSolver(_SudokuSolver): def __init__(self, sudoku): # type: (DiagonalSudoku) -> None super().__init__(sudoku) self.diagonal_left_to_right = [(i, i) for i in range(self.size)] self.diagonal_right_to_left = [ (i, j) for i, j in enumerate(range(self.size-1, -1, -1))] def _solve(self): # type: () -> Optional[DiagonalSudoku] blanks = self.__get_blanks() blank_count = len(blanks) are_blanks_filled = [False for _ in range(blank_count)] blank_fillers = self.__calculate_blank_cell_fillers(blanks) solution_board = self.__get_solution( DiagonalSudoku._copy_board(self.sudoku.board), blanks, blank_fillers, are_blanks_filled) solution_difficulty = 0 if not solution_board: return None return DiagonalSudoku(self.width, board=solution_board, difficulty=solution_difficulty) def __get_blanks(self): # type: () -> List[Tuple[int, int]] blanks = [] for i, row in enumerate(self.sudoku.board): for j, cell in enumerate(row): if cell == Sudoku._empty_cell_value: blanks += [(i, j)] return blanks def __is_neighbor(self, blank1, blank2): # type: (Tuple[int, int], Tuple[int, int]) -> bool """ The function checks whether the cells are neighbors. Checks whether they are in one row, in one column, in one square whose dimensions are `self.width` and in the same diagonal. """ row1, col1 = blank1 row2, col2 = blank2 if row1 == row2 or col1 == col2: return True grid_row1, grid_col1 = row1 // self.height, col1 // self.width grid_row2, grid_col2 = row2 // self.height, col2 // self.width if grid_row1 == grid_row2 and grid_col1 == grid_col2: return True if blank1 in self.diagonal_left_to_right and blank2 in self.diagonal_left_to_right: return True return blank1 in self.diagonal_right_to_left and blank2 in self.diagonal_right_to_left def __calculate_blank_cell_fillers(self, blanks): # type: (List[Tuple[int, int]]) -> List[List[List[bool]]] sudoku = self.sudoku valid_fillers = [[[True for _ in range(self.size)] for _ in range( self.size)] for _ in range(self.size)] for row, col in blanks: for i in range(self.size): same_row = sudoku.board[row][i] same_col = sudoku.board[i][col] if same_row and i != col: valid_fillers[row][col][same_row - 1] = False if same_col and i != row: valid_fillers[row][col][same_col - 1] = False grid_row, grid_col = row // sudoku.height, col // sudoku.width grid_row_start = grid_row * sudoku.height grid_col_start = grid_col * sudoku.width for y_offset in range(sudoku.height): for x_offset in range(sudoku.width): if grid_row_start + y_offset == row and grid_col_start + x_offset == col: continue cell = sudoku.board[grid_row_start + y_offset][grid_col_start + x_offset] if cell: valid_fillers[row][col][cell - 1] = False if (row, col) in self.diagonal_left_to_right: for j in self.diagonal_left_to_right: same_diagonal = sudoku.board[row][col] if j == (row, col) or not same_diagonal: continue valid_fillers[row][col][same_diagonal - 1] = False elif (row, col) in self.diagonal_right_to_left: for j in self.diagonal_right_to_left: same_diagonal = sudoku.board[j[0]][j[1]] if j == (row, col) or not same_diagonal: continue valid_fillers[row][col][same_diagonal - 1] = False return valid_fillers def __get_solution(self, board, blanks, blank_fillers, are_blanks_filled): # type: (List[List[Optional[int]]], List[Tuple[int, int]], List[List[List[bool]]], List[bool]) -> Optional[List[List[int]]] min_filler_count = None chosen_blank = None for i, blank in enumerate(blanks): x, y = blank if are_blanks_filled[i]: continue valid_filler_count = sum(blank_fillers[x][y]) if valid_filler_count == 0: return None if not min_filler_count or valid_filler_count < min_filler_count: min_filler_count = valid_filler_count chosen_blank = blank chosen_blank_index = i if not chosen_blank: return cast(List[List[int]], board) row, col = chosen_blank are_blanks_filled[chosen_blank_index] = True revert_list = [False for _ in range(len(blanks))] for number in range(self.size): if not blank_fillers[row][col][number]: continue board[row][col] = number + 1 for i, blank in enumerate(blanks): blank_row, blank_col = blank if blank == chosen_blank: continue if self.__is_neighbor(blank, chosen_blank) and blank_fillers[blank_row][blank_col][number]: blank_fillers[blank_row][blank_col][number] = False revert_list[i] = True else: revert_list[i] = False solution_board = self.__get_solution( board, blanks, blank_fillers, are_blanks_filled) if solution_board: return solution_board for i, blank in enumerate(blanks): if revert_list[i]: blank_row, blank_col = blank blank_fillers[blank_row][blank_col][number] = True are_blanks_filled[chosen_blank_index] = False board[row][col] = Sudoku._empty_cell_value return None