GarrickW
GarrickW

Reputation: 217

Python A* algorithm not searching appropriately

So I'm trying to write a Python implementation of the A* algorithm. My algorithm finds the path to the target without trouble, but when I get the program to visualize the closed and open lists, I notice that the closed list, whenever the obstacles are a little complicated, will balloon into a large, perfect diamond shape. In other words, my algorithm is adding nodes to the closed list that are significantly further from the target than any of the neighbors of the "expected" closed list should be.

To illustrate, when a point in the closed list is (2, 1) away from the target, but a wall is blocking the way, the algorithm will add both the node at (2, 2) away from the target (to try and get over the wall) and the algorithm at (3, 1) away from the target... for no good reason, since it's clearly further.

I'm not sure what I'm doing wrong. This distance calculation is meant to use the "Manhattan method" (imperfect for my purposes, but it shouldn't be causing such problems), but other methods continue to offer the same problem (or indeed worse problems).

def distance(self, tile1, tile2):
    self.xDist = abs(tile1.col * TILE_SIZE - tile2.col * TILE_SIZE)
    self.yDist = abs(tile1.row * TILE_SIZE - tile2.row * TILE_SIZE)
    self.totalDist = self.straightCost * (self.xDist + self.yDist)
    return self.totalDist

The code for filling the closed list looks like this. Here, .score2 is the H value, calculated using the distance method shown above.

    while self.endTile not in self.openList:
        self.smallestScore = MAX_DIST * 50
        self.bestTile = None
        for tile in self.openList:
            if tile.score[2] <= self.smallestScore:
                self.bestTile = tile
                self.smallestScore = tile.score[2]
        self.examine(self.bestTile)

The "examine" function adds the examined tile to the closed list and its viable neighbors to the open list, and seems to be working fine. The algorithm appears to be admitting all tiles with an H score of X (where X varies depending on the complexity of the maze the target is set in).

Slowing it down to a node-by-node visualization basically reveals that it is filling in an area of size X, and finds the path when the entrance into the maze is hit by the fill. I really have no clue what I'm doing wrong, and I've been trying to puzzle this out for two days now.

EDIT: in the interest of solving the problem, here is a fuller extract of code. This is the examine() function:

def examine(self, tile):
    #Add the tile to the closed list, color it in, remove it from open list.
    self.closedList.append(tile)
    tile.color = CLOSED
    pygame.draw.rect(windowSurface, tile.color, tile.rect)
    pygame.display.update(tile.rect)
    self.openList.remove(tile)
    #Search all neighbors.
    for a, b in ((tile.col + 1, tile.row), (tile.col - 1, tile.row),
                 (tile.col + 1, tile.row + 1), (tile.col + 1, tile.row - 1),
                 (tile.col - 1, tile.row + 1), (tile.col - 1, tile.row - 1),
                 (tile.col, tile.row + 1), (tile.col, tile.row - 1)):
        #Ignore if out of range.
        if a not in range(GRID_WIDTH) or b not in range(GRID_HEIGHT):
            pass
        #If the neighbor is pathable, add it to the open list.
        elif self.tileMap[b][a].pathable and self.tileMap[b][a] not in self.openList and self.tileMap[b][a] not in self.closedList:
            self.where = abs((a - tile.col)) + abs((b - tile.row))
            if self.where == 0 or self.where == 2:
                self.G = tile.score[1] + self.diagCost
                self.H = self.distance(self.endTile, self.tileMap[b][a])
                self.F = self.G + self.H
            elif self.where == 1:
                self.G = tile.score[1] + self.straightCost
                self.H = self.distance(self.endTile, self.tileMap[b][a])
                self.F = self.G + self.H

            #Append to list and modify variables.
            self.tileMap[b][a].score = (self.F, self.G, self.H)
            self.tileMap[b][a].parent = tile
            self.tileMap[b][a].color = OPEN
            pygame.draw.rect(windowSurface, self.tileMap[b][a].color, self.tileMap[b][a].rect)
            pygame.display.update(self.tileMap[b][a].rect)
            self.openList.append(self.tileMap[b][a])
        #If it's already in one of the lists, check to see if this isn't a better way to get to it.
        elif self.tileMap[b][a] in self.openList or self.tileMap[b][a] in self.closedList:
            self.where = abs((a - tile.col)) + abs((b - tile.row))
            if self.where == 0 or self.where == 2:
                if tile.score[1] + self.diagCost < self.tileMap[b][a].score[1]:
                    self.tileMap[b][a].score = (self.tileMap[b][a].score[0], tile.score[1] + self.diagCost, self.tileMap[b][a].score[2])
                    self.tileMap[b][a].parent = tile
                    print "parent changed!"
            elif self.where == 1:
                if tile.score[1] + self.straightCost < self.tileMap[b][a].score[1]:
                    self.tileMap[b][a].score = (self.tileMap[b][a].score[0], tile.score[1] + self.straightCost, self.tileMap[b][a].score[2])
                    self.tileMap[b][a].parent = tile
                    print "parent changed!"

And this is a newer version of the iteration that slows down so I can watch it progress. It is meant to find the node with the lowest score[0] (score is a tuple of F, G, H scores).

def path(self):
    self.openList.append(self.startTile)
    self.startTile.score = (self.distance(self.startTile, self.endTile), 0, self.distance(self.startTile, self.endTile))
    self.examine(self.openList[0])
    self.countDown = 0
    while self.endTile not in self.openList:
        if self.countDown <= 0:
            self.countDown = 5000
            self.smallestScore = MAX_DIST * 50
            self.bestTile = self.startTile
            for tile in self.openList:
                if tile.score[0] <= self.smallestScore:
                    self.bestTile = tile
                    self.smallestScore = tile.score[0]
            self.examine(self.bestTile)
        else:
            self.countDown -= timePassed

The following is an image illustrating the excess search area, using self.totalDist = self.diagCost * math.sqrt(pow(self.xDist, 2) + pow(self.yDist, 2))

excess search area

Alternatively, removing the TILE_SIZE constant from the equation gets me this equally inefficient-looking search area:

enter image description here

It seems to me that it shouldn't be searching all that extra area - just the area immediately surrounding the obstacles.

Upvotes: 2

Views: 660

Answers (1)

Jasmijn
Jasmijn

Reputation: 10452

My theory: it's because Manhattan distance is not admissible in this case, because you can move diagonal as well.

Try this:

def distance(self, tile1, tile2):
    self.xDist = abs(tile1.col * TILE_SIZE - tile2.col * TILE_SIZE)
    self.yDist = abs(tile1.row * TILE_SIZE - tile2.row * TILE_SIZE)
    self.totalDist = self.diagCost * math.sqrt(self.xDist*self.xDist + self.yDist*self.yDist)
                     # or it might be self.straightCost, depending on their values.
                     # self.diagCost is probably right, though.
    return self.totalDist

Upvotes: 1

Related Questions