CMU 15-112: Fundamentals of Programming and Computer Science
Sudoku Hints (from lecture on 22-Nov)


State class
  * state.board
  * state.legals
  * copy.deepcopy(state)

Debug printing
  * def printBoard(self):
        # copy print2dList from here:
        # https://www.cs.cmu.edu/~112/notes/notes-2d-lists.html#printing
        print2dList(self.board)
  * def printLegals(self):
        colWidth = 4
        for col in range(9):
            colWidth = max(colWidth, 1+max([len(self.legals[row][col]) for row in range(9)]))
        for row in range(9):
            for col in range(9):
                label = ''.join([str(v) for v in sorted(self.legals[row][col])])
                if label == '': label = '-'
                print(f"{' '*(colWidth - len(label))}{label}", end='')
            print()
  * def print(self): self.printBoard(); self.printLegals()

Board loading
  * def loadBoardPaths(filters):
        boardPaths = [ ]
        for filename in os.listdir(f'boards/'):
            if filename.endswith('.txt'):
                if hasFilters(filename, filters):
                    boardPaths.append(f'boards/{filename}')
        return boardPaths

    def hasFilters(filename, filters=None):
        if filters == None: return True
        for filter in filters:
            if filter not in filename:
                return False
        return True

    def loadRandomBoard(filters=None):
        ...

State set, ban, unban
  * state.set(self, row, col, value)
  * state.ban(self, row, col, values)
  * state.unban(self, row, col, values)

State Constructor
  * create board + legals
  * set each non-zero value in the starting board 
    (use state.set so legals are updated)

Regions
  * A region is a list of 9 (row,col) tuples
  * def getRowRegion(self, row):
    def getColRegion(self, col):
    def getBlockRegion(self, block):
    def getBlock(self, row, col):
    def getBlockRegionByCell(self, row, col):
    def getCellRegions(self, row, col):
    def getAllRegions(self):
    def getAllRegionsThatContainTargets(self, targets):

Backtracking Solver
  * Basic backtracker: expand first empty cell
  * Faster backtracker: expand cell with fewest legals

Hints
  * Hint 1: obvious (naked) single
  * Hint 2: obvious (naked) tuple (pair, triple, quad, quint)
    def getHint2(self):
        for region in self.getAllRegions():
            for N in range(2, 6):
                result = self.applyRule2(region, N)
                if result != None:
                    return result
        return None

    def applyRule2(self, region, N):
        '''
        This method uses:
            * itertools.combinations(region, N)
            * self.valuesAreOnlyLegals(values, targets)
            * self.getBansForAllRegions(values, targets)
        '''

    def getBansForAllRegions(self, values, targets):
        # The values (to ban) can stay in the targets, but they must be
        # banned from all other cells in all regions that contain all
        # the targets
        bans = [ ]
        for region in self.getAllRegionsThatContainTargets(targets):
            ...

Testing the backtracker
  * def testBacktracker(filters):
        time0 = time.time()
        boardPaths = sorted(loadBoardPaths(filters))
        failedPaths = [ ]
        for boardPath in boardPaths:
            board = loadBoard(boardPath)
            print(boardPath)
            solution = solveWithBacktracking(board, verbose=False)
            if not solution:
                failedPaths.append(boardPath)
        print()
        totalCount = len(boardPaths)
        failedCount = len(failedPaths)
        okCount = totalCount - failedCount
        time1 = time.time()
        if len(failedPaths) > 0:
            print('Failed boards:')
            for path in failedPaths:
                print(f'    {path}')
        percent = round(100 * okCount/totalCount)
        print(f'Success rate: {okCount}/{totalCount} = {percent}%')
        print(f'Total time: {round(time1-time0, 1)} seconds')

    Call like so:
        testBacktracker(filters=['easy'])

carpe diem