Advertisement
Guest User

sudoku.py

a guest
Feb 16th, 2019
157
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 18.28 KB | None | 0 0
  1. # !/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3.  
  4. """
  5. I know this is a common work, but I was inspired to try implement my
  6. own way after perusing some algorithm and data structures pages I felt
  7. like trying this.
  8. Furthermore it's going to be added to the collection of 2D games that
  9. I am currently working on in pygame.
  10. .
  11. """
  12.  
  13. __author__ = "</Dan Rhamba>"
  14. __copyright__ = "Copyright 2019, The Beyond Knowledge Project"
  15. __credits__ = ["The Most High"]
  16. __licence__ = "Dan Rhamba Beyond Knowledge Foundation"
  17. __version__ = "1.0.1"
  18. __maintainer__ = "</Dan Rhamba>"
  19. __email__ = "dnlramba9@gmail.com"
  20. __status__ = "Learning"
  21.  
  22. import os as _os
  23. import random as _random
  24. import time as _time
  25. from copy import deepcopy as _deepcopy
  26.  
  27. #import joblib as _joblib
  28.  
  29.  
  30. class SudokuError(Exception):
  31.     """Raise exceptions for various sudoku rules where necessary"""
  32.     pass
  33.  
  34.  
  35. class Sudoku:
  36.     """Main sudoku class. Employs two backtracking techniques to solve or create sudoku i.e
  37.    a greedy bruteforce approach and an intelligent one. Most methods are static. Just call them without
  38.    instantiating it if all you only need puzzles or solved puzzles. This class is intended to be
  39.    used as a module so it defines additional utilities for that purpose."""
  40.  
  41.     def __init__(self, grid: list = None):
  42.         """grid is a list containing 9 lists filled with values from 0 - 9 and should
  43.        pass sudoku validity test."""
  44.  
  45.         if grid is None:
  46.             self.grid = [[0 for i in range(9)] for j in range(9)]
  47.         else:
  48.             if not self.is_9_by_9(grid):
  49.                 raise SudokuError('Can only take a 9x9 sudoku puzzle')
  50.  
  51.             self.grid = _deepcopy(grid)
  52.             if not self.is_valid():
  53.                 raise SudokuError('Your passed puzzle is in valid')
  54.             self.grid_copy = _deepcopy(grid)
  55.         self.grid_copy = _deepcopy(self.grid)
  56.  
  57.     def __str__(self):
  58.         s = ''
  59.         for row in self.grid:
  60.             s += str(row) + '\n'
  61.         return s
  62.  
  63.     def __eq__(self, other):
  64.         if not isinstance(other, Sudoku):
  65.             raise SudokuError('Cannot compare sudoku object to None sudoku object')
  66.         for i in range(9):
  67.             for j in range(9):
  68.                 if self.grid[i][j] != other.grid[i][j]:
  69.                     return False
  70.         return True
  71.  
  72.     def __hash__(self):
  73.         return hash(str(self.grid))
  74.  
  75.     @staticmethod
  76.     def is_9_by_9(grid: list) -> bool:
  77.         """checks whether the passed sudoku puzzle/grid is 9x9. If so, it returns
  78.        true otherwise false."""
  79.         if len(grid) != 9:
  80.             return False
  81.         for row in grid:
  82.             if len(row) != 9:
  83.                 return False
  84.         return True
  85.  
  86.     @staticmethod
  87.     def get_indices(index: int) -> list:
  88.         """returns the list of indices that the passed index ranges in a block"""
  89.         if 0 < index > 8:
  90.             raise SudokuError('Invalid column/row number passed.')
  91.         if index < 3:
  92.             return list(range(0, 3))
  93.         elif index < 6:
  94.             return list(range(3, 6))
  95.         else:
  96.             return list(range(6, 9))
  97.  
  98.     @staticmethod
  99.     def remove_zeros(values: list):
  100.         """removes zeros in passed list."""
  101.         while 0 in values:
  102.             values.remove(0)
  103.  
  104.     def get_block_values(self, cell: tuple, remove_0: bool = True) -> list:
  105.         """returns members of a block given the cell.
  106.        It will then remove 0 valued cells if remove_0 is true otherwise it will keep them
  107.        Note: Not passing the grid returns the values of the grid initialized with the Sudoku class."""
  108.  
  109.         self.validate_cell(cell)
  110.         i, j = cell
  111.         r = self.get_indices(i)
  112.         c = self.get_indices(j)
  113.         block_values = []
  114.         for i in r:
  115.             for j in c:
  116.                 block_values.append(self.grid[i][j])
  117.         if remove_0:
  118.             self.remove_zeros(block_values)
  119.         return block_values
  120.  
  121.     def get_row_values(self, i: int, remove_0: bool = True) -> list:
  122.         """returns values of the given row i.
  123.        It will then remove 0 valued cells if remove_0 is true otherwise it will keep them.
  124.        Note: Not passing the grid returns the values of the grid initialized with the Sudoku class."""
  125.         if 0 < i > 8:
  126.             raise SudokuError('Invalid column number passed.')
  127.  
  128.         row_values = self.grid[i][:]  # Note the copying
  129.         if remove_0:
  130.             self.remove_zeros(row_values)
  131.         return row_values
  132.  
  133.     def get_column_values(self, j: int, remove_0: bool = True) -> list:
  134.         """return values of column j.
  135.        It will then remove 0 valued cell if remove_0 is true otherwise it will keep them.
  136.        Note: Not passing the grid returns the values of the grid initialized with the Sudoku class."""
  137.         if 0 < j > 8:
  138.             raise SudokuError('Invalid column number passed.')
  139.  
  140.         column_values = []
  141.         for row in self.grid:
  142.             column_values.append(row[j])
  143.         if remove_0:
  144.             self.remove_zeros(column_values)
  145.         return column_values
  146.  
  147.     def get_cell_members(self, cell: tuple, remove_0: bool = True) -> list:
  148.         """returns numbers already filled according to sudoku rules.
  149.        It will then remove 0 valued cell if remove_0 is true otherwise it will keep them."""
  150.  
  151.         self.validate_cell(cell)
  152.         i, j = cell
  153.         block_members = self.get_block_values(cell, remove_0=remove_0)
  154.         row_members = self.get_row_values(i, remove_0=remove_0)
  155.         column_members = self.get_column_values(j, remove_0=remove_0)
  156.         all_members = block_members + row_members + column_members
  157.         if remove_0:
  158.             while 0 in all_members:
  159.                 all_members.remove(0)
  160.  
  161.         return all_members
  162.  
  163.     @staticmethod
  164.     def validate_cell(cell: tuple, value: int = None):
  165.         """returns true if the passed cell is a valid one, false otherwise.
  166.        cell values should be in range 0-8. If value is passed then it should in range 0-9"""
  167.         i, j = cell
  168.         check = [i, j, value] if value else [i, j]
  169.         for integer in check:
  170.             if 0 > integer > 9:
  171.                 raise SudokuError('Invalid value or cell passed')
  172.  
  173.     def assign(self, cell: tuple, value: int):
  174.         """assigns the value passed to the passed cell. Accepted values range from 0-9"""
  175.         i, j = cell
  176.         self.grid[i][j] = value
  177.  
  178.     def get_value(self, cell: tuple) -> int:
  179.         """returns value in the passed cell."""
  180.         i, j = cell
  181.         check = [i, j]
  182.         for integer in check:
  183.             if 0 > integer > 8:
  184.                 raise SudokuError('Invalid cell passed')
  185.         return self.grid[i][j]
  186.  
  187.     def accepts(self, cell: tuple, value: int) -> bool:
  188.         """return true if the passed value can be accepted by the passed cell otherwise false."""
  189.         cell_members = self.get_cell_members(cell, remove_0=True)
  190.         return value not in cell_members
  191.  
  192.     def accepted_values(self, cell: tuple) -> list:
  193.         """returns the compliment of members i.e values that are not yet filled/not members
  194.        of the passed cell."""
  195.         values = set(range(1, 10))
  196.         cell_members = set(self.get_cell_members(cell, remove_0=True))
  197.         return list(values - cell_members)
  198.  
  199.     def is_occupied(self, cell: tuple) -> bool:
  200.         """returns true if the passed cell is already taken i.e not 0 otherwise false."""
  201.         i, j = cell
  202.         return self.grid[i][j] > 0
  203.  
  204.     def is_valid(self) -> bool:
  205.         """returns true if our sudoku is valid so far.
  206.        Note: checks even if the grid ain't full i.e solved."""
  207.         for i in range(9):
  208.             for j in range(9):
  209.                 cell = i, j
  210.                 block_members = self.get_block_values(cell)
  211.                 # if they aren't equal then there must be a repeated value
  212.                 if len(block_members) != len(set(block_members)):
  213.                     return False
  214.                 if i == 0:
  215.                     # only need to check once
  216.                     column_members = self.get_column_values(j)
  217.                     # if they aren't equal then there must be a repeated value
  218.                     if len(column_members) != len(set(column_members)):
  219.                         return False
  220.             row_members = self.get_row_values(i)
  221.             # if they aren't equal then there must be a repeated value
  222.             if len(row_members) != len(set(row_members)):
  223.                 return False
  224.         return True
  225.  
  226.     def is_complete(self) -> bool:
  227.         """checks if our grid is complete returning true, otherwise false."""
  228.         is_valid = self.is_valid()
  229.         if not is_valid:
  230.             raise SudokuError('Cannot check for invalid sudoku')
  231.         for row in self.grid:
  232.             if 0 in row:
  233.                 return False
  234.         return True
  235.  
  236.     def refresh_grid(self):
  237.         self.grid = _deepcopy(self.grid_copy)
  238.  
  239.     def occupied_cells(self) -> list:
  240.         """return cells that are not zero valued in a list"""
  241.         occupied = []
  242.         for i in range(9):
  243.             for j in range(9):
  244.                 if self.is_occupied((i, j)):
  245.                     occupied.append((i, j))
  246.         return occupied
  247.  
  248.     def empty_cells(self) -> list:
  249.         """return cells that are zero valued in a list"""
  250.         not_occupied_cells = []
  251.         for i in range(9):
  252.             for j in range(9):
  253.                 if not self.is_occupied((i, j)):
  254.                     not_occupied_cells.append((i, j))
  255.         return not_occupied_cells
  256.  
  257.     def empty_cells_count(self) -> int:
  258.         """returns the number of unfilled cells."""
  259.         count = 0
  260.         for row in self.grid:
  261.             count += row.count(0)
  262.         return count
  263.  
  264.     def minimum_accepted(self) -> tuple:
  265.         """returns the cell with the minimum accepted values and the number of the accepted values in a tuple.
  266.        Simply to see if we can improve efficiency of greedy brute forcing."""
  267.         # get the first empty_cell
  268.         cell = None
  269.         for i in range(9):
  270.             for j in range(9):
  271.                 if not self.is_occupied((i, j)):
  272.                     cell = i, j
  273.  
  274.         if cell is None:
  275.             raise SudokuError('You should not use this method on a solved sudoku puzzle')
  276.  
  277.         n = len(self.accepted_values(cell))
  278.  
  279.         for i in range(9):
  280.             for j in range(9):
  281.                 if self.is_occupied((i, j)):
  282.                     continue
  283.                 m = len(self.accepted_values((i, j)))
  284.                 # we do need 0 or any filled cell
  285.                 if n > m > 0:
  286.                     n = m
  287.                     cell = i, j
  288.         return cell, n
  289.  
  290.     def intelligent_algo(self):
  291.         """Tries to apply some reasoning  and tricks to minimise _time complexity
  292.         experienced in greedy brute forcingtechnique"""
  293.         count = 0  # will see how many times we refreshed the grid
  294.         while not self.is_complete():
  295.             cell = self.minimum_accepted()[0]
  296.             values = self.accepted_values(cell)
  297.             try:
  298.                 choice = _random.choice(values)
  299.                 self.assign(cell, choice)
  300.             except (ValueError, IndexError):
  301.                 # cannot be solved so we refresh
  302.                 self.refresh_grid()
  303.             count += 1
  304.         return count
  305.  
  306.     def greedy_brute_force(self):
  307.         """applies back tracking algorithm but with brute_force approach to
  308.         fill the cells with accepted values.
  309.        Note it is an inplace method but return count for analysis purposes as
  310.         with intelligent_algo too."""
  311.         count = 0
  312.         while not self.is_complete():
  313.  
  314.             for i in range(9):
  315.                 for j in range(9):
  316.                     cell = i, j
  317.                     accepted_values = self.accepted_values(cell)
  318.                     if len(accepted_values) == 0 and not self.is_occupied(cell):
  319.                         self.refresh_grid()
  320.                     try:
  321.                         choice = _random.choice(accepted_values)
  322.                         if not self.is_occupied((i, j)):
  323.                             self.assign(cell, choice)
  324.                         else:
  325.                             continue
  326.  
  327.                     except (IndexError, ValueError):
  328.                         pass
  329.  
  330.             count += 1
  331.  
  332.         return count
  333.  
  334.     def generate_complete_sudoku(self) -> list:
  335.         """returns list of lists of complete sudoku values."""
  336.         sudoku = Sudoku()
  337.         sudoku.intelligent_algo()
  338.         return sudoku.grid
  339.  
  340.     @staticmethod
  341.     def generate_puzzle(empty_cells: int = 50, from_file=False, filename=None) -> list:
  342.         """generate sudoku puzzle. Unfilled cells will have zeros and will equal
  343.        empty_cells. Tries to first search in file if matching puzzle can be found
  344.        before generating from scratch if from_file is true.
  345.        Note: generating from file creates file in the current working directory, not
  346.        in this module directory even if your match won't be found.."""
  347.         puzzle = _Puzzle(empty_cells=empty_cells)
  348.         if from_file:
  349.             return puzzle.get_from_file(empty_cells=empty_cells, filename=filename).grid
  350.         else:
  351.             return puzzle.create_puzzle(empty_cells=empty_cells)
  352.  
  353.     @staticmethod
  354.     def solve_puzzle(puzzle: list) -> list:
  355.         """solves the passed puzzle and returns it. raises SudokuError if the
  356.        passed puzzle is invalid"""
  357.         sudoku = Sudoku(grid=puzzle)
  358.         sudoku.intelligent_algo()
  359.         return sudoku.grid
  360.  
  361.     @staticmethod
  362.     def display(sdk):
  363.         """prints the passed sudoku object or grid. You can pass sudoku object
  364.        or grid in a list of lists."""
  365.         if isinstance(sdk, Sudoku):
  366.             print(sdk)
  367.         elif isinstance(sdk, list):
  368.             for row in sdk:
  369.                 print(row)
  370.  
  371.  
  372. class _Puzzle:
  373.     """Employs Sudoku class capabilities to generate unique sudoku puzzles.
  374.    Stores the newly generated ones in a dict object as sudoku class. You
  375.    can either choose to generate new ones or use the already generated if
  376.    either fits your needs. The latter is faster."""
  377.  
  378.     def __init__(self, empty_cells: int = 50):
  379.         """empty cells is the number of cells with zero values to be solved."""
  380.         self.puzzle = self.create_puzzle(empty_cells=empty_cells)
  381.  
  382.     def __str__(self):
  383.         s = ''
  384.         for row in self.puzzle:
  385.             s += str(row) + '\n'
  386.         return s
  387.  
  388.     def __eq__(self, other):
  389.         if not isinstance(other, _Puzzle):
  390.             raise SudokuError('Cannot compare puzzle object to None puzzle object')
  391.         for i in range(9):
  392.             for j in range(9):
  393.                 if self.puzzle[i][j] != other.puzzle[i][j]:
  394.                     return False
  395.         return True
  396.  
  397.     def __hash__(self):
  398.         return hash(str(self.puzzle))
  399.  
  400.     @staticmethod
  401.     def create_puzzle(empty_cells: int = 50) -> list:
  402.         """creates a sudoku puzzle"""
  403.         sudoku = Sudoku()
  404.         sudoku.intelligent_algo()
  405.         count = 1
  406.         while sudoku.empty_cells_count() < empty_cells and count < 100:
  407.             occupied = sudoku.occupied_cells()
  408.             cell = _random.choice(occupied)
  409.  
  410.             if not sudoku.is_occupied(cell):
  411.                 count += 1
  412.                 continue
  413.             temp = sudoku.get_value(cell)
  414.             sudoku.assign(cell, 0)
  415.  
  416.             # back track to check for uniqueness
  417.             # create a copy of grid so far
  418.             copy_grid = _deepcopy(sudoku.grid)
  419.             sudoku_test = Sudoku(grid=copy_grid)
  420.             sudoku_test.intelligent_algo()
  421.             # if n times the solved sudoku is equal then it has to be unique
  422.             for k in range(5):
  423.                 sudoku_test2 = Sudoku(grid=copy_grid)
  424.                 # if it takes longer to be solved then it has to be unique
  425.                 start = _time.time()
  426.                 sudoku_test2.intelligent_algo()
  427.                 stop = _time.time()
  428.                 if stop - start > 20:
  429.                     return sudoku.grid
  430.                 if sudoku_test != sudoku_test2:
  431.                     # replace back the last modified cell
  432.                     sudoku.assign(cell, temp)
  433.                     break
  434.             count += 1
  435.         # storing the puzzle before return
  436.         #_Puzzle.store_in_file(sudoku)
  437.         return sudoku.grid
  438.  
  439.     @staticmethod
  440.     def store_in_file(sudoku: Sudoku, filename: str = None, capacity: int = 10000):
  441.         """stores the passed sudoku in file. To avoid excessive memory usage we set a limit
  442.        of stored sudoku with capacity."""
  443.         if filename is None:
  444.             filename = _os.path.join('res', 'sudoku_puzzles')
  445.         try:
  446.             stored_puzzles = _joblib.load(filename)
  447.             key = len(stored_puzzles) + 1
  448.             stored_puzzles[key] = sudoku
  449.             if len(stored_puzzles) < capacity:
  450.                 _joblib.dump(stored_puzzles, filename)
  451.         except FileNotFoundError:
  452.             _joblib.dump({1: sudoku}, filename)
  453.  
  454.     @staticmethod
  455.     def get_from_file(empty_cells: int = 50, filename: str = None) -> Sudoku:
  456.         """returns Sudoku object from stored from file.
  457.        If empty cells is None, it will return randomly otherwise the one matching the case.
  458.        If no matching case is found or FileNotFoundError is thrown it will generate a new one,
  459.         may take longer in that case."""
  460.         if 17 < empty_cells > 81:
  461.             raise SudokuError('Invalid empty cells. Should be integer between 17 and 81')
  462.         if filename is None:
  463.             filename = _os.path.join('res', 'sudoku_puzzles')
  464.         try:
  465.             stored_puzzles = _joblib.load(filename)
  466.             values = list(stored_puzzles.values())
  467.             _random.shuffle(values)
  468.             for sudoku in values:
  469.                 if sudoku.empty_cells_count() == empty_cells:
  470.                     return sudoku
  471.         except FileNotFoundError:
  472.             pass
  473.  
  474.         grid = _Puzzle.create_puzzle(empty_cells=empty_cells)
  475.         sudoku = Sudoku(grid=grid)
  476.         return sudoku
  477.  
  478.  
  479. def main():
  480.     puzzle = Sudoku.generate_puzzle()
  481.     solved_puzzle = Sudoku.solve_puzzle(empty_cells=30)
  482.     print(puzzle)
  483.     print()
  484.     print(solved_puzzle)
  485.    
  486.    
  487. if __name__ == '__main__':
  488.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement