Sudoku – All Challenges and Tests

#  SUDOKU CODE CHALLENGES - given to keep us all on the same page(s)
#  John Oakey ver: 120921
#  VALUES AND CONTAINERS for these challenges - the following lines are given:
#  An Example New Sudoku Board - list of rows with each element
#  of a row holding integers for given, or guessed values.  The
#  row elements are columns in order from zero to nine - r zero is not used
#  but is defined so that column numbers are logical to the programmer.

r0=[0,  0,0,0,0,0,0,0,0,0]  #  There are a lot of ways to structure this data, the most
                            #  efficient being a dictionary holding 10 elements,
r1=[0,  1,0,0,0,7,0,5,8,3]  #  with each element being a corresponding row with
r2=[0,  0,2,0,9,0,0,6,0,0]  #  it's data being a list of values by ordered column.
r3=[0,  0,8,0,0,3,1,0,2,0]  #  BUT... by doing it as at left it is VASTLY easier to
r4=[0,  0,0,0,4,1,0,0,0,0]  #  enter, check and visualize a puzzle.
r5=[0,  0,0,8,0,0,0,4,0,0]  #  We "throw away" r0 so when we want to access the first
r6=[0,  0,0,0,0,8,6,0,0,0]  #  row we can slice [1] instead of [0]
r7=[0,  0,7,0,5,9,0,0,6,0]  # <-- ok, this is an actual sudoku puzzle board
r8=[0,  0,0,3,0,0,7,0,9,0]
r9=[0,  9,1,5,0,6,0,0,0,7]

pvr0=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #  Note: we could create the
                                                  #  "pvr" (possible value row) lists of lists with:
pvr1=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #  bl = "=[[0],[0],[0],[0],[0],[0],[0],[0],[0],[0]]"
pvr2=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #  for i in range(10):
pvr3=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #      exec("pvr" + str(i) + bl)
pvr4=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #  but by printing it out as at left a beginner
pvr5=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  #  can better visualize the data structure
pvr6=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]  
pvr7=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]
pvr8=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]
pvr9=[[0],  [0],[0],[0],[0],[0],[0],[0],[0],[0]]

#  Create a tuple to hold given/guessed rows r0-r9 and one for our all possible value lists
t_posted = (r0,r1,r2,r3,r4,r5,r6,r7,r8,r9) # a tuple whose elements are lists
t_all_pcv = (pvr0,pvr1,pvr2,pvr3,pvr4,pvr5,pvr6,pvr7,pvr8,pvr9) # pvr = possible values row
pvlist = []  # a reuseable temporary list to hold possible values
fs_all_values = frozenset([1,2,3,4,5,6,7,8,9]) # all valid sudoku values in  frozen set,
#  now a tuple to hold 10 "sub-tuples" with stored box corners
toft_box_corners=((0,0),(1,1),(1,4),(1,7),(4,1),(4,4),(4,7),(7,1),(7,4),(7,7)) # toft = tuple of tuples

#  __________FUNCTIONS______________________________

#  function to print given/guessed Sudoku board data
def print_board():
    print("Columns",end = "")                 # first print the columns title
    for col in range(1,10):                 # then the header for each column
                                            #  for more on f string formatting see Big Daddy's
                                            #  Formatting Options toolbox
        print((f"***---{ str(col):}---***").center(15),end="")
    print()
    for row in range(1,10):                 # for every row
        print(f"Row{str(row)}: ", end="")   # we print the title and then
        for col in range(1,10):             # for every column in the row
            x = str(t_posted[row][col])     # we print the t_posted value
            print(f" | {x:^12}",end="")            
        print("|")
        print(" "*7 + "-"*135)

##
#  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)  
          
#  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

#-----
#  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))

#  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))

#  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)))
#-----

#  function to find POSSIBLE VALUES for a given row and column CELL
#  first find used values in row, col and box for a single r/c cell
#  then find inverse set using the .difference operation vs fs_all-values
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

#  function to compile ALL POSSIBLE VALUES in all cells of a board 
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)
#-----------

# function to post a guess
def post(row, col, guess):
    t_posted[row][col]= guess

#  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)

                     
#________________________TESTS_______________________________________________

# test1 printing board
print_board()
#------------
#  begin with a pair of "random" values to find for tests (box is calculated later):
row, col = 5,4
print("\n" + "Testing functions to find used numbers in row " +str(row) + ", col " + str(col) + ", and calculated box " + str(boxnum(row, col)))
print("the box number named function yeilds: "+ str(box_number(row,col)))  # this line if you are testing the named function
print("the box number from a lambda yeilds: "+ str(boxnum(row,col)))   # this line if you are testing the lambda version

# get given/single numbers in row r
rnums = nums_in_a_row(row)
print("row "+str(row) + ":  "+ str(rnums))

# get given/single numbers in col c
cnums = nums_in_a_col(col)
print("col " + str(col) + ": "+ str(cnums))

# get given/single numbers in row/col box
boxnums=[]
b = box_number(row, col)
boxnums = nums_in_a_box(row, col)
print ("box #" + str(b), end = "  ")
print("boxnums are: " + str(boxnums) + "\n")
#------------

# test getting possible values for a cell r,c
print("Test function to find possible values for a cell")
pvlist.clear()
pvlist = (cell_pv(row,col))
print("for row "+str(row)+ " and col "+str(col), end=":   ")
print(pvlist, end="  ")
print("This is the single value or the inverse of the list of combined row, col and box values.\n")
print("And here are all the possible values by row and column.\n")
board_pv() 
print_pv_board()

# test posting a value to the posted and given values in t_posted
print("\nTesting posting a value - but cheating a little to make it a value")
print("that will yeild a matching pair in column four.\n")
row, col, guess = 1, 4, 2
post(row, col, guess)
print("We have a new board of possible values. Note many 2's are gone - except our post.")
print("Notice the 3 & 7 pairs in column 4.  But an impossible 3 remains at R9C4,")
board_pv()
print_pv_board()

# testing finding and removing matched pair values from a column - this demo is for col 4 only
# Note this does not change the results you get when refreshing possible values with
# board_pv since that would destroy our "corrections" of removing impossible
# numbers with a call to mpair_cols (in practice you would also have mpair_rows and mpair_boxes).
print("\nNow testing mpair_cols to demo how this function could be critical to solving a puzzle.")
print("In this example test we are only testing col 4 - in practice we would test all columns.")
mpair_cols(4)
print("This call results in finding a matched pair of [3,7] which means 3's and 7's should be ")
print("removed from any non-matched-pair cells - they are 'impossible numbers'.\n")
print_pv_board()
print("Note that r9c4 used to have [3,8] but now shows only 8.  This revealed single could now be posted in t_posted.")
print("Depending on how you wanted your game to flow, you could let the user do this or you could write code or")
print("alter a function to remove any discovered single automatically.\n")

# test running mpair_cols on all columns
print("Now lets reset the possible values and run mpair_cols on all the columns")
board_pv()
for col in range(1,10):
    print("for col #"+str(col)+ " numbers to remove are: "+ str(mpair_cols(col)))
print()
print_pv_board()

print("\nSpecial Reminder - none of these impossible number removals are permanent!  If we wanted to lock in a single")
print("that is revealed by impossible numbers removed we would create code to compare the output of print_pv_board with")
print("the data output after board_pv is called (which would return us to our starting state) and post any 'new' singles.")