Part B#

General introduction to part B

Part B concerns Sudoku puzzles. Your task will be to write a class Sudoku . Each task B1-B8 will be to implement some part of the class. You do not need to submit your answers separately. Instead, please submit the entire class in the field for the last task (task B8).

A Sudoku puzzle might look like the left figure below. The objective is to fill all fields in the 9 x 9 grid with digits so that each row, each column, and each of the nine 3 x 3 blocks contain all of the digits from 1 to 9. The right figure below shows the solution to the puzzle.

Sudoku puzzle Sudoku puzzle

Source: Wikipedia

In all tasks that follow, the sudoku grid will be represented by a list of lists. The inner lists each correspond to a row and their elements are the digits currently on the grid. Empty fields are represented by the digit 0.

Example: The (unsolved) puzzle in the picture above would be represented by the list picture_grid:

# List-of-lists representation of the unsolved Sudoku puzzle above 
picture_grid = [[5, 3, 0, 0, 7, 0, 0, 0, 0],
                [6, 0, 0, 1, 9, 5, 0, 0, 0],
                [0, 9, 8, 0, 0, 0, 0, 6, 0],
                [8, 0, 0, 0, 6, 0, 0, 0, 3],
                [4, 0, 0, 8, 0, 3, 0, 0, 1],
                [7, 0, 0, 0, 2, 0, 0, 0, 6],
                [0, 6, 0, 0, 0, 0, 2, 8, 0],
                [0, 0, 0, 4, 1, 9, 0, 0, 5],
                [0, 0, 0, 0, 8, 0, 0, 7, 9]]

B1 (1 point)#

Create a class Sudoku with a constructor that takes a parameter grid and stores its value in an attribute grid.

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
class Sudoku:
    def __init__(self, grid):
        self.grid = grid

B2 (2 points)#

Add another method in the Sudoku class so that Sudoku objects are printed in a readable way.

The following code:

picture_grid = [[5, 3, 0, 0, 7, 0, 0, 0, 0],
                [6, 0, 0, 1, 9, 5, 0, 0, 0],
                [0, 9, 8, 0, 0, 0, 0, 6, 0],
                [8, 0, 0, 0, 6, 0, 0, 0, 3],
                [4, 0, 0, 8, 0, 3, 0, 0, 1],
                [7, 0, 0, 0, 2, 0, 0, 0, 6],
                [0, 6, 0, 0, 0, 0, 2, 8, 0],
                [0, 0, 0, 4, 1, 9, 0, 0, 5],
                [0, 0, 0, 0, 8, 0, 0, 7, 9]]
my_sudoku = Sudoku(picture_grid)
print(my_sudoku)

should produce the printout

5 3 0 0 7 0 0 0 0
6 0 0 1 9 5 0 0 0
0 9 8 0 0 0 0 6 0
8 0 0 0 6 0 0 0 3
4 0 0 8 0 3 0 0 1
7 0 0 0 2 0 0 0 6
0 6 0 0 0 0 2 8 0
0 0 0 4 1 9 0 0 5
0 0 0 0 8 0 0 7 9

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def __str__(self):
        res = '\n'
        for row in self.grid:
            row_str = ' '.join([str(x) for x in row])
            res += row_str + '\n'
        return res

B3 (1 point)#

Complete the method get_column_as_list() and add it to your class.

    def get_column_as_list(self, column_index):
        """Return a list of all entries in the input column. 
        """

With picture_grid defined as in task B2, the following code

my_sudoku = Sudoku(picture_grid)
print(my_sudoku.get_column_as_list(0))
print(my_sudoku.get_column_as_list(4))
print(my_sudoku.get_column_as_list(8))

should produce the printout

[5, 6, 0, 8, 4, 7, 0, 0, 0]
[7, 9, 0, 6, 0, 2, 0, 1, 8]
[0, 0, 0, 3, 1, 6, 0, 5, 9]

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def get_column_as_list(self, column_index):
        """Return a list of all entries in the input column. 
        """
        return [row[column_index] for row in self.grid]

B4 (3 points)#

Complete the method get_block_as_list() and add it to your class.

    def get_block_as_list(self, row_index, column_index):
        """Return a list of all entries in the 3x3 block
        that contains the field (row_index, column_index).
 
        Examples:
        --------
 
        The following calls ALL return the same thing:
        a list of all entries in the top-left block.
        sudoku_object.get_block_as_list(0, 0)
        sudoku_object.get_block_as_list(0, 1)
        sudoku_object.get_block_as_list(0, 2)
        sudoku_object.get_block_as_list(1, 0)
        sudoku_object.get_block_as_list(1, 1)
        sudoku_object.get_block_as_list(1, 2)
        sudoku_object.get_block_as_list(2, 0)
        sudoku_object.get_block_as_list(2, 1)
        sudoku_object.get_block_as_list(2, 2)
 
        The following calls ALL return the same thing:
        a list of all entries in the top-middle block.
        sudoku_object.get_block_as_list(0, 3)
        sudoku_object.get_block_as_list(0, 4)
        sudoku_object.get_block_as_list(0, 5)
        sudoku_object.get_block_as_list(1, 3)
        sudoku_object.get_block_as_list(1, 4)
        sudoku_object.get_block_as_list(1, 5)
        sudoku_object.get_block_as_list(2, 3)
        sudoku_object.get_block_as_list(2, 4)
        sudoku_object.get_block_as_list(2, 5)
 
        etc.
 
        """

With picture_grid defined as in task B2, the following code

my_sudoku = Sudoku(picture_grid)
print(my_sudoku.get_block_as_list(0, 2))
print(my_sudoku.get_block_as_list(4, 8))
print(my_sudoku.get_block_as_list(7, 1)) 

should produce the printout

[5, 3, 0, 6, 0, 0, 0, 9, 8]
[0, 0, 3, 0, 0, 1, 0, 0, 6]
[0, 6, 0, 0, 0, 0, 0, 0, 0]

Note, however, that the order of the elements in the output list is not important! Your method may order the elements of the output list in any way. Therefore, you do not need to match the example printout element by element.

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def get_block_as_list(self, row_index, column_index):
        """Return a list of all entries in the 3x3 block
        that contains the field (row_index, column_index).
        """

        # Find indices for top-left field in the block
        row_index_top_left = (row_index//3)*3
        col_index_top_left = (column_index//3)*3
        
        block_list = []
        for i in range(row_index_top_left, row_index_top_left + 3):
            for j in range(col_index_top_left, col_index_top_left + 3):
                block_list.append(self.grid[i][j])
        return block_list

B5 (4 points)#

Complete the methods all_rows_complete(), all_columns_complete(), and all_blocks_complete() and add them to your class.

    def all_rows_complete(self):
        """Check whether all rows contain the digits 1 through 9"""
        return True #REPLACE THIS
  
    def all_columns_complete(self):
        """Check whether all columns contain the digits 1 through 9"""
        return True #REPLACE THIS
 
    def all_blocks_complete(self):
        """Check whether all blocks contain the digits 1 through 9"""
        return True #REPLACE THIS

The method all_rows_complete() should return True if all rows in the grid contain the digits 1 to 9, and False otherwise. The methods all_columns_complete() and all_blocks_complete() should work similarly, for columns and blocks.

NOTE: You may use methods from B1-B4 to solve this problem even if you did not implement them. You may assume that the methods exist and are working as they should.

If you wish, you may use the following function to check that your methods are working correctly:

function for testing
def test_solution_checks():
    solved_sudoku_grid = [[4, 5, 3, 8, 2, 6, 1, 9, 7],
                            [8, 9, 2, 5, 7, 1, 6, 3, 4],
                            [1, 6, 7, 4, 9, 3, 5, 2, 8],
                            [7, 1, 4, 9, 5, 2, 8, 6, 3],
                            [5, 8, 6, 1, 3, 7, 2, 4, 9],
                            [3, 2, 9, 6, 8, 4, 7, 5, 1],
                            [9, 3, 5, 2, 1, 8, 4, 7, 6],
                            [6, 7, 1, 3, 4, 5, 9, 8, 2],
                            [2, 4, 8, 7, 6, 9, 3, 1, 5],
                            ]
    # Apply tests to a correctly completed sudoku
    print('\n----------------------------------------------')
    print('Running tests on a correctly solved sudoku...')
    print('Incorrect methods will generate output below.')
    print('No output indicates that your code passed the tests.')
    solved_sudoku = Sudoku(solved_sudoku_grid)
    rows_complete = solved_sudoku.all_rows_complete()
    columns_complete = solved_sudoku.all_columns_complete()
    blocks_complete = solved_sudoku.all_blocks_complete()
 
    if not rows_complete:
        print('TEST FAILED! all_rows_complete flagged a correctly solved sudoku as incorrect.')
    if not columns_complete:
        print('TEST FAILED! all_columns_complete flagged a correctly solved sudoku as incorrect.')
    if not blocks_complete:
        print('TEST FAILED! all_blocks_complete flagged a correctly solved sudoku as incorrect.')    
  
    # Apply tests to incorrect sudokus
    print('\n----------------------------------------------')
    print('Running tests on incorrect sudokus...')
    print('Incorrect methods will generate output below.')
    print('No output indicates that your code passed the tests.')
    row_method_working = True
    column_method_working = True
    block_method_working = True
    for i in range(9):
        for j in range(9):
            solved_sudoku_grid_copy = [[x for x in row] for row in solved_sudoku_grid]
            solved_sudoku_grid_copy[i][j] += 1
            sudoku_test = Sudoku(solved_sudoku_grid_copy)
            rows = sudoku_test.all_rows_complete()
            columns = sudoku_test.all_columns_complete()
            blocks = sudoku_test.all_blocks_complete()
            if rows:
                row_method_working = False
            if columns:
                column_method_working = False
            if blocks:
                block_method_working = False
 
    if not row_method_working:
        print('TEST FAILED! all_rows_complete flagged one or more incorrect sudokus as correct.')
    if not column_method_working:
        print('TEST FAILED! all_columns_complete flagged one or more incorrect sudokus as correct.')
    if not block_method_working:
        print('TEST FAILED! all_blocks_complete flagged one or more incorrect sudokus as correct.')
    print('\n----------------------------------------------')

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def contains_1to9(self, lst):
        """Check whether lst contains 1, 2, 3, ..., 9."""
        return sorted(lst) == list(range(1, 10))
    
    def all_rows_complete(self):
        """Check whether all rows contain the digits 1 through 9"""
        for row in self.grid:
            if not self.contains_1to9(row):
                return False
        return True
    
    def all_columns_complete(self):
        """Check whether all columns contain the digits 1 through 9"""
        for column_index in range(9):
            column = self.get_column_as_list(column_index)
            if not self.contains_1to9(column):
                return False
        return True
    
    def all_blocks_complete(self):
        """Check whether all blocks contain the digits 1 through 9"""
        row_indices = [0, 3, 6]
        column_indices = [0, 3, 6]
        for row_index in row_indices:
            for col_index in column_indices:
                block = self.get_block_as_list(row_index, col_index)
                if not self.contains_1to9(block):
                    return False
        return True

B6 (1 point)#

Complete the method is_solved() and add it to your class. The method shall return True if all rows, columns and blocks contain the digits 1 to 9. Otherwise, the method shall return False.

    def is_solved(self):
        """Check whether the current grid is a solved sudoku"""

NOTE: You may use methods from B1-B5 to solve this problem even if you did not implement them. You may assume that the methods exist and are working as they should.

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def is_solved(self):
        """Check whether the current grid is a solved sudoku"""
        rows = self.all_rows_complete()
        columns = self.all_columns_complete()
        blocks = self.all_blocks_complete()
        return rows and columns and blocks

B7 (4 points)#

Complete the method candidate_digits() and add it to your class.

    def candidate_digits(self):
        """Return a dict of candidate digits for
        all of the currently empty fields. 
 
        All digits that are not yet in the same
        row, column or box as the empty field
        are considered candidate digits.
 
        The returned dict has tuples of field
        coordinates ((row_index, column_index))
        as keys and lists of candidate digits
        as values.
 
        Example:
        --------
        For the grid below, which has only 
        3 empty fields, the method returns
        {(0, 0): [8, 9], (0, 8): [9], (1, 0): [9]}
 
        0 1 2 7 5 3 6 4 0
        0 4 3 6 8 2 1 7 5
        6 7 5 4 9 1 2 8 3
        1 5 4 2 3 7 8 9 6
        3 6 9 8 4 5 7 2 1
        2 8 7 1 6 9 5 3 4
        5 2 1 9 7 4 3 6 8
        4 3 8 5 2 6 9 1 7
        7 9 6 3 1 8 4 5 2
 
        """

The following code

nearly_solved_grid = [[4, 5, 0, 8, 2, 6, 1, 9, 7],
                      [8, 0, 2, 5, 7, 1, 6, 3, 4],
                      [1, 6, 0, 4, 9, 3, 5, 2, 0],
                      [7, 1, 4, 9, 5, 0, 0, 6, 0],
                      [5, 0, 6, 1, 0, 7, 0, 4, 0],
                      [3, 0, 9, 0, 8, 4, 7, 5, 1],
                      [0, 0, 0, 2, 1, 0, 4, 7, 0],
                      [6, 0, 1, 3, 4, 5, 9, 8, 2],
                      [0, 4, 0, 7, 6, 0, 3, 1, 5]]
  
nearly_solved_sudoku = Sudoku(nearly_solved_grid)
print(nearly_solved_sudoku.candidate_digits())

should produce the printout

{(0, 2): [3], (1, 1): [9], (2, 2): [7], (2, 8): [8], (3, 5): [2], (3, 6): [2, 8],
(3, 8): [3, 8], (4, 1): [2, 8], (4, 4): [3], (4, 6): [2, 8], (4, 8): [3, 8, 9],
(5, 1): [2], (5, 3): [6], (6, 0): [9], (6, 1): [3, 8, 9], (6, 2): [3, 5, 8],
(6, 5): [8, 9], (6, 8): [6], (7, 1): [7], (8, 0): [2, 9], (8, 2): [8], (8, 5): [8, 9]}

NOTE: You may use methods from B1-B6 to solve this problem even if you did not implement them. You may assume that the methods exist and are working as they should.

No submission here. Please submit the entire Sudoku class in the field for task B8. Include your best attempts at all the methods in B1-B8 in that submission.

Example solution
    def candidate_digits(self):
        """Return a dict of candidate digits for
        all of the currently empty fields. 
        """
        candidate_digits = {}
        for row_index in range(9):
            for column_index in range(9):
                # Check if this box is empty
                if self.grid[row_index][column_index] == 0:
                    # Check which digits are already in this row, column, and block
                    row = self.grid[row_index]
                    column = self.get_column_as_list(column_index)
                    block = self.get_block_as_list(row_index, column_index)
                    all_digits = row + column + block
                    
                    # Construct list of candidates for this box
                    local_candidates = [x for x in range(1, 10) if x not in all_digits]
                    
                    # Add list of candidates to dictionary
                    candidate_digits[(row_index, column_index)] = local_candidates
        return candidate_digits

B8 (4 points)#

Complete the method random_solve() and add it to your class.

    def random_solve(self):
        """Solve the sudoku by selecting one of the candidate digits
        at random until a solution is found"""

The method’s objective is to solve the Sudoku by adding nonzero digits to the empty fields in the grid attribute. The method shall use the following algorithm:

  1. Determine the candidate digits for all empty fields.

  2. For every empty field, randomly select one of the candidate digits for that field and write it in the field.

  3. Check if the full grid that you ended up with meets all the criteria for a solved sudoku. If it does, you are done. Otherwise, go back to step 2 and repeat.

This algorithm is not optimal and is therefore only able to solve sudokus with relatively few empty fields in reasonable time. You may use the puzzle defined by nearly_solved_grid to test it. If random_solve() is correctly implemented it should solve this puzzle within a second.

# Test your method on this grid
nearly_solved_grid = [[4, 5, 0, 8, 2, 6, 1, 9, 7],
                      [8, 0, 2, 5, 7, 1, 6, 3, 4],
                      [1, 6, 0, 4, 9, 3, 5, 2, 0],
                      [7, 1, 4, 9, 5, 0, 0, 6, 0],
                      [5, 0, 6, 1, 0, 7, 0, 4, 0],
                      [3, 0, 9, 0, 8, 4, 7, 5, 1],
                      [0, 0, 0, 2, 1, 0, 4, 7, 0],
                      [6, 0, 1, 3, 4, 5, 9, 8, 2],
                      [0, 4, 0, 7, 6, 0, 3, 1, 5]]

NOTE: You may use methods from B1-B7 to solve this problem even if you did not implement them. You may assume that the methods exist and are working as they should.

If you did not solve B7, note that the candidate digits corresponding to nearly_solved_grid are provided in B7. You can use those candidate digits in random_solve() to verify that random_solve() is working correctly.

Example solution
    def random_solve(self):
        """Solve the sudoku by selecting one of the candidate digits
        at random until a solution is found"""
        import random # Can/should be placed outside class
        candidate_digits = self.candidate_digits()
        while not self.is_solved():
            # Choose one of the candidate digits at random, for each empty field
            for row_index, column_index in candidate_digits.keys():
                local_candidates = candidate_digits[(row_index, column_index)]
                random_index = random.randint(0, len(local_candidates)-1)
                self.grid[row_index][column_index] = local_candidates[random_index]