CS 2005 Techniques of Programming WPI, B Term 1996
Craig E. Wills Project 2 (30 pts)
Assigned: Wednesday, November 6, 1996 Due: Tuesday (11:59pm), November 19, 1996

Introduction

This assignment is designed for you to put into practice the concept of stacks while working on a group project. You are to use an array of records to implement the stacks (as opposed to pointers). The assignment will also help you learn about an interesting area for the application of computer science--the traversal of a maze. This problem is encountered in applications such as a robot needing to move through an obstacle course. Different approaches have been used in MQP projects here at WPI.

Background

A common method for traversing a maze in an automatic fashion is to continually move up, down, left or right until the goal (perhaps a pot of gold!) is found. The maze can be modeled as a grid where each point is either unoccupied (free) or contains a wall obstacle. A key idea in traversing the maze is to not revisit a point that was previously visited. Revisiting a point at best wastes movement and at worst can lead to looping in a circle forever.

Basic Objective

The basic objective of your assignment is to implement a set of maze traversal algorithms that are built using the stack abstraction. Although recursion could also be used to implement these algorithms you MUST use a stack implementation and NOT use recursion. The model is a grid where movement from any point is allowed in one of four directions: north (up), south (down), east (right), west (left). Your program will read in a maze and print it out. The program will then ask the user to select a traversal algorithm from the ones listed below and if the traversal is successful will reprint the maze along with the path that is found.

The basic objective of the assignment includes implementing and testing four maze traversal algorithms. These algorithms differ only in the strategy used for exploring the four possible directions from the current point. In all cases movement in a direction is disallowed if the neighboring point in that direction contains an obstacle or if that neighboring point has already been visited. All algorithms terminate when either the goal point is found, in which case success is reported, or when all reachable points have been explored without finding the goal point. The four algorithms:

  1. Fixed Order(fix)--this algorithm uses a fixed order in how it tries the four neighboring direction. The order of priority (from highest to lowest) is: north, east, south, west. This algorithm ignores the current direction of travel.

  2. Maintain Direction(dir)--this algorithm gives first priority to maintaining the direction being traveled when the point is entered. This approach is desirable to minimize turns because a robot would need to slow down. The second direction to try is to turn left, the third direction to try is to turn right and the last direction is to go backwards.

  3. Towards Goal(goal)--this algorithm always knows the location of the goal point, but must find the path to reach the goal. It will always give first priority to the vertical or horizontal direction that is closest to the straight-line direction from the current point to the goal. It will give second priority to the direction second closest to the straight-line direction. The third priority should be the opposite of the second direction and the fourth priority should be the opposite of the first direction. This algorithm ignores the current direction of travel.

  4. Towards Goal Maintaining Direction(gdir)--this algorithm combines the previous two algorithms. It gives first priority to maintaining the direction being traveled when the point is entered. The second priority is the remaining direction (of the three) that is closest to the straight-line direction from the current point to the goal. The third direction is the next closest to the straight-line direction and the remaining direction is the last neighbor direction to try.

Maze Input

The input for your program will be the map of a maze contained in a file named mazemap in the current directory. The file contains the maze where the character `#' represents walls and obstacles in the maze, a space ` ' represents a free point, `<' represents the starting point and `>' the goal. These definitions are captured in the following:

#define FILENAME "mazemap"
#define WALL '#'
#define FREE ' '
#define START '<'
#define GOAL '>'

As shown in the following maze example, you may assume that a maze is always completely surrounded by a wall so you will not have to make any special checks for boundaries.

##########
#        #
#  #> # #####
#  #  #     ###
#  #     ##   #
#  # ##########
#  ###   #
## # # # #
#<       #
##########

Point Definition

Traversing a maze requires movement between points. In this assignment a point is defined as an (row,column) coordinate. Because some of the algorithms also need to use the current direction of travel you should define an enumerated type for the direction with the resulting definition for a point as follows:

enum directions {north, south, east, west};

struct Point {
    int row;      /* row coordinate */
    int col;      /* column coordinate */
    enum directions dir;  /* direction of travel in reaching this point */
};

Map Abstraction

You will need to create a Map abstract data type (as a C++ class) to store the maze. This abstraction should be implemented using a two dimensional array where the maximum vertical dimension of a maze is 20 (number of rows) and the maximum horizontal dimension of the maze is 80 (number of columns). Important: the upper left corner of the grid is considered to be coordinate (0,0). The abstraction can be defined as:

#define MAXROWSIZE 20
#define MAXCOLSIZE 80

class Map {
  private:
    int maxrow;   /* actual maximum row dimension */
    int maxcol;   /* actual maximum col dimension */
    char grid[MAXROWSIZE][MAXCOLSIZE];   /* the grid itself */
  public:
    Init(Point &ptStart, Point &ptGoal);
    SetPoint(Point pt, char ch);
    char GetPoint(Point pt);
    Print();
};

You will need the following methods to access this data structure. Your maze traversal algorithms should call these routines to access the map.

/*
 * Init - initialize the map from the input file and find the start and 
 *           goal points.
 */
Map::Init(Point &ptStart, Point &ptGoal)

/*
 * SetPoint - set the value of the map at the given point to the given character
 */
Map::SetPoint(Point pt, char ch) 

/*
 * GetPoint - return the value of the map at the given point
 */
char Map::GetPoint(Point pt) 

/*
 * Print - print the given map
 */
Map::Print()

The initial direction for the starting point is always assumed to be north using the following declaration. This value should be set in the Map::Init() routine.

#define INITDIR north                /* initial direction of travel */

The main program should define a variable of type Map class and invoke the Init() method to initially read and create the internal representation of the maze. Initiate() should first set the entire grid to the value FREE before reading in the maze. The routine should also determine the actual size (maximum number of columns and rows) in the particular maze for later printing.

In addition to reading in and printing the map, the Init() routine will also need to verify that the map is valid by containing both a start point and a goal point. If the map is valid, the routine should return a zero, otherwise return a negative value to indicate an error. The location of the starting and goal point also need to be returned. These are included as call-by-reference parameters to Init().

To read the file mazemap you will need to use the C++ file I/O routines as described in Section 4.4 of the Shiflet textbook. You will need to use the ifstream class contained in the header file ``<fstream.h>.'' A simple example showing the use of this class to read in and print out a maze is shown below (and contained in the file indicated). The getline() method reads in a line of text (not including the newline character) into the indicated buffer. You can either read an input line into a buffer and copy it into the map grid or read into the map grid directly (as shown below).

/*
 * /cs/cs2005/pub/example/fileIO.C -- sample use of Unix C++ file I/O
 */

#include <fstream.h>
#include "maze.h"

main()
{
    Map map;
    Point ptStart, ptGoal;

    if (map.Init(ptStart, ptGoal) < 0)
        cout << "Map initialization failed.\n";
}
        
/*
 * Init -- initialize the map.  WARNING: this is only a beginning, more work
 *         will be needed for the final version to set other values
 */
Map::Init(Point &ptStart, Point &ptGoal)
{
    ifstream mazefile(FILENAME); // create the stream variable for file
    int i;
    
    if (!mazefile) {                // variable will be zero if error
        cerr << "Could not open the file " << FILENAME " for reading.\n";
        exit(1);         // exit the program with error code of one 
    }

    while ((mazefile.getline(grid[i], MAXCOLSIZE)) && (!mazefile.eof())) {
        cout << grid[i] << "\n";        // echo the input
        i++;
    }

    mazefile.close();                // close the file
    return(0);                        // success
}

Program Organization

A simple main() routine will control the program. It should first call the Init() method of your Map class variable to initialize and print the maze. It will then determine the type of traversal algorithm by reading a string from the terminal. The string should be: ``fix,'' ``dir,'' ``goal'' or ``gdir.'' This string will determine the algorithm to use. This input should guide the program in calling one of the following routines (these can simply be implemented as functions, not as class member functions):

FixTraverseMaze(Map &map, Point ptStart)

DirTraverseMaze(Map &map, Point ptStart)

GoalTraverseMaze(Map &map, Point ptStart, Point ptGoal)

GdirTraverseMaze(Map &map, Point ptStart, Point ptGoal)

These routines should be built on top of stack routines implemented as a C++ class using an array of records. Your stack implementation should be similar to that given in class except the basic Item_type should be a Point rather than a char and the maximum size of the array implementing the stack should be much larger.

Maze Traversal

Each maze traversal algorithm should work in the same manner except for the strategy used on visiting neighboring points while searching for the goal. The outline of the basic algorithm is as follows:

Push the starting point on the stack
While the stack is not empty {
    Pop a point off the stack (making sure it is not now a dead end)
    If the point is the goal {
        Set the value and print results
        return
    }
    Set the value of the point to the current path value
    Push the point back on the stack to save it
    Push free neighbors of the point on stack in reverse order of strategy
    If no neighbors pushed on stack then a dead end reached {
        Continue popping points off stack while they are on the current path
        For each of these points set map value to DEADEND (`.')
        Stop when the popped point has not been previously visited
        Push this new point back on stack and continue
    }
}

To help show the current path, you should set the starting point in the path to the letter `A' and add one to the letter value for each succeeding point on the path. When you backtrack you will need to subtract one from the current letter value for each backtrack made on the current path. The character definitions for the initial CURRENT value and the DEADEND value are thus:

#define DEADEND '.'
#define CURRENT 'A'   /* initial value to use */

Sample Output

The following is sample output from a run of the maze traversal with the sample input. You can copy this sample maze from the file /cs/cs2005/pub/example/mazemap. Your output should be similar. If the maze does not have a path from the start point to the goal point then your traversal algorithm should print a failure message and return. You should include summary messages about the maze itself and the path found as shown.

> maze
##########     
#        #     
#  #> # #####  
#  #  #     ###
#  #     ##   #
#  # ##########
#  ###   #     
## # # # #     
#<       #     
##########     
Maze contains 10 rows, 15 columns, start: (8,1), goal: (2,4)

What policy to use? fix

Path was found of length 24 with 7 wrong points visited in a total of 38 moves.
##########     
# IJKLMN.#     
# H#XW#O#####  
# G# V#PQ...###
# F# UTSR##...#
# E# ##########
# D###   #     
##C# # # #     
#AB      #     
##########

As shown, the algorithm does not find the shortest path, but does find a valid path from the start point to the goal point. It also shows that some points were visited that were not part of the final path. Each traversal algorithm may find a different path from the start to the goal.

Group Organization

You should discuss the overall organization of the program and make sure that each member of the group understands the overall task to be done. You should also discuss the specifics of each traversal algorithm. If your group does not understand some aspect of the project you should consult with your PLA, a TA or the instructor. After the discussion your group should break the implementation into pieces. Each group member should implement one of the algorithms. The group should also assign responsibility for getting the main program and the map abstraction working as it will be difficult to test each algorithm until they are completed.

Your program will be divided into seven modules (files)--one for the main program, one for the map class routines, one for the stack class routines and one for each of the policies. The file names should be maze.C, map.C, stack.C, fix.C, dir.C, goal.C and gdir.C. You should create a header file (maze.h) that contains definitions common to all of the files. You can copy the file /cs/cs2005/pub/example/maze.h to your directory. This file contains all the definitions contained in this handout. You should include only definitions (e.g. class and structure definitions, #define's, typedef's) and not any code to execute in the header files. Never use the #include statement to include `.C' files.

You can compile the modules together by giving all needed files to the compiler. You will be learning how to use another tool for building modules together in your next lab. You are responsible for testing your program on a variety of input data.

Additional Work

Completion of the basic objectives with satisfactory style and documentation is worth 27 of the 30 points. For the additional three points your group can implement another algorithm--``best'', which finds and shows the best path between the starting and goal points. The approach is to use one of the basic algorithms as a basis for this algorithm. However if you find the goal then do not terminate, but save a copy of the current map and the length of the path (if shorter than any previous paths found). Then backtrack as if a dead end had been found, but rather than setting the points to DEADEND indicating a wrong path, set them to FREE so they might be tried again. The end result is that your algorithm should find all possible paths (terminating when no more points are reachable). At that point you should print out the best path found along with its length. Code for this algorithm should be put in the module best.c with the algorithm selected at the prompt with the string ``best.''

Submission

Your group will have a directory created on your behalf in which you can share work about the project. An email list will also be created containing the members of your group. You will be turning in your project as a group as directed in lab. Each group member will also be turning in an individual assessment of how the group worked together--what went well and what did not. Included in this assessment will be the contribution of each group member. You are expected to talk as a group about each member's contributions before submitting your individual assessments. More details on project assessment will be given near the due date of the assignment.