Sudoku Challenges cont’d.

It turns out that some readers considered Challenge 2 a little bit of a trick because the one line solution is a lambda function.

A lambda function is an inline, unnamed function. Here are the solutions as both a standard and lambda function:


#  function to find box number given row and column
def box_number(row, col):
    box = ((0,1,4,7)[(row-1)//3 + 1]) + ((col-1)//3)  # quick way to find box number
    return(box)    
    '''This line can use some explaination - think of it like this: 
    rsval = (0,1,4,7)  #  0 is throw away placeholder; 1,4,7 are base row values
    rstack = (row-1)//3 +1  #  using floor division gives an index value in groups of 3
    csec = (col-1)//3  #  floor div gives val to add to row base
    box = rsval[rstack] + csec  # add the col idex value to the row index giving box 1-9   '''

boxnum = lambda row, col:((0,1,4,7)[(row-1)//3 + 1]) + ((col-1)//3)  #lambda is an unnamed inline function</span>

More to come, but in the meantime I have posted the complete challenge function set and tests on the next page plus my results on the page following that – sorry the formatting doesn’t transfer to WordPress.

Challenge #3 nums_in_a_row(row)
nums_in_a_row(row):           Produce a sorted list of the numbers used (given/guessed) in a row
For example, in our demo data, for row 6, only the numbers 8 and 6 are used. Get this one and the next is a piece of cake.  The numbers have to be sorted or in future comparisons dyslexic lists will not compare as equal.  In other words, [1,2] is not the same as [2,1].  For a given row just step through the columns, see if its values are greater than zero and if so, add it to a list you might call “rnums”.  It’s OK to have multiple values, we will deal with those later, but the list has to be sorted before you return it.

Here is one possible solution:

#  function to find single numbers in row(row)
def nums_in_a_row(row):
    rnums = []
    for col in range(1,10):
        if t_posted[row][col]>0:
            rnums.append(t_posted[row][col])
    return(sorted(rnums))</span>

Challenge #4 nums_in_a_col(col)
nums_in_a_col(col):              Produce a list of the numbers used (given/guessed) in a column
This is just a tiny variation of nums_in_a_row. Step through rows of a column instead of columns of a row. With the col variable passed in and “fixed” you only have to step through the rows and return a sorted list.

A possible solution:

#  function to find single numbers in col(col)
def nums_in_a_col(col):
    cnums = []
    for row in range(1,10):
        if t_posted[row][col] > 0:
            cnums.append(t_posted[row][col])
    return(sorted(cnums))</span>

Challenge #5    nums_in_a_box(row, col)
nums_in_a_box(row, col):     Produce a list of the numbers used (given/guessed) in a box
Dealing with boxes is a good bit more difficult than just rows or just columns. There are a lot of ways to approach this but by defining the box corners as a given we drastically reduce the effort. If you choose to use the box corner information given in a tuple of tuples  (resulting in my name toft_box_corners) you will still step through rows and columns but you can calculate the cells needed from the corner of the box. And you get the box from the function you have already written, so we have one function calling another. Don’t forget to return a sorted list.  I would love to see a better solution, but here is one to start with:

#  function to find single numbers in a box(b)
def nums_in_a_box(row, col):   # given any row and column
    boxnums = []
    box = box_number(row, col)
    for r in range(toft_box_corners[box][0], (toft_box_corners[box][0])+3):
        for c in range(toft_box_corners[box][1], (toft_box_corners[box][1])+3):
            if t_posted[r][c] > 0:
                boxnums.append(t_posted[r][c])
    s_boxnums = set(boxnums)
    return(sorted(list(s_boxnums)))

Challenge #6    cell_pv(row, col)
cell_pv(row, col): Produce a list of the values possible for a cell given row and column

You didn’t know it, perhaps, but you have already done the hard work in the last three functions finding the numbers in a row, column and box.  Remember it was OK to have duplicate numbers.  That is because we are going to use the properties of a set to remove them.  The first thing we have to do is not mess up our tuple of values that are given or guessed.  Look and see if the value for that location is greater than zero and if it is return that value.   If it isn’t, create a set called used_nums and put the values for that row in it by calling your function nums_in_a_row(row). Now update your set for the values in your column and box. The set will not allow any duplicate values.  So when that process is finished you have a list of all used numbers – exactly the opposite of what you want.  But in the given header we have a frozen set (fs_all_values) holding a single list of all possible values – i.e., the number 1 through 9.  One of the really powerful functions of a set is the “.difference” command that compares a set to another iterable – in this case our used_nums – and returns just the unrepresented values.  This, of course, would be the values still possible for the given row/col cell.  If you return no values you have an error but we are not concerned with error checking in this challenge example.  If you return only one value you could post that to t_posted as a guess if you want to – that also is not our problem at the moment. Convert these possible values to a sorted list and return it to complete this function.

Here is a possible solution:

def cell_pv(row, col):
    if t_posted[row][col] > 0:
        return([t_posted[row][col]])
    else:  #a set is ideal for this purpose, it does not allow duplicates
        used_nums = set(nums_in_a_row(row))       # make a place to put used numbers
        used_nums.update(nums_in_a_col(col))      # using a set eliminates duplicates
        used_nums.update(nums_in_a_box(row,col))  # from the row/col/box function but we
        return(sorted(list(fs_all_values.difference(used_nums)))) # must return a sorted list

Challenge #7       board_pv()
cell_pv(row, col):            Fill the lists in tuple t_all_pcv of possible values for each cel

You only have one small piece left to put in place to have a major requirement for any kind of game you might design around Sudoku. You have to have a function to fill or update our entire tuple of lists holding possible cell values (pcv) – which we assigned in the header “t_all_pcv”. The cells are logically accessed with [row][col] and when you are finished, return the updated t_all_pcv.

Here is a possible solution:

def board_pv():
    for row in range(1,10):
        for col in range(1,10):
            t_all_pcv[row][col] = cell_pv(row, col)
    return(t_all_pcv)

Challenge #8    print_pv_board()
print_pv_board() Produce a text version of the board showing possible values for all cells
Caveat: do NOT call board_pv to update values inside your function. You will need that list unmodified in challenge #10. This is just a print out of the values that Sudoku players have to have to make decisions about what to guess. Since this is a text display (we could do better in GUI) it will not be perfect but we will do the best we can. Here is another great opportunity to learn/review your critical f-string skills. The display should look something like this:

Here is a possible solution:

#  function to print possible values in cells by row and column
def print_pv_board():          #  Note: we do not call board_pv to update
    print("Columns",end="")    #  because it would erase any results from identifying and 
    for col in range(1,10):    #  removing impossible numbers revealed by matched pairs
        print((f"???---{ str(col):}---???").center(15),end="")
    print()                    #  this will NOT produce a neat alligned print out because
    for row in range(1,10):    #  there are highly variable lengths to the apv lists
        print(f"Row{str(row)}: ", end="")
        for col in range(1,10):
            x = str(t_all_pcv[row][col])
            print(f" | {x:^12}",end="")            
        print("|")
        print(" "*7 + "-"*135) 

Challenge #9    post(row, col, guess)
post(row, col, guess): Add a guessed value to a list in the t_posted tuple

The is the easiest challenge by far. All you have to do is take in the row, column and guess and put it in the right spot in t_posted().

Here is a possible solution:

def post(row, col, guess):
    t_posted[row][col]= guess

Challenge #10    mpair_cols(col) 

mpair_cols(col)                 Create a function to find and remove “impossible numbers” by finding matched pairs in a column

Nine was the easiest, so ten is by far the hardest. This challenge is to remove impossible numbers from a column. If you wanted to continue on your own, there would also be a need for functions to remove impossible number from rows and boxes as well as doing something with your potential discoveries and also discovering your useable results. First let’s define an “impossible number” so you will understand why this function might be needed. (Special note: some Sudoku puzzles cannot be solved without removing impossible numbers.) 

In a column (or row or box) if you have two cells holding matched pairs of numbers – two cells which both hold x and y and only x and y – then x and y must be in those cells only and can (actually sometimes must) be removed from consideration if they show up in other cells. An x or y found in any cell that is not one of the matched pair is an “impossible number”.

I do not like the solution that follows and I hope you can do better. Please let me know if you can! But along the way as you work with lists and sets I think you will need to have a global variable and learn to use clear(), sorted(), add(), append(), len(), count(), and pop(). That is a lot of Python iterable functions to learn and use in a short function! The general process will be to find any matched pairs and extract the numbers or, if there are not any, return an empty list. If we find a matched pair then we need to look through all the other cells (ignoring the matched pair cells) and remove the values from its list if present held in t_all_pcv, then return the “corrected” list of column values. Depending on what you want to happen in your game you will then decide how to use the information. Note that until we again call board_pv, your removals are maintained in t_all_pcv but not posted permanently in t_posted. The decision on how when and why to do that is up to you and your game. When board_pv is called, all your work is reset. That’s what it does.

Here is a possible solution:

#  COLUMN function to find & remove numbers in matched pairs
def mpair_cols(col):
    global to_remove
    to_remove=[]
    to_remove.clear()
    popped_list =[]
    pv_list = []
    for row in range(1,10):
        pv_list.append(t_all_pcv[row][col])
    # create "pairs" with a list comprehension - MUST sort pairs or comparisons will not work later
    pairs = [sorted(item) for item in pv_list if len(item) == 2]
    if len(pairs) < 2:  #  if 0 or 1 pair we can't have a matching set
        return []  # so return an empty list
    else:   #  ok, we know there are 2 to 8 pairs - whatever the length of pairs list
        s_nums2delete = set()   
        for x in range(len(pairs)):             #  for however many pairs we found
            if pairs.count(pairs[x]) == 2:      #  if there are two identical pairs
                s_nums2delete.add(pairs[x][0])  #  try to put each of it's values
                s_nums2delete.add(pairs[x][1])  #  in a set - the set will refuse duplicates
                to_remove =sorted(list(s_nums2delete))  #  convert the filtered set to a list
    if to_remove == []:                         #  if there are no values to remove we're done
        return[]
    else:
        for row in range(1,10):                 #  otherwise we need to remove them from t_all_pcv
            if to_remove == t_all_pcv[row][col]:  # except don't remove #'s from our matched pairs
                continue
            elif to_remove[0] in t_all_pcv[row][col]:  # look at zero and if it is there look at one
                popped_list.append(t_all_pcv[row][col].pop(t_all_pcv[row][col].index(to_remove[0])))
                if to_remove[1] in t_all_pcv[row][col]:
                    popped_list.append(t_all_pcv[row][col].pop(t_all_pcv[row][col].index(to_remove[1])))
            elif to_remove[1] in t_all_pcv[row][col]:  # otherwise just check out one
                popped_list.append(t_all_pcv[row][col].pop(t_all_pcv[row][col].index(to_remove[1])))
        return(popped_list)

That’s all she wrote unless someone has a really great reason to extend the challenges. You can always send your comments to: oakey.john@yahoo.com