Search code examples
pythona-starpuzzlesliding-tile-puzzlestate-space

N-puzzle problem using A-star search algorithm


I am making a n-puzzle solver for my Artificial Intelligence class using A* in Python. The problem I have with my solution is , that the solve_puzzle() is not working as it should. While searching through the nodes it gets just deeper and deeper, but it never ends, the depth goes into really high numbers (depth of some child node is 5k for example).

I think it gets stuck in some loop and it keeps just looping through the same nodes (That is just what I think, I am not sure about that). I don't really know, I tried 3 different A* solutions, but the result is the same - the search loop never ends and never reaches the Goal.

I can solve very primitive puzzle like
start -> 0, 1, 2, 3, 4, 5, 6, 7, 8
goal -> 1, 2, 0, 3, 4, 5, 6, 7, 8 (just two moves to left are made)
but for just a little more complex puzzle like
start = 0, 1, 2, 3, 4, 5, 6, 7, 8
goal = 1, 2, 5, 0, 3, 4, 6, 7, 8 it is a never ending search.

I move the tile with number, not the blank tile.

Below is the whole code:

'''
class Node():

    def __init__(self, n_size, m_size, current_state, goal_state, choosen_heuristic, parent):
        self.n_size = n_size
        self.m_size = m_size
        self.dimension = self.n_size * self.m_size
        self.current_state = current_state
        self.goal_state = goal_state
        self.choosen_heuristic = choosen_heuristic
        self.parent = parent
        self.child = [None, None, None, None]
        self.last_operator = None
        self.heuristic_cost = 0
        self.depth = 0
        self.priority = self.depth + self.heuristic_cost

    def check_if_goal_is_reached(self):
        if (self.heuristic_cost == 0):
            print("GOAL IS REACHED!")
            exit(1)                                         #for now
            #return

    def get_blank_tile_position(self):
        blank_position = 0
        for i in range(self.dimension):
            if (self.current_state[i] == 0):
                blank_position = i
        return blank_position

    def misplaced_tiles_heuristic(self):
        misplaced_sum = 0
        for i in range(self.dimension):
            if self.current_state[i] != self.goal_state[i]:
                misplaced_sum += 1
        #print("Count of misplaced tiless in this node is : ", misplaced_sum)
        self.heuristic_cost = "misplaced"
        self.heuristic_cost = misplaced_sum
        self.check_if_goal_is_reached()
        return misplaced_sum

    def manhattan_distance(self):
        distance_sum = 0
        for i in range(self.dimension):
            current_x = self.current_state[i] % self.n_size
            goal_x = self.goal_state[i] % self.n_size
            current_y = self.current_state[i] // self.m_size
            goal_y = self.goal_state[i] // self.m_size
            distance_sum += abs(current_x - goal_x) + abs(current_y - goal_y)
        #print("Sum of Manhattan distance for this node is : ", distance_sum)
        self.heuristic_cost = "manhattan"
        #print("Hĺbka tohto uzla : ", self.depth)
        self.check_if_goal_is_reached()
        return distance_sum

    def generate_children(self, choosen_heuristic):
        possible_directions = []
        current_node_blank_position = self.get_blank_tile_position()
        # UP - I move a tile with number on it, not the blank tile
        if current_node_blank_position < (self.dimension - self.n_size):
            self.child[0] = Node(self.n_size, self.m_size, self.current_state, self.goal_state, self.choosen_heuristic, self.current_state)
            self.child[0] = copy.deepcopy(self)
            self.child[0].parent = self.current_state
            self.child[0].last_operator = "UP"
            self.child[0].depth = self.depth + 1

            new_blank_position = current_node_blank_position + self.m_size

            temp = self.child[0].current_state[current_node_blank_position]
            self.child[0].current_state[current_node_blank_position] =  self.child[0].current_state[new_blank_position]
            self.child[0].current_state[new_blank_position] = temp

            if choosen_heuristic == "misplaced":
                self.child[0].misplaced_tiles_heuristic()
            if choosen_heuristic == "manhattan":
                self.child[0].manhattan_distance()

            possible_directions.append("UP")
            print("Depth of this node is : : ", self.child[0].depth)
        else:           
            self.child[0] = None
        # DOWN - I move a tile with number on it, not the blank tile
        if current_node_blank_position > (self.n_size - 1):
            self.child[1] = Node(self.n_size, self.m_size, self.current_state, self.goal_state, self.choosen_heuristic, self.current_state)
            self.child[1] = copy.deepcopy(self)
            self.child[1].parent = self.current_state
            self.child[1].last_operator = "DOWN"
            self.child[1].depth = self.depth + 1

            new_blank_position = current_node_blank_position - self.m_size

            temp = self.child[1].current_state[current_node_blank_position]
            self.child[1].current_state[current_node_blank_position] =  self.child[1].current_state[new_blank_position]
            self.child[1].current_state[new_blank_position] = temp

            if choosen_heuristic == "misplaced":
                self.child[1].misplaced_tiles_heuristic()
            if choosen_heuristic == "manhattan":
                self.child[1].manhattan_distance()

            possible_directions.append("DOWN")
            #print("Depth of this node is : : ", self.child[1].depth)
        else:           
            self.child[1] = None
        # RIGHT - I move a tile with number on it, not the blank tile
        if (current_node_blank_position + self.n_size) % self.m_size:
            self.child[2] = Node(self.n_size, self.m_size, self.current_state, self.goal_state, self.choosen_heuristic, self.current_state)
            self.child[2] = copy.deepcopy(self)
            self.child[2].parent = self.current_state
            self.child[2].last_operator = "RIGHT"
            self.child[2].depth = self.depth + 1

            new_blank_position = current_node_blank_position - 1

            temp = self.child[2].current_state[current_node_blank_position]
            self.child[2].current_state[current_node_blank_position] =  self.child[2].current_state[new_blank_position]
            self.child[2].current_state[new_blank_position] = temp

            if choosen_heuristic == "misplaced":
                self.child[2].misplaced_tiles_heuristic()
            if choosen_heuristic == "manhattan":
                self.child[2].manhattan_distance()

            possible_directions.append("RIGHT")
            #print("Depth of this node is : : ", self.child[2].depth)
        else:           
            self.child[2] = None
        # LEFT - I move a tile with number on it, not the blank tile
        if (current_node_blank_position + 1) % self.m_size:
            self.child[3] = Node(self.n_size, self.m_size, self.current_state, self.goal_state, self.choosen_heuristic, self.current_state)
            self.child[3] = copy.deepcopy(self)
            self.child[3].parent = self.current_state
            self.child[3].last_operator = "LEFT"
            self.child[3].depth = self.depth + 1

            new_blank_position = current_node_blank_position + 1

            temp = self.child[3].current_state[current_node_blank_position]
            self.child[3].current_state[current_node_blank_position] =  self.child[3].current_state[new_blank_position]
            self.child[3].current_state[new_blank_position] = temp

            if choosen_heuristic == "misplaced":
                self.child[3].misplaced_tiles_heuristic()
            if choosen_heuristic == "manhattan":
                self.child[3].manhattan_distance()

            possible_directions.append("LEFT")
            #print("Depth of this node is : ", self.child[3].depth)
        else:           
            self.child[3] = None

        #print("From this node (the parent node) I can move to -> ", possible_directions)


def get_node_priority(node):
    return node.priority

def solve_puzzle(n_size, m_size, current_state, goal_state, choosen_heuristic):
    open_list = []
    closed_list = []
    init_node_parent = None
    init_node = Node(n_size, m_size, current_state, goal_state, choosen_heuristic, init_node_parent)
    open_list.append(init_node)

    while len(open_list) != 0:
        if (len(open_list) == 0):
            print("Fail - solution not found !")
        else:
            node = open_list.pop(0)

            if (node.parent != None):
                node.check_if_goal_is_reached()

            node.generate_children(choosen_heuristic)
            closed_list.append(node)

            temp_list = []
            for i in range(4):
                if node.child[i] != None:
                    temp_list.insert(0, node.child[i])
            sorted_temp_list = sorted(temp_list, key=get_node_priority, reverse=True)
            for x in range(len(sorted_temp_list)):
                if sorted_temp_list[x] != None:
                    open_list.insert(0, sorted_temp_list[x])

def print_current_node(current_node):
    puzzle = ""
    if current_node is None:
        puzzle = ""
        print(puzzle)
    else:
        puzzle = ""
        count_lines = 0
        for i in range(current_node.dimension):
            puzzle +=  (str(current_node.current_state[i]) + " ")
            count_lines += 1
            if (count_lines % current_node.m_size == 0):
                puzzle += "\n"
        print(puzzle)


def main():

    ###################
    #   Static Data   #
    ###################
    # static for now, later I will let user to choose
    n_size = 3
    m_size = 3
    current_state = [0, 1, 2, 3, 4, 5, 6, 7, 8]
    goal_state = [8, 0, 6, 5, 4, 7, 2, 3, 1]
    choosen_heuristic = "manhattan"
    start_time = time.process_time()
    solve_puzzle(n_size, m_size, current_state, goal_state, choosen_heuristic)
    end_time = time.process_time()
    search_time = end_time - start_time
    print("Solved in : ", search_time, "seconds.")

main()
'''

Solution

  • Make sure you test with a solvable puzzle. Here are some solvable tests:

        goal:     {1,2,3,4,5,6,7,8,0}
        6 steps:  {1,3,5,4,0,2,7,8,6}
        18 steps: {1,4,0,5,2,8,7,6,3}
        26 steps: {2,1,7,5,0,8,3,4,6}
        27 steps: {8,5,3,4,7,0,6,1,2}
        28 steps: {0,6,7,3,8,5,4,2,1}
        30 steps: {5,7,0,4,6,8,1,2,3}
        31 steps: {8,6,7,2,5,4,3,0,1}  (highest number of steps possible for 3x3 )