├── objects ├── __init__.py ├── SudokuGrid.pyc ├── SudokuSquare.pyc ├── GameResources.pyc ├── GameResources.py └── SudokuSquare.py ├── images └── sudoku-board-bare.jpg ├── visualize.py ├── PySudoku.py ├── README.md ├── solution.py └── solution_test.py /objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /objects/SudokuGrid.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canyon289/AIND-Sudoku/master/objects/SudokuGrid.pyc -------------------------------------------------------------------------------- /objects/SudokuSquare.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canyon289/AIND-Sudoku/master/objects/SudokuSquare.pyc -------------------------------------------------------------------------------- /objects/GameResources.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canyon289/AIND-Sudoku/master/objects/GameResources.pyc -------------------------------------------------------------------------------- /images/sudoku-board-bare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/canyon289/AIND-Sudoku/master/images/sudoku-board-bare.jpg -------------------------------------------------------------------------------- /objects/GameResources.py: -------------------------------------------------------------------------------- 1 | import os, pygame 2 | 3 | def load_image(name): 4 | """A better load of images.""" 5 | fullname = os.path.join("images", name) 6 | try: 7 | image = pygame.image.load(fullname) 8 | if image.get_alpha() == None: 9 | image = image.convert() 10 | else: 11 | image = image.convert_alpha() 12 | except pygame.error: 13 | print("Oops! Could not load image:", fullname) 14 | return image, image.get_rect() 15 | -------------------------------------------------------------------------------- /visualize.py: -------------------------------------------------------------------------------- 1 | from PySudoku import play 2 | 3 | def visualize_assignments(assignments): 4 | """ Visualizes the set of assignments created by the Sudoku AI""" 5 | last_assignment = None 6 | filtered_assignments = [] 7 | 8 | for i in range(len(assignments)): 9 | if last_assignment: 10 | last_assignment_items = [item for item in last_assignment.items() if len(item[1]) == 1] 11 | current_assignment_items = [item for item in assignments[i].items() if len(item[1]) == 1] 12 | shared_items = set(last_assignment_items) & set(current_assignment_items) 13 | if len(shared_items) < len(current_assignment_items): 14 | filtered_assignments.append(assignments[i]) 15 | last_assignment = assignments[i] 16 | 17 | play(filtered_assignments) 18 | -------------------------------------------------------------------------------- /PySudoku.py: -------------------------------------------------------------------------------- 1 | import sys, os, random, pygame 2 | sys.path.append(os.path.join("objects")) 3 | import SudokuSquare 4 | from GameResources import * 5 | 6 | digits = '123456789' 7 | rows = 'ABCDEFGHI' 8 | 9 | 10 | def play(values_list): 11 | pygame.init() 12 | 13 | 14 | size = width, height = 700, 700 15 | screen = pygame.display.set_mode(size) 16 | 17 | background_image = pygame.image.load("./images/sudoku-board-bare.jpg").convert() 18 | 19 | clock = pygame.time.Clock() 20 | 21 | # The puzzleNumber sets a seed so either generate 22 | # a random number to fill in here or accept user 23 | # input for a duplicatable puzzle. 24 | 25 | for values in values_list: 26 | pygame.event.pump() 27 | theSquares = [] 28 | initXLoc = 0 29 | initYLoc = 0 30 | startX, startY, editable, number = 0, 0, "N", 0 31 | for y in range(9): 32 | for x in range(9): 33 | if x in (0, 1, 2): startX = (x * 57) + 38 34 | if x in (3, 4, 5): startX = (x * 57) + 99 35 | if x in (6, 7, 8): startX = (x * 57) + 159 36 | 37 | if y in (0, 1, 2): startY = (y * 57) + 35 38 | if y in (3, 4, 5): startY = (y * 57) + 100 39 | if y in (6, 7, 8): startY = (y * 57) + 165 40 | col = digits[y] 41 | row = rows[x] 42 | string_number = values[row + col] 43 | if len(string_number) > 1 or string_number == '' or string_number == '.': 44 | number = None 45 | else: 46 | number = int(string_number) 47 | theSquares.append(SudokuSquare.SudokuSquare(number, startX, startY, editable, x, y)) 48 | 49 | screen.blit(background_image, (0, 0)) 50 | for num in theSquares: 51 | num.draw() 52 | 53 | pygame.display.flip() 54 | pygame.display.update() 55 | clock.tick(5) 56 | 57 | 58 | if __name__ == "__main__": 59 | main() 60 | sys.exit() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Artificial Intelligence Nanodegree 2 | ## Introductory Project: Diagonal Sudoku Solver 3 | 4 | # Question 1 (Naked Twins) 5 | Q: How do we use constraint propagation to solve the naked twins problem? 6 | A: Naked Twins is another method through which possible digits can eliminated 7 | as values for boxes. With this method when two boxes have the same two solutions 8 | and are located in the same unit, or constraint space, this means that 9 | no other boxes besides can be these two values. Eliminating the 10 | conflicting digits from the non twin boxes is the constraint propogation. 11 | Worded another way constraint propogation allows us to further cut down 12 | the possible solution space for all boxes in a unit, given that two 13 | boxes in the unit are constrained to the same two values. 14 | 15 | # Question 2 (Diagonal Sudoku) 16 | Q: How do we use constraint propagation to solve the diagonal sudoku problem? 17 | A: Diagonal Sudoku introduces another constraint, in that the diagonal boxes 18 | now also cannot share the same digit as another box in the same diagonal. 19 | This constraint is added by adding two more units to our unitlist. When 20 | solving the puzzle the constraint propogation is handled identically to the 21 | normal row, box, and square, constraint. If any of the boxes in the diagonals 22 | have a known value, that value constrains the possible solution digits of 23 | all the other boxes. By removing the solved digit from all other diagonals 24 | we are using constraint propogation to narrow down the possible solutions 25 | repeatedly until the final solution is obtained. 26 | 27 | ### Install 28 | 29 | This project requires **Python 3**. 30 | 31 | We recommend students install [Anaconda](https://www.continuum.io/downloads), a pre-packaged Python distribution that contains all of the necessary libraries and software for this project. 32 | Please try using the environment we provided in the Anaconda lesson of the Nanodegree. 33 | 34 | ##### Optional: Pygame 35 | 36 | Optionally, you can also install pygame if you want to see your visualization. If you've followed our instructions for setting up our conda environment, you should be all set. 37 | 38 | If not, please see how to download pygame [here](http://www.pygame.org/download.shtml). 39 | 40 | ### Code 41 | 42 | * `solutions.py` - You'll fill this in as part of your solution. 43 | * `solution_test.py` - Do not modify this. You can test your solution by running `python solution_test.py`. 44 | * `PySudoku.py` - Do not modify this. This is code for visualizing your solution. 45 | * `visualize.py` - Do not modify this. This is code for visualizing your solution. 46 | 47 | ### Visualizing 48 | 49 | To visualize your solution, please only assign values to the values_dict using the ```assign_values``` function provided in solution.py 50 | 51 | ### Data 52 | 53 | The data consists of a text file of diagonal sudokus for you to solve. 54 | -------------------------------------------------------------------------------- /objects/SudokuSquare.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | from pygame import * 4 | 5 | def AAfilledRoundedRect(surface,rect,color,radius=0.4): 6 | 7 | """ 8 | AAfilledRoundedRect(surface,rect,color,radius=0.4) 9 | 10 | surface : destination 11 | rect : rectangle 12 | color : rgb or rgba 13 | radius : 0 <= radius <= 1 14 | """ 15 | 16 | rect = Rect(rect) 17 | color = Color(*color) 18 | alpha = color.a 19 | color.a = 0 20 | pos = rect.topleft 21 | rect.topleft = 0,0 22 | rectangle = Surface(rect.size,SRCALPHA) 23 | 24 | circle = Surface([min(rect.size)*3]*2,SRCALPHA) 25 | draw.ellipse(circle,(0,0,0),circle.get_rect(),0) 26 | circle = transform.smoothscale(circle,[int(min(rect.size)*radius)]*2) 27 | 28 | radius = rectangle.blit(circle,(0,0)) 29 | radius.bottomright = rect.bottomright 30 | rectangle.blit(circle,radius) 31 | radius.topright = rect.topright 32 | rectangle.blit(circle,radius) 33 | radius.bottomleft = rect.bottomleft 34 | rectangle.blit(circle,radius) 35 | 36 | rectangle.fill((0,0,0),rect.inflate(-radius.w,0)) 37 | rectangle.fill((0,0,0),rect.inflate(0,-radius.h)) 38 | 39 | rectangle.fill(color,special_flags=BLEND_RGBA_MAX) 40 | rectangle.fill((255,255,255,alpha),special_flags=BLEND_RGBA_MIN) 41 | 42 | return surface.blit(rectangle,pos) 43 | 44 | class SudokuSquare: 45 | """A sudoku square class.""" 46 | def __init__(self, number=None, offsetX=0, offsetY=0, edit="Y", xLoc=0, yLoc=0): 47 | if number != None: 48 | number = str(number) 49 | self.color = (2, 204, 186) 50 | else: 51 | number = "" 52 | self.color = (255, 255, 255) 53 | # print("FONTS", pygame.font.get_fonts()) 54 | self.font = pygame.font.SysFont('opensans', 21) 55 | self.text = self.font.render(number, 1, (255, 255, 255)) 56 | self.textpos = self.text.get_rect() 57 | self.textpos = self.textpos.move(offsetX + 17, offsetY + 4) 58 | 59 | # self.collide = pygame.Surface((25, 22)) 60 | # self.collide = self.collide.convert() 61 | # AAfilledRoundedRect(pygame.display.get_surface(), (xLoc, yLoc, 25, 22), (255, 255, 255)) 62 | # self.collide.fill((2, 204, 186)) 63 | # self.collideRect = self.collide.get_rect() 64 | # self.collideRect = self.collideRect.move(offsetX + 1, offsetY + 1) 65 | # The rect around the text is 11 x 28 66 | 67 | self.edit = edit 68 | self.xLoc = xLoc 69 | self.yLoc = yLoc 70 | self.offsetX = offsetX 71 | self.offsetY = offsetY 72 | 73 | def draw(self): 74 | screen = pygame.display.get_surface() 75 | AAfilledRoundedRect(screen, (self.offsetX, self.offsetY, 45, 40), self.color) 76 | 77 | # screen.blit(self.collide, self.collideRect) 78 | screen.blit(self.text, self.textpos) 79 | 80 | 81 | def checkCollide(self, collision): 82 | if len(collision) == 2: 83 | return self.collideRect.collidepoint(collision) 84 | elif len(collision) == 4: 85 | return self.collideRect.colliderect(collision) 86 | else: 87 | return False 88 | 89 | 90 | def highlight(self): 91 | self.collide.fill((190, 190, 255)) 92 | self.draw() 93 | 94 | 95 | def unhighlight(self): 96 | self.collide.fill((255, 255, 255, 255)) 97 | self.draw() 98 | 99 | 100 | def change(self, number): 101 | if number != None: 102 | number = str(number) 103 | else: 104 | number = "" 105 | 106 | if self.edit == "Y": 107 | self.text = self.font.render(number, 1, (0, 0, 0)) 108 | self.draw() 109 | return 0 110 | else: 111 | return 1 112 | 113 | 114 | def currentLoc(self): 115 | return self.xLoc, self.yLoc -------------------------------------------------------------------------------- /solution.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | 3 | rows = 'ABCDEFGHI' 4 | cols = '123456789' 5 | 6 | def cross(a, b): 7 | return [s+t for s in a for t in b] 8 | 9 | boxes = cross(rows, cols) 10 | 11 | row_units = [cross(r, cols) for r in rows] 12 | column_units = [cross(rows, c) for c in cols] 13 | square_units = [cross(rs, cs) for rs in ('ABC','DEF','GHI') for cs in ('123','456','789')] 14 | diagonal_units = [[r+c for r,c in zip(rows,cols)],[r+c for r,c in zip(rows[::-1],cols)]] 15 | 16 | # Unit list is a list of lists of all units which are groups of boxes 17 | unitlist = row_units + column_units + square_units + diagonal_units 18 | 19 | # Unit is a dictionary of all boxes in a unit keyed by box 20 | units = dict((s, [u for u in unitlist if s in u]) for s in boxes) 21 | 22 | # Peers combine all units for a box, removing the box itself 23 | peers = dict((s, set(sum(units[s],[]))-set([s])) for s in boxes) 24 | 25 | assignments = [] 26 | 27 | def assign_value(values, box, value): 28 | """ 29 | Please use this function to update your values dictionary! 30 | Assigns a value to a given box. If it updates the board record it. 31 | """ 32 | values[box] = value 33 | if len(value) == 1: 34 | assignments.append(values.copy()) 35 | return values 36 | 37 | def naked_twins(values): 38 | """Eliminate values using the naked twins strategy. 39 | Args: 40 | values(dict): a dictionary of the form {'box_name': '123456789', ...} 41 | 42 | Returns: 43 | the values dictionary with the naked twins eliminated from peers. 44 | """ 45 | # Find all instances of naked twins 46 | 47 | for unit in unitlist: 48 | current_values = [] 49 | for box in unit: 50 | value = values[box] 51 | if len(value) == 2: 52 | current_values.append(value) 53 | counts = Counter(current_values) 54 | 55 | # Eliminate the naked twins as possibilities for their peers 56 | for value, count in counts.items(): 57 | if count == 2: 58 | for box in unit: 59 | if values[box] != value: 60 | for digit in value: 61 | values[box] = values[box].replace(digit,'') 62 | return values 63 | 64 | def grid_values(grid): 65 | """ 66 | Convert grid into a dict of {square: char} with '123456789' for empties. 67 | Args: 68 | grid(string) - A grid in string form. 69 | Returns: 70 | A grid in dictionary form 71 | Keys: The boxes, e.g., 'A1' 72 | Values: The value in each box, e.g., '8'. If the box has no value, then the value will be '123456789'. 73 | """ 74 | values = [] 75 | all_digits = '123456789' 76 | for c in grid: 77 | if c == '.': 78 | values.append(all_digits) 79 | elif c in all_digits: 80 | values.append(c) 81 | assert len(values) == 81 82 | return dict(zip(boxes, values)) 83 | 84 | def display(values): 85 | """ 86 | Display the values as a 2-D grid. 87 | Input: The sudoku in dictionary form 88 | Output: None 89 | """ 90 | width = 1+max(len(values[s]) for s in boxes) 91 | line = '+'.join(['-'*(width*3)]*3) 92 | for r in rows: 93 | print(''.join(values[r+c].center(width)+('|' if c in '36' else '') 94 | for c in cols)) 95 | if r in 'CF': print(line) 96 | return 97 | 98 | def eliminate(values): 99 | """Eliminates known values from peers of box""" 100 | # Get all boxes with solved values 101 | solved_values = [box for box in values.keys() if len(values[box]) == 1] 102 | 103 | # For each solved box eliminate the value from all peers 104 | for box in solved_values: 105 | digit = values[box] 106 | for peer in peers[box]: 107 | values[peer] = values[peer].replace(digit,'') 108 | return values 109 | 110 | def only_choice(values): 111 | """Determines if there is only one box where number can be placed""" 112 | for unit in unitlist: 113 | for digit in '123456789': 114 | dplaces = [box for box in unit if digit in values[box]] 115 | if len(dplaces) == 1: 116 | values = assign_value(values, dplaces[0], digit) 117 | return values 118 | 119 | def reduce_puzzle(values): 120 | solved_values = [box for box in values.keys() if len(values[box]) == 1] 121 | stalled = False 122 | while not stalled: 123 | # Check how many boxes have a determined value 124 | solved_values_before = len([box for box in values.keys() if len(values[box]) == 1]) 125 | # Use the Eliminate Strategy 126 | values = eliminate(values) 127 | # Use the Only Choice Strategy 128 | values = only_choice(values) 129 | # Use the Naked Twins strategy 130 | values = naked_twins(values) 131 | # Check how many boxes have a determined value, to compare 132 | solved_values_after = len([box for box in values.keys() if len(values[box]) == 1]) 133 | # If no new values were added, stop the loop. 134 | stalled = solved_values_before == solved_values_after 135 | # Sanity check, return False if there is a box with zero available values: 136 | if len([box for box in values.keys() if len(values[box]) == 0]): 137 | return False 138 | return values 139 | 140 | 141 | def search(values): 142 | "Using depth-first search and propagation, create a search tree and solve the sudoku." 143 | # First, reduce the puzzle using the previous function 144 | values = reduce_puzzle(values) 145 | if values is False: 146 | return False ## Failed earlier 147 | if all(len(values[s]) == 1 for s in boxes): 148 | return values ## Solved! 149 | # Chose one of the unfilled square s with the fewest possibilities 150 | n,s = min((len(values[s]), s) for s in boxes if len(values[s]) > 1) 151 | # Now use recurrence to solve each one of the resulting sudokus, and 152 | for value in values[s]: 153 | new_sudoku = values.copy() 154 | new_sudoku[s] = value 155 | attempt = search(new_sudoku) 156 | if attempt: 157 | return attempt 158 | 159 | def solve(grid): 160 | """ 161 | Find the solution to a Sudoku grid. 162 | Args: 163 | grid(string): a string representing a sudoku grid. 164 | Example: '2.............62....1....7...6..8...3...9...7...6..4...4....8....52.............3' 165 | Returns: 166 | The dictionary representation of the final sudoku grid. False if no solution exists. 167 | """ 168 | return search(grid_values(grid)) 169 | 170 | if __name__ == '__main__': 171 | diag_sudoku_grid = '2.............62....1....7...6..8...3...9...7...6..4...4....8....52.............3' 172 | display(solve(diag_sudoku_grid)) 173 | 174 | try: 175 | from visualize import visualize_assignments 176 | visualize_assignments(assignments) 177 | except: 178 | print('We could not visualize your board due to a pygame issue. Not a problem! It is not a requirement.') 179 | -------------------------------------------------------------------------------- /solution_test.py: -------------------------------------------------------------------------------- 1 | import solution 2 | import unittest 3 | 4 | 5 | class TestNakedTwins(unittest.TestCase): 6 | before_naked_twins_1 = {'I6': '4', 'H9': '3', 'I2': '6', 'E8': '1', 'H3': '5', 'H7': '8', 'I7': '1', 'I4': '8', 7 | 'H5': '6', 'F9': '7', 'G7': '6', 'G6': '3', 'G5': '2', 'E1': '8', 'G3': '1', 'G2': '8', 8 | 'G1': '7', 'I1': '23', 'C8': '5', 'I3': '23', 'E5': '347', 'I5': '5', 'C9': '1', 'G9': '5', 9 | 'G8': '4', 'A1': '1', 'A3': '4', 'A2': '237', 'A5': '9', 'A4': '2357', 'A7': '27', 10 | 'A6': '257', 'C3': '8', 'C2': '237', 'C1': '23', 'E6': '579', 'C7': '9', 'C6': '6', 11 | 'C5': '37', 'C4': '4', 'I9': '9', 'D8': '8', 'I8': '7', 'E4': '6', 'D9': '6', 'H8': '2', 12 | 'F6': '125', 'A9': '8', 'G4': '9', 'A8': '6', 'E7': '345', 'E3': '379', 'F1': '6', 13 | 'F2': '4', 'F3': '23', 'F4': '1235', 'F5': '8', 'E2': '37', 'F7': '35', 'F8': '9', 14 | 'D2': '1', 'H1': '4', 'H6': '17', 'H2': '9', 'H4': '17', 'D3': '2379', 'B4': '27', 15 | 'B5': '1', 'B6': '8', 'B7': '27', 'E9': '2', 'B1': '9', 'B2': '5', 'B3': '6', 'D6': '279', 16 | 'D7': '34', 'D4': '237', 'D5': '347', 'B8': '3', 'B9': '4', 'D1': '5'} 17 | possible_solutions_1 = [ 18 | {'G7': '6', 'G6': '3', 'G5': '2', 'G4': '9', 'G3': '1', 'G2': '8', 'G1': '7', 'G9': '5', 'G8': '4', 'C9': '1', 19 | 'C8': '5', 'C3': '8', 'C2': '237', 'C1': '23', 'C7': '9', 'C6': '6', 'C5': '37', 'A4': '2357', 'A9': '8', 20 | 'A8': '6', 'F1': '6', 'F2': '4', 'F3': '23', 'F4': '1235', 'F5': '8', 'F6': '125', 'F7': '35', 'F8': '9', 21 | 'F9': '7', 'B4': '27', 'B5': '1', 'B6': '8', 'B7': '27', 'E9': '2', 'B1': '9', 'B2': '5', 'B3': '6', 'C4': '4', 22 | 'B8': '3', 'B9': '4', 'I9': '9', 'I8': '7', 'I1': '23', 'I3': '23', 'I2': '6', 'I5': '5', 'I4': '8', 'I7': '1', 23 | 'I6': '4', 'A1': '1', 'A3': '4', 'A2': '237', 'A5': '9', 'E8': '1', 'A7': '27', 'A6': '257', 'E5': '347', 24 | 'E4': '6', 'E7': '345', 'E6': '579', 'E1': '8', 'E3': '79', 'E2': '37', 'H8': '2', 'H9': '3', 'H2': '9', 25 | 'H3': '5', 'H1': '4', 'H6': '17', 'H7': '8', 'H4': '17', 'H5': '6', 'D8': '8', 'D9': '6', 'D6': '279', 26 | 'D7': '34', 'D4': '237', 'D5': '347', 'D2': '1', 'D3': '79', 'D1': '5'}, 27 | {'I6': '4', 'H9': '3', 'I2': '6', 'E8': '1', 'H3': '5', 'H7': '8', 'I7': '1', 'I4': '8', 'H5': '6', 'F9': '7', 28 | 'G7': '6', 'G6': '3', 'G5': '2', 'E1': '8', 'G3': '1', 'G2': '8', 'G1': '7', 'I1': '23', 'C8': '5', 'I3': '23', 29 | 'E5': '347', 'I5': '5', 'C9': '1', 'G9': '5', 'G8': '4', 'A1': '1', 'A3': '4', 'A2': '237', 'A5': '9', 30 | 'A4': '2357', 'A7': '27', 'A6': '257', 'C3': '8', 'C2': '237', 'C1': '23', 'E6': '579', 'C7': '9', 'C6': '6', 31 | 'C5': '37', 'C4': '4', 'I9': '9', 'D8': '8', 'I8': '7', 'E4': '6', 'D9': '6', 'H8': '2', 'F6': '125', 32 | 'A9': '8', 'G4': '9', 'A8': '6', 'E7': '345', 'E3': '79', 'F1': '6', 'F2': '4', 'F3': '23', 'F4': '1235', 33 | 'F5': '8', 'E2': '3', 'F7': '35', 'F8': '9', 'D2': '1', 'H1': '4', 'H6': '17', 'H2': '9', 'H4': '17', 34 | 'D3': '79', 'B4': '27', 'B5': '1', 'B6': '8', 'B7': '27', 'E9': '2', 'B1': '9', 'B2': '5', 'B3': '6', 35 | 'D6': '279', 'D7': '34', 'D4': '237', 'D5': '347', 'B8': '3', 'B9': '4', 'D1': '5'} 36 | ] 37 | 38 | before_naked_twins_2 = {'A1': '23', 'A2': '4', 'A3': '7', 'A4': '6', 'A5': '8', 'A6': '5', 'A7': '23', 'A8': '9', 39 | 'A9': '1', 'B1': '6', 'B2': '9', 'B3': '8', 'B4': '4', 'B5': '37', 'B6': '1', 'B7': '237', 40 | 'B8': '5', 'B9': '237', 'C1': '23', 'C2': '5', 'C3': '1', 'C4': '23', 'C5': '379', 41 | 'C6': '2379', 'C7': '8', 'C8': '6', 'C9': '4', 'D1': '8', 'D2': '17', 'D3': '9', 42 | 'D4': '1235', 'D5': '6', 'D6': '237', 'D7': '4', 'D8': '27', 'D9': '2357', 'E1': '5', 43 | 'E2': '6', 'E3': '2', 'E4': '8', 'E5': '347', 'E6': '347', 'E7': '37', 'E8': '1', 'E9': '9', 44 | 'F1': '4', 'F2': '17', 'F3': '3', 'F4': '125', 'F5': '579', 'F6': '279', 'F7': '6', 45 | 'F8': '8', 'F9': '257', 'G1': '1', 'G2': '8', 'G3': '6', 'G4': '35', 'G5': '345', 46 | 'G6': '34', 'G7': '9', 'G8': '27', 'G9': '27', 'H1': '7', 'H2': '2', 'H3': '4', 'H4': '9', 47 | 'H5': '1', 'H6': '8', 'H7': '5', 'H8': '3', 'H9': '6', 'I1': '9', 'I2': '3', 'I3': '5', 48 | 'I4': '7', 'I5': '2', 'I6': '6', 'I7': '1', 'I8': '4', 'I9': '8'} 49 | possible_solutions_2 = [ 50 | {'A1': '23', 'A2': '4', 'A3': '7', 'A4': '6', 'A5': '8', 'A6': '5', 'A7': '23', 'A8': '9', 'A9': '1', 'B1': '6', 51 | 'B2': '9', 'B3': '8', 'B4': '4', 'B5': '37', 'B6': '1', 'B7': '237', 'B8': '5', 'B9': '237', 'C1': '23', 52 | 'C2': '5', 'C3': '1', 'C4': '23', 'C5': '79', 'C6': '79', 'C7': '8', 'C8': '6', 'C9': '4', 'D1': '8', 53 | 'D2': '17', 'D3': '9', 'D4': '1235', 'D5': '6', 'D6': '237', 'D7': '4', 'D8': '27', 'D9': '2357', 'E1': '5', 54 | 'E2': '6', 'E3': '2', 'E4': '8', 'E5': '347', 'E6': '347', 'E7': '37', 'E8': '1', 'E9': '9', 'F1': '4', 55 | 'F2': '17', 'F3': '3', 'F4': '125', 'F5': '579', 'F6': '279', 'F7': '6', 'F8': '8', 'F9': '257', 'G1': '1', 56 | 'G2': '8', 'G3': '6', 'G4': '35', 'G5': '345', 'G6': '34', 'G7': '9', 'G8': '27', 'G9': '27', 'H1': '7', 57 | 'H2': '2', 'H3': '4', 'H4': '9', 'H5': '1', 'H6': '8', 'H7': '5', 'H8': '3', 'H9': '6', 'I1': '9', 'I2': '3', 58 | 'I3': '5', 'I4': '7', 'I5': '2', 'I6': '6', 'I7': '1', 'I8': '4', 'I9': '8'}, 59 | {'A1': '23', 'A2': '4', 'A3': '7', 'A4': '6', 'A5': '8', 'A6': '5', 'A7': '23', 'A8': '9', 'A9': '1', 'B1': '6', 60 | 'B2': '9', 'B3': '8', 'B4': '4', 'B5': '3', 'B6': '1', 'B7': '237', 'B8': '5', 'B9': '237', 'C1': '23', 61 | 'C2': '5', 'C3': '1', 'C4': '23', 'C5': '79', 'C6': '79', 'C7': '8', 'C8': '6', 'C9': '4', 'D1': '8', 62 | 'D2': '17', 'D3': '9', 'D4': '1235', 'D5': '6', 'D6': '237', 'D7': '4', 'D8': '27', 'D9': '2357', 'E1': '5', 63 | 'E2': '6', 'E3': '2', 'E4': '8', 'E5': '347', 'E6': '347', 'E7': '37', 'E8': '1', 'E9': '9', 'F1': '4', 64 | 'F2': '17', 'F3': '3', 'F4': '125', 'F5': '579', 'F6': '279', 'F7': '6', 'F8': '8', 'F9': '257', 'G1': '1', 65 | 'G2': '8', 'G3': '6', 'G4': '35', 'G5': '345', 'G6': '34', 'G7': '9', 'G8': '27', 'G9': '27', 'H1': '7', 66 | 'H2': '2', 'H3': '4', 'H4': '9', 'H5': '1', 'H6': '8', 'H7': '5', 'H8': '3', 'H9': '6', 'I1': '9', 'I2': '3', 67 | 'I3': '5', 'I4': '7', 'I5': '2', 'I6': '6', 'I7': '1', 'I8': '4', 'I9': '8'} 68 | ] 69 | 70 | def test_naked_twins(self): 71 | self.assertTrue(solution.naked_twins(self.before_naked_twins_1) in self.possible_solutions_1, 72 | "Your naked_twins function produced an unexpected board.") 73 | 74 | def test_naked_twins2(self): 75 | self.assertTrue(solution.naked_twins(self.before_naked_twins_2) in self.possible_solutions_2, 76 | "Your naked_twins function produced an unexpected board.") 77 | 78 | 79 | 80 | class TestDiagonalSudoku(unittest.TestCase): 81 | diagonal_grid = '2.............62....1....7...6..8...3...9...7...6..4...4....8....52.............3' 82 | solved_diag_sudoku = {'G7': '8', 'G6': '9', 'G5': '7', 'G4': '3', 'G3': '2', 'G2': '4', 'G1': '6', 'G9': '5', 83 | 'G8': '1', 'C9': '6', 'C8': '7', 'C3': '1', 'C2': '9', 'C1': '4', 'C7': '5', 'C6': '3', 84 | 'C5': '2', 'C4': '8', 'E5': '9', 'E4': '1', 'F1': '1', 'F2': '2', 'F3': '9', 'F4': '6', 85 | 'F5': '5', 'F6': '7', 'F7': '4', 'F8': '3', 'F9': '8', 'B4': '7', 'B5': '1', 'B6': '6', 86 | 'B7': '2', 'B1': '8', 'B2': '5', 'B3': '3', 'B8': '4', 'B9': '9', 'I9': '3', 'I8': '2', 87 | 'I1': '7', 'I3': '8', 'I2': '1', 'I5': '6', 'I4': '5', 'I7': '9', 'I6': '4', 'A1': '2', 88 | 'A3': '7', 'A2': '6', 'E9': '7', 'A4': '9', 'A7': '3', 'A6': '5', 'A9': '1', 'A8': '8', 89 | 'E7': '6', 'E6': '2', 'E1': '3', 'E3': '4', 'E2': '8', 'E8': '5', 'A5': '4', 'H8': '6', 90 | 'H9': '4', 'H2': '3', 'H3': '5', 'H1': '9', 'H6': '1', 'H7': '7', 'H4': '2', 'H5': '8', 91 | 'D8': '9', 'D9': '2', 'D6': '8', 'D7': '1', 'D4': '4', 'D5': '3', 'D2': '7', 'D3': '6', 92 | 'D1': '5'} 93 | 94 | def test_solve(self): 95 | self.assertEqual(solution.solve(self.diagonal_grid), self.solved_diag_sudoku) 96 | 97 | if __name__ == '__main__': 98 | unittest.main() 99 | --------------------------------------------------------------------------------