CSCI 151 - Stacks and Queues Simply A-Maze-ing!

Due 10:00pm, Sunday, March 10

In this lab you will use the power of a stack and a queue to explore and find your way through a maze. The purpose of this lab is to:

This lab is a little larger than past labs; it has many different pieces that all need to work together. Most pieces are small, but it may take some additional patience on your part. Just chug along and the end will show itself in due time!

You may work with a partner on this assignment -- both during and after the lab session.

Martian Exploration

You are part of a science research team exploring the vast canal systems on Mars. Due to the harsh surface conditions, all of the exploration is done through the use of remote controlled robots. To explore an area, you drop in a large rover-bot that needs to make its way to a different pick-up point. Each rover-bot has a large army of smaller probe-bots that can be used for to scout the route.

Unfortunately, the probe-bots have to be manually controlled, and only one group can move at a time, and only explore one new square at a time. I think the software may have been written by an earlier CSCI 151 class. :-) These probe-bots travel in small swarms, so groups can be left to explore in different directions at each intersection.

Two of your colleagues (Susan Queue and Haywood Stack) have been arguing about the best way to use the probe-bots to scout a route for the rover-bot. Susie thinks that the locations should be scouted in a first-come, first-served fashion, while Hay thinks that you should focus on the most recently explored areas first. (The captain Rex Ursvie has a different approach based on earlier technologies and is just staying out of the discussion.)

Your job is to simulate the various approaches using your nifty CSCI 151 skillz the examine the different approaches suggested by Susie Queue and Hay Stack.

Starting point code

I've supplied starting point code, including some sample mazes here: MazeApp.zip.

In this lab I won't be giving you any *.class files, just *.java source files. Remember, to get Eclipse working with these you should do the following:

  1. Create a lab4 folder
  2. Unpack the above starting point code into your lab4 folder
  3. Start up Eclipse (remember to use the same workspace you setup in lab 1).
  4. Create a new Java project in Eclipse, make sure that the "Location" in the setup page is your lab4 folder.
  5. You should now see a number of files in the default package for the project including errors in the MazeApp class. Don't worry, you'll be fixing them soon enough.

Part 1 - Representing the Maze

First let's talk about your basic maze. It has walls and pathways, and it has one (or more) starting point(s) and one (or more) exit point(s). (To keep things simple, let's just assume it has no more than one of each.) Furthermore, one wall is just like another, and any open space (not including start and finish) is also identical. So, we can think of a maze as being made up of individual squares, each square either empty, a wall, the start, or the exit.

Below is a graphical representation of the maze found in the file maze-2. The green box represents the start, the red box the exit, and the black squares the walls.


We represent such a maze with a text file of the following format. The first line of the file contains two integers. The first indicates the number of rows (R), the second, the number of columns (C).

The rest of the file will be R rows of C integers. The value of the integers will be as follows:

    0 - an empty space
    1 - a wall
    2 - the start 
    3 - the exit 

In terms of coordinates, consider the upper left corner to be position [0,0] and the lower right to be [R-1,C-1].

For example, this is the text version of the maze above (start is at [6,4] and exit at [6,11]).

7 13
0 0 0 0 0 0 1 0 0 0 0 0 0
1 1 0 1 1 1 1 1 1 1 0 1 0
0 1 0 0 0 0 0 0 0 0 0 1 0
0 1 0 1 1 0 0 1 1 1 0 1 0
0 0 0 1 0 0 0 0 0 1 0 1 0
0 1 1 1 1 1 0 1 0 1 0 1 0
0 0 0 0 2 0 0 1 0 0 1 3 0

The Square Class

Make a class Square that represents a single square in the maze. A Square should have a class variable that represents its type (space, wall, start, or exit). It is your own design decision whether you want to make this variable an int or a char or something else. Regardless, you should define constants to represent the four starting types of squares.

It turns out that later it will be useful if a Square knows where it is positioned in the maze. Therefore, you should give each Square a private variable of type int named row and col to represent it's location within the maze.

Please also include the following methods in your class:

String toString
Return the character corresponding to this Square, using the following notations:
    _ - empty space (0)
    # - wall        (1)
    S - Start       (2)
    E - Exit        (3)

    o - is on the solver work list
    . - has been explored
    x - is on the final path to the exit
The later symbols are only applied to empty spaces, not start or exit squares.
This is a perfect time to use a switch() statement [Weiss 1.5.8]
public int getRow()
public int getCol()
public int getType()
Accessor methods to get the values of the various class variables. Don't just make all the variables public.
public Square(int row, int col, int type)
constructor to create a new Square object

Be aware that you might be changing this class later.

The Maze Class

Now we can pretty easily set up the maze itself. Create a Maze class that stores the logical layout of a maze. It should contain (as a class variable) a 2D array of Squares. Initially, this array will be empty and you will use a method to populate it.

As you begin working on Maze.java don't forget to also create MazeTest.java with JUnit test cases for the various methods.

Please include the following methods in your Maze class:

public Maze()
A constructor that takes no arguments
NOTE: you can just omit writing a constructor and Java will just use the default values, the real work will be done with the loadMaze() method described next.
boolean loadMaze(String fname)

load the maze that is contained in the file named fname. The format of the file is described above. As a quick refresher, here is how you can declare a 2D array and fill it:

private Square[][]  maze;   // somewhere as a class variable

/* ... */

this.maze = new Square[numRows][numCols];

for (int row=0; row < numRows; row++) {
    for (int col=0; col < numCols; col++) {
        maze[row][col] = null;
    }
}
        
If you encounter a problem while reading in the file, you should return false to indicate that it failed. Returning true indicates that you have now loaded the file from disk successfully.
Be sure to catch the exception that is raised if the user specifies an incorrect file and print out an appropriate error message when this occurs and return false. Don't just let the program crash dumping the stack trace to the user.
ArrayList<Square> getNeighbors(Square sq)
return an ArrayList of the Square neighbors of the parameter Square sq. There will be at most four of these (to the North, East, South, and West) and you should list them in that order.
If the square is on a border, skip over directions that are out of bounds of the maze. Don't be adding in null values.
Square getStart()
Square getFinish()
Accessor methods that return the saved start/finish locations.
void reset()
Return the maze back to the initial state after loading. Erase any marking on squares (e.g., visited or worklist) but keep the layout.
One way you might do this is by giving each Square a reset() method too, and then just loop through the squares and asking them to reset themselves.
String toString
return a String representation of this Maze in the format given below. (This is where it's useful to have a working Square.toString method.)
For example, the maze above (i.e., maze-2) would be returned by toString as follows. (You may leave the spaces out from between squares if you so choose.)
    _ _ _ _ _ _ # _ _ _ _ _ _               ______#______
    # # _ # # # # # # # _ # _               ##_#######_#_
    _ # _ _ _ _ _ _ _ _ _ # _               _#_________#_
    _ # _ # # _ _ # # # _ # _      OR       _#_##__###_#_
    _ _ _ # _ _ _ _ _ # _ # _               ___#_____#_#_
    _ # # # # # _ # _ # _ # _               _#####_#_#_#_
    _ _ _ _ S _ _ # _ _ # E _               ____S__#__#E_

To keep things running quickly for larger mazes, you should use the StringBuilder class, which sort of works like an Arraylist, but for Strings. For example, you should replace the following O(n2) code

String s = "";
for( int i=0; i < data.length; i++ ) {
    s = s + data[i];
} 
with the better O(n) code
StringBuilder sb = new StringBuilder();
for( int i=0; i < data.length; i++ ) {
    sb.append(data[i]);
}
String s = new String(sb);
See p584 of Weiss for reference, if you like.

Before you continue, you should test that your Maze class works correctly. You can do this by, that's right, creating a JUnit test. Among other things, this test should load a maze from one of the supplied files, get the neighbours of some specific square (the start square, for example), and assert that (1) there are the correct number of neighbours, and (2) the neighbours are in the correct locations. You probably should do this for the corners and border cases, at least. There should also be a test to print out the maze, and to confirm your getStart and getFinish methods return the correct squares.

You may assume that any well-formed maze will have exactly one start and exactly one finish. You may not assume that all valid mazes will be entirely enclosed within walls.

Part 2 - Stacks and Queues

We've been talking about stacks and queues in class, and now it is your time to put that theory to good use. Write two classes MyStack<T> and MyQueue<T> that implement the supplied interfaces StackADT and QueueADT, respectively.

MyStack
An implementation of the provided StackADT interface that is capable of storing an arbitrarily large amount of data. Use ArrayList storage (that is, an array where you don't have to deal with writing a doubleSize method).

MyQueue
An implementation of the provided QueueADT interface that is capable of storing an arbitrarily large amount of data.

Methods are specified in the interface files supplied in the starting point code. Be sure to throw the correct exceptions. If you get stuck, you can always peek at the text to help you out. Don't copy anything directly, but you may use it as a guide. This part of the lab is not meant to take you very long, so if you find you are spending a lot of time on it, check with the text to make sure you are on track.

Before continuing, you should add JUnit tests for MyStack and MyQueue that performs testing on your data structures. (Call these MyStackTest and MyQueueTest.) Don't forget to test the exceptions too. If you'd like to test your datastructure by confirming it matches the behaviour of Java's (highly recommended), the closest structures are Stack and ConcurrentLinkedQueue. Notice that the names of the functions are different, so you'll have to make that adjustment in your testing code.

Part 3 - Solving the Maze

Now that you have a maze and working stack and queue data structures, we can use them to solve mazes! You'll next be implementing the application portion of this lab, writing up MazeSolver classes which will bundle up the functionality of determining if a given maze has a valid solution. That is, whether you can get from the start to the finish without jumping over any walls.

Our maze solving algorithm goes something like this: begin at the start location, and trace along all possible paths to (eventually) visit every reachable square. If at some point you visit the finish Square, it was reachable. If you run out of squares to check, it isn't reachable.

Boiling this down into pseudocode, we have the following:

At the start

  1. Create an (empty) worklist (stack/queue) of locations to explore.
  2. Add the start location to it.

Each step thereafter

  1. Is the worklist empty? If so, the exit is unreachable; terminate the algorithm.
  2. Otherwise, grab the "next" location to explore from the worklist.
  3. Does the location correspond to the exit square? If so, the finish was reachable; terminate the algorithm and output the path you found.
  4. Otherwise, it is a reachable non-finish location that we haven't explored yet. So, explore it as follows:
    • compute all the adjacent up, right, down, left locations that are inside the maze and aren't walls, and
    • add them to the worklist for later exploration provided they have not previously been added to the worklist.
  5. Also, record the fact that you've explored this location so you won't ever have to explore it again. Note that a location is considered "explored" once its neighbors have been put on the worklist. The neighbors themselves are not "explored" until they are removed from the worklist and checked for their neighbors.

Note that this pseudocode is entirely agnostic as to what kind of worklist you use (namely, a stack or a queue). You'll need to pick one when you create the worklist, but subsequently everything should work abstractly in terms of the worklist operations.

The MazeSolver Abstract Class

Thus, you will create an abstract class MazeSolver that will implement the above algorithm, with a general worklist. Its abstract methods will be implemented differently depending on whether the worklist is a stack or a queue. The MazeSolver class should have a non-public class member of type Maze, and should have the following methods:

abstract void makeEmpty()
create an empty worklist
abstract boolean isEmpty()
return true if the worklist is empty
abstract void add(Square sq)
add the given Square to the worklist
abstract Square next()
return the "next" item from the worklist
MazeSolver(Maze maze)
Create a (non-abstract) constructor that takes a Maze as a parameter and stores it in a variable that the children classes can access.
boolean isSolved()
A non-abstract method that the application program can use to see if this algorithm has solved this maze. That is, has it determined the path to the exit or if there is no path.
This method will return true if either:
  1. A path from the start to the exit has been found; OR
  2. You determine there is no such path (worklist is now empty)
String getPath()
Returns either a string of the solution path as a list of coordinates [i,j] from the start to the exit or a message indicating no such path exists
If the maze isn't solved, you should probably return a message indicating such.
Square step()
perform one iteration of the algorithm above (i.e., steps 1 through 5) and return the Square that was just explored (and null if no such Square exists). Note that this is not an abstract method, that is, you should implement this method in the MazeSolver class by calling the abstract methods listed above.
In order to keep track of which squares have previously been added to the worklist, you will "mark" each square that you place in the worklist. Then, before you add a square to the worklist, you should first check that it is not marked (and if it is, refrain from adding it).
Here is the suggestion for marking a Square: have each Square keep track of which Square added it to the worklist (i.e., "Which Square was being explored when this Square was added to the worklist?"). That is, add a new class member Square previous to the Square class, which will represent the Square previous to the current one; initialize this variable to null in the constructor/reset method. Then, when a Square is being added to the list for the first time, you will set the previous variable to point to the current Square (the Square that is being explored). If the previous variable is already non-null, then this Square has already been placed on the list, and you should not do so again.
You may also want to add in additional methods to your Square class to help you with this.
void solve()
repeatedly call step() until you get to the exit square or the worklist is empty.

The MazeSolver constructor should take as a parameter the Maze to be solved, and should perform the two initialization steps of creating an empty worklist (using the makeEmpty abstract method) and adding the maze's start location to it (using the add abstract method).

The MazeSolverStack and MazeSolverQueue Classes

Now we just need to actually create two different implementations of the MazeSolver class. Create two new classes MazeSolverStack and MazeSolverQueue that extend the MazeSolver class. These will not be abstract classes, and so you must implement the MazeSolver's abstract methods. Each class should contain as a class variable a worklist of the appropriate type (so, MazeSolverStack should have a class member of type MyStack<Square> and MazeSolverQueue should have one of type MyQueue<Square>). All you have to do to implement the abstract methods is perform the appropriate operations on the stack or queue class member. For example, the MazeSolverStack add method may look like this:

public void add(Square sq) {
    stack.push(sq);
}

So, as you can imagine, this part will not take you very long.

Don't forget to include a call to super(maze) as the first line of your constructor.

Also, for testing purposes, give both MazeSolverStack and MazeSolverQueue main( ) methods that get a maze from the command-line argument, create the appropriate type of worklist, call the solve( ) method to find a solution and then print the resulting path, if there is one.

Tracing the path

In order to output the solution to the maze in step 3 of the algorithm, you will need to keep track of the path that was followed in your algorithm. This seems to be a difficult proposition; however, you've already done most of the work when you marked your worklist nodes using the previous variable. Let us explain.

In order to keep from wandering in a circle, you should avoid exploring the same location twice. You only ever explore locations that are placed on your worklist, so you can guarantee that each location is explored at most once by making sure that each location goes on the worklist at most once. You've already accomplished this by "marking" each square that you place in the worklist.

You are marking the square by putting an arrow in it that points back to the square from which you added it to the worklist. Now, when you are at the exit, you can just follow the arrows back to the start.

Of course, following the arrows gives you the path in reverse order. If only you had a way to keep track of items such that the Last item In was the First item Out, then you could read all the arrows in one pass and write them back out in the correct order...

Part 4 - Animatronics!

If everything is working in your Maze and MazeSolver classes, you should be able to run the MazeApp program and get a GUI interface that will allow you to animate the process of finding the solution of the maze.

You should not need to modify anything in this file. However, this is a relatively new GUI, so if you notice anything not working correctly or if I am making some unstated assumptions about your Maze/MazeSolver, let me know ASAP so I can get an update out.

Click to enbiggen:

The load and quit buttons operate as you might expect. The reset button will call the Maze's reset() method and then create a new MazeSolver. If you click on the stack button it will toggle between using a Stack or Queue to solve the maze. The step button performs a single step of the MazeSolver and start will animate things taking one step per timer delay interval. Don't forget to hit Enter after changing values in the textboxes.

Your Maze's toString method is used to display the maze in the main window, and the getPath() method from MazeSolver is used for the bottom window.

Part 5 - Written Questions

Create a plain text file called "README" that contains the following information:

  1. Your name(s)
  2. Any known problems or assumptions made in your classes or program.
  3. The Big-O complexity of your solver algorithm and some explanation of how you computed it
  4. One or two sentences explaining which version of the search algorithm is "better" and examples of why. You should point specifically to the examples provided, and form your hypotheses by watching how the maze solver proceeds through the maze, and the solution it finds. Is one solver faster at finding solutions than the other? Does one solver find better solutions than the other?
  5. If you know it, give the "real name" of each of these solver algorithms.

handin

You should be using Javadoc style comments in your programs. You don't need to generate the HTML output, but you do need to be writing them.

Look through your programs and make sure you've included your name at the top of all of them.

Honor code

If you adhered to the honor code in this assignment, add the following statement to your README file:

I have adhered to the Honor Code in this assignment.

handin

You now just need to electronically handin all your files. Assignment is 4.

Don't forget to run lshand and verify that things were submitted.

Bonus mazes

I've got some bigger mazes for you to try out once your program is working.


Last Modified: September 20, 2015 - Benjamin A. Kuperman - Origingal GUI and Minotaurification by Alexa Sharp VI Powered