Escape the Maze with A* Search Algorithm | by Alberto Naranjo | Towards Data Science

The A* (A-Star) search algorithm is one of the most commonly used algorithms for path planning. It relies mainly on brute force and heuristics to find the least costly route between the two points. Today we will learn how to use it to escape a deadly maze while avoiding some lurking treacherous enemies.
The A* search algorithm was first published in 1968 by Stanford researchers as part of the Shakey the robot project. They wanted an improved version of the Dijkstra algorithm that uses a heuristic function to minimize the cost of movement and plan a path for the robot to navigate autonomously around corridors and different rooms in a building. Like some kind of ancient Roomba, it used several distance sensors and physical “bumper” sensors to detect nearby obstacles and build up a local map for navigation.
One of the keys of the A* search algorithm compared to other search or pathfinder algorithms is the use of some heuristic function to determine or estimate the current cost from the start point and the future cost to the end position or goal. Based on this cost calculation the algorithm will explore the different paths and choose the one that minimizes the cost to the goal. The most basic cost can be easily calculated as the distance to the goal. For example positions farthest to the goal point will be more costly than the closest ones, however different terrains will also be more costly than others for example mud over a paved road. We could also create a cost function for the altitude or slope in the terrain. Finally, some obstacles may be impassable and they will affect the final path.
The first thing we need to do is to set up and define our world map and define some rules, like how can we are allowed to move around and what kind of obstacles exists.
We will be working with a two-dimensional map with cartesian coordinates x and y, where x are the columns and y the rows. On every coordinate, there’s a position or cell in the map. The map also defines some constraints on how we can move per step. We will define one step up, down, left, or right as possible movements in our map. One step at a time and with no diagonal movement.
We will have 4 basic cell types that represent obstacles, one for the start and the other for the goal position:
🌴 Palms represent impassable obstacles and form walls and map boundaries👣 Footprints are just empty spaces👹 Oni monsters are deadly enemies we need to avoid🐥 Chicks are enemies as well, but not so deadly as we will discover later ...🐼 Suave panda is our hero, it also represents the starting point in the map🛖 Hut is the goal or end position we need to reach from the starting point
With the previous elements we can build rather complicated maps with several obstacles and route options to reach the goal point:
To build a map I’m just using a plain text file as input to parse. I invite you to explore the article’s repository in Github and to create and test your maps. You can even create new obstacles and enemies or customize their cost as we will see later.
The next step will be to build a map cost using a heuristic function. In this case, I have chosen to pre-process the whole map and to create a cost map for every position in the map. In a different case, we may want to avoid costly pre-processing and to process heuristics on-demand every time we discover a new position. This can be useful if the map content is unknown and we need to discover and build the map at the same time we are navigating through the positions.
To calculate the cost per position we will take only two variables: local cost of the position and distance cost to the goal. The local cost indicates how hard is to traverse this position, it can simulate the cost of real terrain like mud versus a paved road. Walking through mud is more difficult and costly so we would like to avoid it when possible.
In our case enemies increase the cost at their positions and the 8 neighboring positions around them, so we would like to avoid being close to the enemy by one cell distance. Oni monsters have more cost than chick enemies, so in case, we would be forced to decide between crossing close to any of them it will be less costly to choose walking next to the chick as we will discover later. Following this cost structure, walls have assigned costs as well. But it’s a cost too high to assume so we will mainly ignore them and assume walls as impassible obstacles. These are the cost per cell/position types:
🌴 Walls: 1000👣 Empty: 0👹 Enemy: 100 (and 100 for the 8 cells around)🐥 Lesser enemy: 50 (and 50for the 8 cells around)
The second part of our cost calculation will be related to the distance. In general, advancing towards the goal will have less cost than walking away from it. So we would like to choose always the shortest path to the goal when possible.
For this cost based on distance, we can just use the Euclidean distance for every position to the goal position. The Euclidean distance formula between two points in a two-dimensional space is d = √((x1-x0)2 + (y1-y0)2). But as we can’t move in angle between points in the map (we are using 4-way movement with no diagonals) the distance cost function could be just simplified to be d = abs(x1–x0) + abs(y1-y0) called Manhattan distance in taxicab geometry.
Aggregating both costs, position cost, and distance cost, we can build now a cost map:
In bright red, we can see the walls with some unaffordable cost over 1000. Then the oni monsters area in darker red. The chick cost area is in purple. And finally the open spaces with just the distance cost to the goal position in darker blueish colors.As you can see scaling this cost map correctly will be key for the search algorithm to find the best route from start to the goal point.
Having now the initial map and the map cost ready we can start with the A* search algorithm implementation. In the article’s repository in Github, I have used a full Python implementation not only for the algorithm itself but also for visualizing and parsing the input map and cost map. But my goal in this article is to explore the implementation in general terms without focusing on any language in particular.
The first data structure we will use in the A* search algorithm implementation will be a simple node list. This node list will represent a path to the goal position we will be building node by node as we discover better ways to navigate around obstacles while minimizing the accumulated cost of the route.
One node represents a position in the map with accumulated cost, and it will reference a parent node is connected to. Traversing the nodes in reverse from the last child to the first parent will produce a route from the starting point to the last node in the list.
class PositionNode:    def __init__(self, parent: "PositionNode", position: tuple):        self.cost = 0        self.parent: "PositionNode" = parent        self.position: tuple = position    def __eq__(self, other: "PositionNode"):        return self.position == other.position
As you can see the PositionNode class have 3 properties: cost related to the accumulated cost from the start of the route. position as a tuple, or two position array with x and y.parent as the reference to the parent node object who is another object of the PositionNode class. An object with a null or None value in parent would be the root node or the starting position in the path.
Next, we will define two lists of node objects (not connected) as arrays of objects:
nodes_to_visit[] Here we will add the newly discovered nodes/positions from the movements with their parent node and accumulated cost. This list of unconnected nodes is actually a priority queue of nodes, where the first node we add will be the first node we will pop out to process following a FIFO data element order.nodes_visited[] Here we will move the nodes/positions from the previous list we have already visited and we don’t need to check anymore.
Before entering the main loop, we can set up the start position as the first node to visit in the nodes_to_visit list.
Running the main search algorithm loop will look like this:
1. Check if any node in the list to visit have a lower cost. If so select it as the current node.2. Move the current node from the “nodes to visit” list to the “visited nodes” list.3. Check if the current node is actually the goal position by checking the position coordinates. If so we are done! and we will return the last linked node that should be the goal position node.4. Check all possible movements from the current node and create their nodes, updating the current cost for all of them5. Add new nodes to nodes to visit list ONLY if the same position is not in the list OR its accumulated cost is lower than the previously added node for the same position. This is important to note, because we may visit the same position more than one time but as part of a different route with a lower accumulated cost.
If we run out of nodes to visit during our main search loop means that no path can be found in the map, probably because the start position to the goal position is disconnected or blocked by some impassable obstacle like walls.
After successfully reaching the goal point we can return the last connected node and we need to reconstruct a path of points by just traversing the nodes in reverse using the referenced parent until the last node with a null or None parent. Reversing this list of points will give us the correct order from start to finish position.
If we study in detail the chosen path we will be amazed at how well optimized actually is! The first optimization for open spaces is the distance cost. The path only walks positions that make the distance shorter to the goal position while avoiding costly obstacles.
Next, it tries to optimize (with the distance cost already aggregated) the monster cost. It avoids the first monster cost area even at the expense of taking some extra steps. The second encounter is even more interesting because there are two ways: one shorter but with a costly monster and another longer but with a lesser enemy with a lower cost (the chick). The cost of taking a detour and facing the chick is lower in this case than the shorter route but facing the oni monster. The last encounter also showcases these cost-based decisions, a shorter path is available with a monster but a longer monster-free route is preferred to reach the goal position.
Now let’s explore the limits of these cost-based decisions. In the next maze the route selected is also trying to avoid the enemy and choosing a significantly longer path to the goal position:
However is we make this detour slightly longer (by adding only one more row) in the same map. We will reach some cost breakpoint, where the cost of the detour is bigger than facing the chick enemy and taking the shortest route:
As you can see this behavior and decision-making process is solely dictated by how well the heuristic function is designed and scaled to the case. The algorithm itself will just visit all neighboring allowed positions to figure out the lowest route.
There are several improvements and optimizations you can do to adapt this basic approach of the A* search algorithm to fit different cases. If you want to go deeper into the algorithm I recommend you to read Amit Patel's notes on A*, pathfinding, game geometry, and more.
I also highly recommend you to explore this article’s repository in Github with the same examples we have used and the full Python implementation of the map parser and the whole A* search algorithm and try to build your own maps and introduce new obstacles.
Have a happy path planning!