-1

When solving an 8 puzzle with a Manhatten distance heuristic I end up running into a state where there are no positions to move to because I have a list that contains all the previously visited nodes and makes sure no previously visited nodes are visited by comparing the possible nodes to visit to it before choosing one. Upon debugging I can confirm the 8 puzzle that is initially generated is solvable as it is shuffled, as well as the fact that the Manhatten distance is accurately calculated for each possible node before the next node is chosen based on the lowest Manhatten distance. Also, the issue is caused by the fact that the only possible nodes to visit from the empty space's position are in the list of already visited nodes which prevents them from being visited. How could this be resolved while still allowing for no previously visited nodes to be visited?

import random

from tkinter import *


all_node_contents = []
def input_validation(coordinates, user_input):
    global previous_coordinates   #stores the previous co ordinates of the blank space, so we know the co ordinates of where we need to switch the chosen number with each time. Its global so its not disgraded when the function terminates.
    global solved_puzzle            #checks i the puzzle is finished



    if coordinates [1] <0 or coordinates [0] <0 or coordinates [1] >2  or  coordinates [0] >2:    #checks if any of the passed in coordinates go over a certain range, which tells use if those co oridnate are on the grid. 
        pass
    
    elif (int(user_input) == int(frame.grid_slaves(coordinates[0], coordinates[1])[0]['text'])):  #if the number in one of the enterted co ordinates equals the number entered, switch the possitions of the blank space with the entered number block. 
    
        Label (frame, text = frame.grid_slaves(coordinates[0], coordinates[1])[0]['text']
               ).grid(row= previous_coordinates[0], column= previous_coordinates[1])
        
        Label (frame, text = "").grid(row= coordinates[0], column= coordinates[1])
        
        if puzzle_finished_checker() == True:       #checks if the puzzle if solved to print a message
            text_display.configure(text= "The puzzle has been solved", fg="red")
            previous_coordinates = [coordinates[0], coordinates[1]]
            solved_puzzle=True
            return
        
        else:
            previous_coordinates = [coordinates[0], coordinates[1]]    #stores the co cordinates for where the blank space is for the next time.
            text_display.configure(text= "Input the number you want to move into the empty space \n *make sure the number is next to the empty space*", fg="red")  
            return True

def possible_paths():
    global coordinates_up    #stored globally so they can be acsessed throughout each function
    global coordinates_left
    global coordinates_right
    global coordinates_down
     
    #Defines all the possible co ordinates around the blank space, some of these will be invalid unless the previous co ordinate was in the middle.
    coordinates_up = [previous_coordinates[0]-1, previous_coordinates[1]]   
    coordinates_left = [previous_coordinates[0], previous_coordinates[1]-1]
    coordinates_right = [previous_coordinates[0], previous_coordinates[1]+1]
    coordinates_down = [previous_coordinates[0]+1,previous_coordinates[1]]

def button_click():
    global text_display
    global previous_coordinates
    global solved_puzzle
    solved_puzzle = False

    possible_paths()
    
    if solved_puzzle == False:     
        
        #checks to see if one of the surrounding co ordiantes equals the number entered, if not an error is displayed.
        if (input_validation(coordinates_up, number_input.get()) == True):
            pass

        elif(input_validation(coordinates_left, number_input.get()) == True):
            pass

        elif(input_validation(coordinates_right, number_input.get()) == True):
            pass

        elif(input_validation(coordinates_down, number_input.get()) == True):
            pass            

        elif (solved_puzzle == False):
            text_display.configure(text="Please input a number that is surrounding the empty space")      
    else:
        pass
    
        
#generates and arranges all the GUI elements     
puzzle  = Tk()
puzzle.title("Eight Puzzle")
frame = Frame(puzzle)

frame.pack()

number_input= Entry(frame, width= 20)
number_input.grid(row = 5, column =7)


text_display = Label(frame, text="Input the number you want to move into the empty space \n *make sure the number is next to the empty space*", fg="red")
text_display.grid(row = 3, column = 7)

number = 1


#adds all the numbers in to all the blocks in the grid 
for y in range (0,3):
    for x in range (0,3):
        if number != 10:
            if number == 9:
                Label (frame, text = " ").grid(column=x, row=y)
            else:
                Label (frame, text = number).grid(column=x, row=y)
            number= number +1
    
directions=[-1,1]

space_coordinates = [2,2]

#randomly shuffles the puzzle
for _ in range (0,50):
    
    #generates a random index value to select either -1 or 1 to randomly generate either number, as this will be added to either the x y of the previous co ordinates to generate a random co ordinate to switc
    ran_direction = random.randint(0,1)
    ran_direction = directions[ran_direction]

    ran_x_or_y = random.randint(0,1)    #randomly generates an index value to select either x or y for the ran direction to be added to.
    
    num_test = space_coordinates[ran_x_or_y] + ran_direction    #create a test varible just to check if the edited x or y co irdinate either goes below 0 or above 2, so its not invalid.
    if (num_test>=0 and num_test<=2):
       
        previous_coordinates = []
        previous_coordinates.append(space_coordinates[0])
        previous_coordinates.append(space_coordinates[1])   #store the current co ordiantes in the previous co ordinates varible before the co ordinates are changed, to keep track of the co ordiantes of the empty space.
        space_coordinates[ran_x_or_y] = space_coordinates[ran_x_or_y] + ran_direction
            
        
        Label (frame, text = frame.grid_slaves(space_coordinates[0], space_coordinates[1])[0]['text']   #switch the previous corodiante space with the randomly generate co ordiante space.
               ).grid(row= previous_coordinates[0], column= previous_coordinates[1])
        
        Label (frame, text = "").grid(row= space_coordinates[0], column= space_coordinates[1])
         
    else:
        pass

nodes_coordinates = [1,2,3,4,5,6,7,8, ""]

correct_coordinates = [ [0,0], [1,0], [2,0], [0,1], [1,1], [2,1], [0,2], [1,2], [2,2]]
def puzzle_finished_checker():
    global position_count
    position_count = 0
    for i in range(0,3):
        for b in range(0,3):            #itterates over all the spaces in the grid 
            current_node = frame.grid_slaves(i, b)[0]['text']
            current_coordinates = [i, b]
            
            if (current_coordinates == correct_coordinates[nodes_coordinates.index(current_node)]):    #checks if the co ordiantes we are on equal the co ordiantes in the correct co ordiantes list from the index value of the current value (the value from the current co ordiantes palce we are one) in the nodes co ordiante list.
                position_count+=1
            else:
                pass
    if position_count == 9:   
        return True
    else:
        return False
            
            
#totals the amount of space each number will need to move from where it is to be solved

def manhatten_distance_calc():
    manhatten_dist_sum = 0
    for y in range(0,3):
        for x in range(0,3):            
            current_node = frame.grid_slaves( y,x)[0]['text']
            current_coordinates = [x, y]
            
            desired_coordinate = correct_coordinates[nodes_coordinates.index(current_node)]
            manhatten_dist_sum += (abs(desired_coordinate[0] - current_coordinates[0]) + abs(desired_coordinate[1] - current_coordinates[1]))
            
    return manhatten_dist_sum
        
   
def puzzle_solve():
    global previous_coordinates
    global count
    global next_node
    global all_node_contents
    
    count = 0
    def path_checker (coordinates):
        global count
        global new_space
        node_been_used = False
        
        current_visited_node = []
        for i in range(0,3):
            for b in range(0,3):
                current_node = frame.grid_slaves(i, b)[0]['text']
                current_visited_node.append(current_node)
                

        
        if coordinates [0] <0 or coordinates [1] <0 or coordinates [0] >2 or coordinates [1] >2:
            return "null"                
        else:
            # here we reverse what we previously did to the grid below, when working out the next grid.
        
            if (count > 0):
                            
                
                Label (frame, text = frame.grid_slaves(previous_coordinates[0], previous_coordinates[1])[0]['text']
                       ).grid(row= new_space[0], column= new_space[1])
                
                Label (frame, text = "").grid(row= previous_coordinates[0], column= previous_coordinates[1])
                puzzle.update()
                
                
                Label (frame, text = frame.grid_slaves(coordinates[0], coordinates[1])[0]['text']
                       ).grid(row= previous_coordinates[0], column= previous_coordinates[1])
                
                Label (frame, text = "").grid(row= coordinates[0], column= coordinates[1])
                new_space = [coordinates[0], coordinates[1]]
                

            else:
                count+= 1
            
                
                Label (frame, text = frame.grid_slaves(coordinates[0], coordinates[1])[0]['text']
                       ).grid(row= previous_coordinates[0], column= previous_coordinates[1])
                
                Label (frame, text = "").grid(row= coordinates[0], column= coordinates[1])
                
                new_space = [coordinates[0], coordinates[1]]

            
            current_visited_node = []
            for i in range(0,3):
                for b in range(0,3):
                    current_node = frame.grid_slaves(i, b)[0]['text']
                    current_visited_node.append(current_node)
                    
                if current_visited_node in all_node_contents:
                    node_been_used = True
                
                if node_been_used == True:
                    return "null"
        
    possible_paths()
    path1 = path_checker(coordinates_up)
    path2 = path_checker(coordinates_left)
    path3 = path_checker(coordinates_right)
    path4 = path_checker(coordinates_down)
   
    possible_nodes = [path1, path2, path3, path4]
    
    ##RESETS THE GRID
    Label (frame, text = frame.grid_slaves(previous_coordinates[0], previous_coordinates[1])[0]['text']
           ).grid(row= new_space[0], column= new_space[1])
    
    Label (frame, text = "").grid(row= previous_coordinates[0], column= previous_coordinates[1])

    node_manhatten_distances =[]
    
    for i in range (len(possible_nodes)):        
        if possible_nodes[i] == "null":
            node_manhatten_distances.append(100)
        else:
            node_manhatten_distances.append(possible_nodes[i][0])
            
            
    next_node = possible_nodes[node_manhatten_distances.index(min(node_manhatten_distances))][1]
    
    print(possible_nodes)
    
    Label (frame, text = frame.grid_slaves(next_node[0], next_node[1])[0]['text']
           ).grid(row= previous_coordinates[0], column= previous_coordinates[1])
   
    Label (frame, text = "").grid(row= next_node[0], column= next_node[1])
    
    previous_coordinates = next_node

    visited_nodes = []
    
    for i in range(0,3):
        for b in range(0,3):
            current_node = frame.grid_slaves(i, b)[0]['text']
            visited_nodes.append(current_node)
            
    all_node_contents.append(visited_nodes)
    print(all_node_contents) 
     
    if puzzle_finished_checker() == True:
        text_display.configure(text= "The puzzle has been solved", fg="red")
        return True
    
def puzzle_solve_button():
    while puzzle_solve() != True:
        if puzzle_solve() == True:
            break

button = Button(frame, text="Enter", command = button_click)
button.grid(row = 6, column = 7)
solve = Button(frame, text="Solve", command = puzzle_solve_button)
solve.grid(row = 7, column = 7)

previous_coordinates = []

previous_coordinates.append(space_coordinates[0])
previous_coordinates.append(space_coordinates[1])
puzzle.mainloop()

taybrie
  • 41
  • 5
  • 1
    Can you please simplify your code? Also PEP8 says that you should keep your code bellow 80 characters per line. Scrolling horizontally is annoying most of the time. – TheLizzard Apr 20 '21 at 20:24
  • @TheLizzard It all plays a part – taybrie Apr 20 '21 at 20:30
  • `path_checker` does not return anything when it succeeds. What are you expecting? Coordinates? And why do you update the UI in `path_checker`? That's surely not the right spot. – Tim Roberts Apr 20 '21 at 20:53
  • `puzzle_solve_button` forces your solution to be done twice. Just do `while not puzzle_solve():` / `pass`. – Tim Roberts Apr 20 '21 at 20:54
  • Globals are evil. You should be passing your coordinates as parameters, not globals. – Tim Roberts Apr 20 '21 at 20:55
  • OK, I added `return 1, coordinates` at the end of `path_checker`, and now it does frantically try different moves. You do realize that EVERY possible move has a Manhatten distance of 1, right? – Tim Roberts Apr 20 '21 at 21:09
  • @TimRoberts: You've put in a bunch effort -- you should create an answer and get some up-votes! ;-) – Ethan Furman Apr 20 '21 at 21:10

1 Answers1

1

OK, I've cleaned up a number of things here. It does frantically attempt to solve the puzzle, but it eventually stops with "no possible moves". I've eliminated some but not all of the globals. Note that you don't really want to create a new label widget each time; just update the text attribute of the ones that are there.

Here's revision 3, which now correctly detects when the user solves the puzzle.

Here's revision 4, which uses a better method of encoding the current state.

Your fundamental problem is that you're not backtracking. At each step, you have up to four possible moves. You just pick one, but if that leads to a dead-end, you don't go back and try one of the others. That's going to require more reworking.

import random

from tkinter import *


all_node_contents = []
def input_validation(coordinates, user_input):
    global previous_coordinates   #stores the previous co ordinates of the blank space, so we know the co ordinates of where we need to switch the chosen number with each time. Its global so its not disgraded when the function terminates.
    global solved_puzzle            #checks i the puzzle is finished


    if coordinates [1] <0 or coordinates [0] <0 or coordinates [1] >2  or  coordinates [0] >2:    #checks if any of the passed in coordinates go over a certain range, which tells use if those co ordinate are on the grid.
        pass

    elif (int(user_input) == int(frame.grid_slaves(*coordinates)[0]['text'])):  #if the number in one of the entered co ordinates equals the number entered, switch the positions of the blank space with the entered number block.

        frame.grid_slaves(*previous_coordinates)[0]['text'] = frame.grid_slaves(*coordinates)[0]['text']
        frame.grid_slaves(*coordinates)[0]['text'] = ""

        previous_coordinates = coordinates[:]
        if puzzle_finished_checker() == True:       #checks if the puzzle if solved to print a message
            text_display.configure(text= "The puzzle has been solved", fg="red")
            solved_puzzle=True
            return
        else:
            text_display.configure(text= "Input the number you want to move into the empty space \n *make sure the number is next to the empty space*", fg="red")
            return True

def possible_paths(was):
    #Defines all the possible co ordinates around the blank space, some of these will be invalid unless the previous co ordinate was in the middle.
    return  (
        [was[0]-1, was[1]] ,
        [was[0],   was[1]-1],
        [was[0],   was[1]+1],
        [was[0]+1, was[1]]
    )

def button_click():
    global text_display
    global previous_coordinates
    global solved_puzzle

    if solved_puzzle:
        return
    paths = possible_paths(previous_coordinates)

    #checks to see if one of the surrounding co ordinates equals the number entered, if not an error is displayed.
    n = number_input.get()
    if not any( input_validation(i,n) for i in paths ):
        text_display.configure(text="Please input a number that is surrounding the empty space")
    number_input.delete(0)
    if puzzle_finished_checker():
        text_display.configure(text="Puzzle is solved!")



#generates and arranges all the GUI elements
puzzle  = Tk()
puzzle.title("Eight Puzzle")
frame = Frame(puzzle)

frame.pack()

number_input= Entry(frame, width= 20)
number_input.grid(row = 5, column =7)


text_display = Label(frame, text="Input the number you want to move into the empty space \n *make sure the number is next to the empty space*", fg="red")
text_display.grid(row = 3, column = 7)

#adds all the numbers in to all the blocks in the grid
for y in range (0,3):
    for x in range (0,3):
        if x==2 and y==2:
            Label (frame, text = " ").grid(column=x, row=y)
        else:
            Label (frame, text = str(y*3+x+1)).grid(column=x, row=y)

directions=[-1,1]

space_coordinates = [2,2]

#randomly shuffles the puzzle
for _ in range(50):

    #generates a random index value to select either -1 or 1 to randomly generate either number, as this will be added to either the x y of the previous co ordinates to generate a random co ordinate to switch
    ran_direction = random.randint(0,1)
    ran_direction = directions[ran_direction]

    ran_x_or_y = random.randint(0,1)    #randomly generates an index value to select either x or y for the ran direction to be added to.

    num_test = space_coordinates[ran_x_or_y] + ran_direction    #create a test variable just to check if the edited x or y coordinate either goes below 0 or above 2, so its not invalid.
    if 0 <= num_test <=2:

        previous_coordinates = space_coordinates[:]
        space_coordinates[ran_x_or_y] = space_coordinates[ran_x_or_y] + ran_direction

        frame.grid_slaves(*previous_coordinates)[0]['text'] = frame.grid_slaves(*space_coordinates)[0]['text']
        frame.grid_slaves(*space_coordinates)[0]['text'] = ""

def get_state():
    vals = ''
    for i in range(0,3):
        for b in range(0,3):            #iterates over all the spaces in the grid
            vals += frame.grid_slaves(i, b)[0]['text']
    return vals

def puzzle_finished_checker():
    return get_state() == "12345678 "

#totals the amount of space each number will need to move from where it is to be solved

nodes_coordinates = "12345678 "
correct_coordinates = [ [0,0], [1,0], [2,0], [0,1], [1,1], [2,1], [0,2], [1,2], [2,2]]

def manhatten_distance_calc():
    manhatten_dist_sum = 0
    for y in range(0,3):
        for x in range(0,3):
            current_node = frame.grid_slaves( y,x)[0]['text']
            current_coordinates = [x, y]

            desired_coordinate = correct_coordinates[nodes_coordinates.index(current_node)]
            manhatten_dist_sum += (abs(desired_coordinate[0] - current_coordinates[0]) + abs(desired_coordinate[1] - current_coordinates[1]))

    return manhatten_dist_sum


def puzzle_solve():
    global previous_coordinates
    global count
    global next_node
    global all_node_contents

    count = 0
    def path_checker (coordinates):
        global count
        global new_space
        node_been_used = False

        if coordinates [0] <0 or coordinates [1] <0 or coordinates [0] >2 or coordinates [1] >2:
            return "null"

        # here we reverse what we previously did to the grid below, when working out the next grid.

        if count:
            frame.grid_slaves(*new_space)[0]['text'] = frame.grid_slaves(*previous_coordinates)[0]['text']
            frame.grid_slaves(*previous_coordinates)[0]['text'] = ' '
            puzzle.update()

        else:
            count+= 1

        frame.grid_slaves(*previous_coordinates)[0]['text'] = frame.grid_slaves(*coordinates)[0]['text'] 
        frame.grid_slaves(*coordinates)[0]['text'] = ' '

        new_space = coordinates[:]

        if get_state() in all_node_contents:
            node_been_used = True
            return "null"

        return 1, coordinates

    possible_nodes = [path_checker(i) for i in possible_paths(previous_coordinates)]

    ##RESETS THE GRID
    frame.grid_slaves(*new_space)[0]['text'] = frame.grid_slaves(*previous_coordinates)[0]['text'];
    frame.grid_slaves(*previous_coordinates)[0]['text'] = ' '

    node_manhatten_distances = [100 if n == 'null' else n[0] for n in possible_nodes]

    next_node = possible_nodes[node_manhatten_distances.index(min(node_manhatten_distances))][1]

    if not isinstance(next_node,list):
        print("No possible moves???")
        print(possible_nodes, node_manhatten_distances)
        return True

    frame.grid_slaves(*previous_coordinates)[0]['text'] = frame.grid_slaves(*next_node)[0]['text']
    frame.grid_slaves(*next_node)[0]['text'] = ' '

    previous_coordinates = next_node

    all_node_contents.append(get_state())

    if puzzle_finished_checker():
        text_display.configure(text= "The puzzle has been solved", fg="red")
        return True

def puzzle_solve_button():
    while not puzzle_solve():
        pass

solved_puzzle = False
button = Button(frame, text="Enter", command = button_click)
button.grid(row = 6, column = 7)
solve = Button(frame, text="Solve", command = puzzle_solve_button)
solve.grid(row = 7, column = 7)

previous_coordinates = space_coordinates[:]
puzzle.mainloop()
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Tim Roberts
  • 48,973
  • 4
  • 21
  • 30
  • for backtracking would i just set the node as the first node in the visited node list when all pathways equal null ? – taybrie Apr 21 '21 at 16:01
  • Well, it's more than "the node". Every cell is going to have 2, 3, or 4 potential moves. What you need to do is have a queue/list of "untried potential moves". Each of those will generate more "untried potential moves" You keep chase the top one until you hit a dead end, then you do the next one. You had better hit a solution before the queue goes empty. I'll try to post an answer with this later today. – Tim Roberts Apr 21 '21 at 17:16
  • It's too bad this was closed. Here is a version that does backtracking. https://gist.github.com/timrprobocom/c930d70c113bcf3f1baa2ba79bd5bff9 You stored the "master state" in the Label controls; I'm storing the state in an object, and copying to the controls when needed. This took about 21,000 tries to solve it in my first attempt, which took several minutes. – Tim Roberts Apr 21 '21 at 18:14
  • Ok, thanks so much for the help, so just to make sure I am clear on this, for every node that is visited based on the best Manhatten distance I keep the untried potential moves that weren't visited in a queue so once we hit a dead end I will set the master state to the first node in the queue and then keep back tracking through the queue till we reach the final state ? – taybrie Apr 21 '21 at 19:12
  • Right. Was it your intention to choose, amongst the alternatives, the choice that was already closest to its destination? Because that part is missing from your code. – Tim Roberts Apr 21 '21 at 19:16
  • the Manhatten distance heuristic ?, because that's working in the code – taybrie Apr 21 '21 at 19:30
  • As I pointed out originally, the `path_checker` function in the code you posted does not return anything when it succeeds. That's where you would return a checked path and its Manhatten distance, but that line is not present. – Tim Roberts Apr 21 '21 at 19:54
  • but there is a line that states return [manhatten_distance_calc(), coordinates] in the path checker function – taybrie Apr 21 '21 at 19:59
  • No, there isn't. Go look. It's not there. – Tim Roberts Apr 21 '21 at 20:00
  • By the way, I've updated my gist to include a "replay" button that plays back the discovered solution slowly, so you can watch it. – Tim Roberts Apr 21 '21 at 21:50
  • oh there is on my one i have, must have copied it wrong. – taybrie Apr 22 '21 at 18:12
  • I am having a bit of trouble when resetting the master node to the node stored in the queue once we reach a dead-end, as its setting it correctly but it has an extra blank space. Here is the code ` if possible_nodes == ["null", "null", "null", "null"]: backtracked_node = untried_nodes.popleft() print(untried_nodes) number = 0 for y in range (0,3): for x in range (0,3): if number < 9: Label (frame, text = backtracked_node[number]).grid(column=x, row=y) number= number +1` – taybrie Apr 23 '21 at 07:45
  • i am essentially trying to change all the squares in the current displayed eight puzzle to the first popped off item from the queue. – taybrie Apr 23 '21 at 07:47
  • You should probably post a new question with your new code. – Tim Roberts Apr 23 '21 at 16:41