Skip to main content

Section 5.11 Exploring a Maze

In this section we will look at a problem that has relevance to the expanding world of robotics: How do you find your way out of a maze? If you have a Roomba vacuum cleaner for your dorm room (don’t all college students?) you will wish that you could reprogram it using what you have learned in this section. The problem we want to solve is to find an exit to a virtual maze when starting at a pre-defined location. The maze problem has roots as deep as the Greek myth about Theseus who was sent into a maze to kill the minotaur. Theseus used a ball of thread to help him find his way back out again once he had finished off the beast. In our problem, we will assume that our starting position is dropped down somewhere into the middle of the maze, a fair distance from any exit. Look at Figure 5.11.1 to get an idea of where we are going in this section.
Figure 5.11.1. Exploring the maze.
To make it easier for us we will assume that our maze is divided up into “squares.” Each square of the maze is either open or occupied by a section of wall. We can only pass through the open squares of the maze. If we bump into a wall, we must try a different direction. We will require a systematic procedure to find our way out of the maze. Here is the procedure:
  • From our starting position we will first try going North one square and then recursively try our procedure from there.
  • If we are not successful by trying a Northern path as the first step then we will take a step to the South and recursively repeat our procedure.
  • If South does not work then we will try a step to the West as our first step and recursively apply our procedure.
  • If North, South, and West have not been successful then apply the procedure recursively from a position one step to our East.
  • If none of these directions works then there is no way to get out of the maze and we fail.
Now, that sounds pretty easy, but there are a couple of details to talk about first. Suppose we take our first recursive step by going North. By following our procedure our next step would also be to the North. But if the North is blocked by a wall we must look at the next step of the procedure and try going to the South. Unfortunately, that step to the south brings us right back to our original starting place. If we apply the recursive procedure from there we will just go back one step to the North and be in an infinite loop. So, we must have a strategy to remember where we have been. In this case, we will assume that we have a bag of bread crumbs we can drop along our way. If we take a step in a certain direction and find that there is a breadcrumb already on that square, we know that we should immediately back up and try the next direction in our procedure. As we will see when we look at the code for this algorithm, backing up is as simple as returning from a recursive function call.
As we do for all recursive algorithms let us review the base cases. Some of them you may already have guessed based on the description in the previous paragraph. In this algorithm, there are four base cases to consider:
  1. We have run into a wall. Since the square is occupied by a wall no further exploration can take place.
  2. We have found a square that has already been explored. We do not want to continue exploring from this position or we will get into a loop.
  3. We have found an outside edge, not occupied by a wall. In other words we have found an exit from the maze.
  4. We have explored a square unsuccessfully in all four directions.
For our program to work we will need to have a way to represent the maze. In this instance, we will stick to a text-only representation (ASCII).
  • readMazeFile Reads a maze file and returns a vector of vector of char representing the maze.
  • findStartPosition Finds the row and column of the starting position.
  • isOnEdge Checks to see if the current position is on the edge, and therefore an exit.
  • print Prints the text of the maze to the screen.
Let’s examine the code for the search function which we call searchFrom. The code is shown in Task 5.11.1.a. Notice that this function takes three parameters: a maze object, the starting row, and the starting column. This is important because as a recursive function, the search logically starts again with each recursive call.

Exploration 5.11.1. searchFrom function.

(a) C++ Implementation.

bool searchFrom(vector<string> &maze, size_t startRow, size_t startColumn) {
    //  Check for base cases:
    //  1. We have run into an obstacle, return false
    if (maze[startRow][startColumn] == MAZE_OBSTACLE)
        return false;

    // 2. We have found a square that has already been explored
    if (maze[startRow][startColumn] == MAZE_TRIED)
        return false;

    // 3. Success, an outside edge not occupied by an obstacle
    if (onEdge(maze, startRow, startColumn)) {
        maze[startRow][startColumn] = 'O';
        return true;
    }

    maze[startRow][startColumn] = MAZE_TRIED;

    // Otherwise, check each cardinal direction (north, south, east, and west).
    // We are checking one space in each direction, thus the plus or minus one below.
    bool found = searchFrom(maze, startRow - 1, startColumn) ||
	    searchFrom(maze, startRow + 1, startColumn) ||
        searchFrom(maze, startRow, startColumn - 1) ||
        searchFrom(maze, startRow, startColumn + 1);

        if (found)
            maze[startRow][startColumn] = MAZE_PATH;
        else
            maze[startRow][startColumn] = MAZE_DEAD_END;

	    return found;
}

(b) Python Implementation.

def searchFrom(maze, startRow, startColumn):
    #  Check for base cases (Steps 1, 2, and 3):

    #  1. We have run into an obstacle, return false
    if maze[startRow][startColumn] == MAZE_OBSTACLE:
        return False
    #  2. We have found a square that has already been explored
    if maze[startRow][startColumn] == MAZE_TRIED:
        return False

    # 3. Success, an outside edge not occupied by an obstacle
    if maze.isOnEdge(startRow, startColumn):
        maze[startRow][startColumn] = MAZE_PATH
        return True

    # 4. Indicate that the currently visited space has been tried.
    # Refer to step two.
    maze[startRow][startColumn] = MAZE_TRIED

    # 5. Otherwise, check each cardinal direction (north, south, east, and west).
    # We are checking one space in each direction, thus the plus or minus one below.
    found = searchFrom(maze, startRow - 1, startColumn) or \
            searchFrom(maze, startRow + 1, startColumn) or \
            searchFrom(maze, startRow, startColumn - 1) or \
            searchFrom(maze, startRow, startColumn + 1)

    # 6. Mark the location as either part of the path or a dead end,
    # depending on whether or not an exit has been found.
    if found:
        maze[startRow][startColumn] = MAZE_PATH
    else:
        maze[startRow][startColumn] = MAZE_DEAD_END

    return found
As you look through the algorithm you will see that the first thing the code does (steps 1 and 2) is determine if the space should be visited. This is done by checking if the spot is an obstacle (MAZE_OBSTACLE), or has already been visited (MAZE_TRIED). The algorithm then determines if it has found an exit (step 3). If none of these cases are true, it continues the search recursively.
You will notice that in the recursive step, there are four recursive calls to searchFrom. It is hard to predict how many of these recursive calls will be used since they are all connected by “or” operators. If the first call to searchFrom returns true then none of the last three calls would be needed. You can interpret this as meaning that a step to (row-1,column) (or North if you want to think geographically) is on the path leading out of the maze. If there is not a good path leading out of the maze to the North then the next recursive call is tried, this one to the South. If South fails then try West, and finally East. If all four recursive calls return false then we have found a dead end. You should download or type in the whole program and experiment with it by changing the order of these calls.
The code for the maze solver is in Exploration 5.11.2. readMazeFile takes the name of a file as its only parameter and and it returns a vector of strings. The file represents a maze by using “+” characters for walls, spaces for open squares, and the letter “S” to indicate the starting position. Data 5.11.3 is an example of a maze data file. The internal representation of the maze is a vector of strings. Each entry in the vector represents one row from the file and consists of the characters described above. For the data file in Listing 5.11.2 the internal representation looks like the following:
{
    "++++++++++++++++++++++",
    "+   +   ++ ++     +   ",
    "+ +   +       +++ + ++",
    "+ + +  ++  ++++   + ++",
    "+++ ++++++    +++ +  +",
    "+          ++  ++    +",
    "+++++ ++++++   +++++ +",
    "+     +   +++++++  + +",
    "+ +++++++      S +   +",
    "+                + +++",
    "++++++++++++++++++ +++"
};
Listing 5.11.2. Example Maze
The searchFrom method uses this internal representation to traverse throughout the maze.

Data 5.11.3. Example Maze File.

Data: maze1.txt
++++++++++++++++++++++
+   +   ++ ++     +  X
+ +   +       +++ + ++
+ + +  ++  ++++   + ++
+++ ++++++    +++ +  +
+          ++  ++    +
+++++ ++++++   +++++ +
+     +   +++++++  + +
+ +++++++      S +   +
+                + +++
++++++++++++++++++X+++

Data 5.11.4. Additional Example Maze File.

Data: maze2.txt
++++++++++++++++++++++
+   +   ++ ++        +
+     ++++++++++     X
+ +    ++  ++++ +++ ++
+ +   + + ++    +++  +
+          ++  ++  + +
+++++ + +      ++  + +
+++++ +++  + +  ++   +
+          + + S+ +  +
+++++ +  + + +       +
++++++++++++++++++++++
Finally, the isOnEdge method uses our current position to test for an exit condition. An exit condition occurs whenever we have navigated to the edge of the maze, either row zero or column zero, or the far right column or the bottom row.
The complete program is shown in Exploration 5.11.2. This program uses the data file maze1.txt shown above. You can also try maze2.txt also shown above. Note that it is a much more simple example file in that the exit is very close to the starting position.

Exploration 5.11.2. Maze search code.

(a) C++ Implementation.

(b) Python Implementation.

Note 5.11.5. Self Check.

Now that you’re familiar with this simple maze-exploring algorithm, use what you’ve learned about file handling, classes, and IO to implement this in C++! To visualize the exploration, print out the characters using cout to create an ASCII representation of your cave. You can also use CTurtle to visualize the traversal throughout the maze.
+++++++
+  + S+
+  +  +
X    ++
+++++++
You have attempted of activities on this page.