pythonalgorithmpath-finding

A* algorithm can't find the goal in Python


I'm still new to Python and this my first ever question on stackoverflow and I've been having trouble for a week to implement an A* algorithm.

The code I've got finds a goal with a straight wall but as you will see, as soon as I extend the wall below the start point and it has to go backwards or around it, it starts looping forever.

I've been banging my head against the wall trying to fix it and how to implement a raise code so it stops looping. Any help will be very much appreciated.

My code:

class Node:
    """A node class for A* Pathfinding"""

    def __init__(self, parent=None, position=None):
        self.parent = parent
        self.position = position

        self.g = 0
        self.h = 0
        self.f = 0

    def __eq__(self, other):
        return self.position == other.position


def astar(maze, start, end):
    """Returns a list of tuples as a path from the given start to the given end in the given maze"""

    # Create start and end node
    start_node = Node(None, start)
    start_node.g = start_node.h = start_node.f = 0
    end_node = Node(None, end)
    end_node.g = end_node.h = end_node.f = 0

    # Initialize both open and closed list
    open_list = []
    closed_list = []

    # Add the start node
    open_list.append(start_node)

    # Loop until you find the end
    while len(open_list) > 0:

        # Get the current node
        current_node = open_list[0]
        current_index = 0
        for index, item in enumerate(open_list):
            if item.f < current_node.f:
                current_node = item
                current_index = index

        # Pop current off open list, add to closed list
        open_list.pop(current_index)
        closed_list.append(current_node)

        # Found the goal
        if current_node == end_node:
            path = []
            current = current_node
            while current is not None:
                path.append(current.position)
                current = current.parent
            return path[::-1]  # Return reversed path

        # Generate children
        children = []
        for new_position in [(0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]:  # Adjacent squares

            # Get node position
            node_position = (current_node.position[0] + new_position[0], current_node.position[1] + new_position[1])

            # Make sure within range
            if node_position[0] > (len(maze) - 1) or node_position[0] < 0 or node_position[1] > (
                    len(maze[len(maze) - 1]) - 1) or node_position[1] < 0:
                continue

            # Make sure walkable terrain
            if maze[node_position[0]][node_position[1]] != 0:
                print("node -", node_position[0],node_position[1], "is blocked")
                continue

            # Create new node
            new_node = Node(current_node, node_position)

            # Append
            children.append(new_node)

        # Loop through children
        for child in children:

            # Child is on the closed list
            for closed_child in closed_list:
                if child == closed_child:
                    continue

            # Create the f, g, and h values
            child.g = current_node.g + 1
            child.h = ((child.position[0] - end_node.position[0]) ** 2) + (
                        (child.position[1] - end_node.position[1]) ** 2)
            child.f = child.g + child.h

            # Child is already in the open list
            for open_node in open_list:
                if child == open_node and child.g > open_node.g:
                    continue

            # Add the child to the open list
            open_list.append(child)


def main():
    maze = [[0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 1, 1, 1, 1, 1, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

    start = (0, 0)
    end = (1, 8)

    path = astar(maze, start, end)
    print(path)


main()

Solution

  • You have these two pieces:

    # Child is on the closed list
    for closed_child in closed_list:
        if child == closed_child:
            continue
    
    # Child is already in the open list
    for open_node in open_list:
        if child == open_node and child.g > open_node.g:
            continue
    

    The continue will continue the inner for loop. Consequently, it does not have any effect. You are rather looking for something like this:

    # Child is on the closed list
    is_in_closed = False
    for closed_child in closed_list:
        if child == closed_child:
            is_in_closed = True
            break
    if is_in_closed:
        continue
    
    # Child is already in the open list
    is_in_open = False
    for open_node in open_list:
        if child == open_node and child.g > open_node.g:
            is_in_open = True
            break
    if is_in_open:
        continue
    

    Two more comments:

    Your distance measure only counts the number of steps. Therefore, a diagonal is as expensive as a horizontal / vertical step. You might want to change this and get the actual length or an approximation of it to find the truly shortest path.

    Your heuristic is the squared distance to the target. This is not an admissible heuristic because it over-estimates the actual cost to the target. As a result, you might not find the correct shortest path. For your cost function (number of steps), a better and admissible heuristic would be max(child.position[0] - end_node.position[0], child.position[1] - end_node.position[1]) (you need at least this number of steps to get to the target).