COMP 142: Project 5 --- Triwizard Tournament Maze

Overview

In Harry Potter and the Goblet of Fire, Harry and three other students participate in the Triwizard Tournament, which culminates in having to navigate a large maze, searching for the Triwizard Cup hidden inside.

In this project, you will write a program to open a text file containing a description of a maze, use a recursive algorithm to have Harry find his way through the maze to the Triwizard Cup, and print out the solution at the end, along with some other statistics.

Maze text file

Each text file describing a maze is organized into lines: each line contains a single string, and all the strings will have the same number of characters.

Here is an example file:

.#
.C
H#
This file describes a 3-row, 2-column maze, though a maze may have any number of rows and any number of columns. The hash marks (#) specify hedges in the maze that Harry cannot go through; there are also implicit hedges surrounding the boundary of the maze (so Harry cannot leave the boundaries of the maze). Harry's starting position in the maze is specified by the letter H, and the Triwizard Cup that Harry is seeking is marked by the letter C. Periods specify open sections of the maze where Harry is allowed to walk as he searches for the cup.

I suggest you read the lines of a maze file into a vector of strings, i.e., a vector<string>. If you do this, you can treat this vector as a 2-d grid of characters. In other words, say you read the lines of the file into a vector<string> called maze. You can then use maze[row][col] to access any square of the grid. For instance, in the maze above, maze[2][0] = 'H' and maze[1][1] = 'C'. Note that we use single quotes because R and C are individual characters.

(row, column) vs (x, y)

Note that in this project, we are storing coordinates as (row, column) rather than as (x, y). (r, c) makes more sense for this project because of the way we read in the maze text files: each string in the file naturally represents a row, and when we store those strings in a vector, the first square bracket index naturally will refer to a row. This may seem slightly backwards, because a "row" refers to how far we go in the "y" direction (vertically), whereas a "column" coordinate specifies how far we go horizontally in the "x" direction. As long as we don't switch back and forth between these two conventions within the same project, everything works, so just remember these facts for this project:

Solving the maze

Finding the Triwizard Cup in a maze lends itself quite naturally to a recursive formulation. We will present two formulations. You will only have to implement the second, but the first is useful for easing into the problem.

First recursive formulation

Suppose that all Harry cares about is if he can find the cup in the maze ( he doesn't care about the path he takes from the starting position to the cup's position).

To solve this problem, Harry asks himself, "Am I currently standing where the cup is located?" (the base case). If so, then Harry is done (success!). If Harry is not standing where the cup is, Harry will try to take one step north, and try to solve the maze from that new location. If that didn't work, Harry will try to take one step south, and try to solve the maze recursively from that location. He will do the same thing for east and west. If one of those four recursive cases succeeds in solving the maze, then Harry is done (also success!). If none of the recursive cases solves the maze, then Harry is done (but with failure).

[ See an example of how the recursive formulation works on the sample maze. ]

Second recursive formulation

The first formulation works, but the problem is is only returns success or failure, not the path taken through the maze.

To remedy this, let's change our recursive formulation to return strings rather than success or failure. Each string will represent the path through the maze from the starting location to the cup: we'll use "N", "S", "E", and "W" for the four cardinal directions, and "C" for the cup. If a path can't be found, we'll use "X" for failure.

[ See an example of how the recursive formulation works on the sample maze. ]

Your program will implement the second recursive formulation, so that we can see the path at the end.

Pseudocode

Here is pseudocode for the outline of how you should write the solve function.
string solve(maze, row, col, numcalls):
  // maze represents the maze we are trying to solve
  // row and col represent Harry's current position as he moves 
  //   through the maze, searching for the cup.
  // numcalls stores the number of calls to solve we have made so far.
  // The return value (string) gives the path from (row, col) to the cup, assuming
  //   the cup can be reached from 
  
  Update numcalls.
  
  Print the current (row, col).
  // This printing step is not needed in the algorithm, but I want you to include it
  // so I can check that your recursion is correct.
  
  Is our current (row, col) where the cup is?
    If yes, return the string "C" (indicating the cup is here).
    
  // If no, keep going below.
  
  Drop a breadcrumb at our current (row, col) position.
  
  Can Harry move NORTH from his current position? 
    If yes, try to solve the maze from one step north.
    Examine the string that comes back from the recursive call.
    If this string is not "X", then that means the recursive call found a solution
      and this string is the path to that solution.
    Update this string to reflect that we moved NORTH to find the solution.
    Return the string.
  
  Can Harry move SOUTH from his current position? 
    If yes, try to solve the maze from one step south.
    Examine the string ... (see above)
  
  Can Harry move EAST from his current position? 
    Repeat for EAST.
  
  Can Harry move WEST from his current position? 
    Repeat for WEST.
    
  // If we get here, it means the maze cannot be solved from our current position,
  // since trying all four possible directions failed (either returned "X" or Harry
  // couldn't move in that direction).  We must be in a dead end.
  
  Pick up the breadcrumb from our current position because breadcrumbs only mark
  the solution, and we didn't find one from here.
  
  Return "X" (indicating a dead end)

What are breadcrumbs?

The "breadcrumbs" are used for marking what squares in the maze we've already visited, for two reasons. First, they will prevent us from going in circles, and second, they will allow us to show the completed path from start to finish after it is discovered. Therefore, the lines of code in the algorithm above that ask if Harry can move in a certain direction should not only take into account the placement of the hedges and the boundaries of the maze, but also the breadcrumbs. In other words, never let Harry walk onto a square with a breadcrumb, because it means he's already been there.

A breadcrumb is placed in a square at the beginning of the function. It is only removed if we reach a dead end, therefore, when the algorithm finds the cup, we can be guaranteed that there will be a single trail of breadcrumbs from start to finish. In your code, you will represent breadcrumbs by a lowercase letter 'o'. When the code asks you to drop a breadcrumb, you will literally alter a character in maze[row][col] from a period to a lowercase 'o'. If you ever pick up the breadcrumb, you will change the 'o' back to a period.

Note that the 'H' symbol never moves in the maze; it is used just to signify where Harry begins. All the "moving" in the maze is accomplished with the row and col arguments that are passed (recursively) to solve. In other words, the current location of Harry is the row/col of the current call to solve. Because of the recursion, Harry will automatically backtrack if he finds himself in a dead end. A nice explanation of this is here.

Note: This algorithm is guaranteed to find a solution to the maze if it exists. For some mazes, it will not necessarily find the shortest path if there are multiple paths Harry could take.

Additional things the program must do

After your program solves the maze, it must print out

Suggested functions

I suggest you write the following functions:

Coding style

All the normal guidelines for coding from previous projects apply. Do not use any global variables.

Helpful hints

To guarantee that your program works exactly the same as mine, you should make sure your rat always checks the directions in the order north, south, east, west.

Sample input and output (five samples)

Your output should match mine, especially in terms of the sequence of locations that Harry visits, the N/S/E/W solution order, the total calls to solve, the length of the path, and the solution picture.

[ maze0.txt ] [ maze0.txt output ]
[ maze1.txt ] [ maze1.txt output ]
[ maze2.txt ] [ maze2.txt output ]
[ maze3.txt ] [ maze3.txt output ]
[ maze4.txt ] [ maze4.txt output ]

How to submit

Upload your code to Moodle as a file called main.cpp.

Challenge problem

In the recursive formulation, we always try the directions in the same order (N, S, E, W). Suppose the cup gives off a magical homing signal that Harry can sense, so he always knows what general direction the cup is in, even if he can't see it over the hedges. Use this signal to have him pick the directions he searches in a more logical order.

Try your solution on this maze: maze5.txt. Using the same N/S/E/W order of checking directions gives an extremely poor solution. If you do this challenge right, Harry should go straight to the cup.