Lab 3

Trapped in a Maze!
Due before 8pm, Sunday 30 Sep 2012

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

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

If you'd like, you may work with one partner on this lab. If you choose to do so, you must both contribute equally to the work of this lab, and are both responsible for understanding its workings. You must hand in one submission between the two of you, with both your names clearly marked somewhere obvious to the graders.

I can offer groups a way to share their files using a "version control system" so that they can collaborate without just emailing files back and forth. Talk to me if you're interested.


Theseus and the Minotaur

I'm sure many of you have at one point heard the old Greek myth of Theseus and the Minotaur. The important plot points are these:

A much more humorous telling of the story can be found here.

In this lab, we will not only give you the skillz to escape any escapable maze without the need for string, we will give you *two* different ways of doing so! If only Ariadne or Theseus had taken CS 151...


Starting point code

I've supplied starting point code, including some sample mazes here: lab3.jar

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



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) finish 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 visual 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 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 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 have a class variable of type java.awt.Point that represents this Square's location in the grid.

Please also include the following methods in your class:

String toString
return the character corresponding to this Square, using the following notations:
    _ - empty space
    # - wall
    S - Start
    E - Exit
    
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. You may not know what to do with them yet; that's okay.


getters and setters
Create various methods to get and set the values of the various class variables. Don't just make all the variables public.

constructor(s)
to initialize class variables

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.

Please include the following methods in your Maze class:

boolean loadMaze(String fname)
load the maze that is contained in the file named fname. The format of the file is described above.

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. Don't just let the program crash dumping the stack trace to the user.

ArrayList<Square> getNeighbours(Square sq)
return an ArrayList of the Square neighbours 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.

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. Again, it may not be clear how exactly this is used, it should become so later!

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.

    _ _ _ _ _ _ # _ _ _ _ _ _ 
    # # _ # # # # # # # _ # _ 
    _ # _ _ _ _ _ _ _ _ _ # _ 
    _ # _ # # _ _ # # # _ # _ 
    _ _ _ # _ _ _ _ _ # _ # _ 
    _ # # # # # _ # _ # _ # _ 
    _ _ _ _ S _ _ # _ _ # E _ 

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 MyLinkedQueue<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).

MyLinkedQueue
An implementation of the provided QueueADT interface that is capable of storing an arbitrarily large amount of data. Use a simple linked-node implementation.

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.

For the LinkedQueue implementation, you might want to make a protected class to handle the nodes. You don't need to create a separate file to do this, instead just have the class description in the same file as MyLinkedQueue but without the "public" modifier.

Before continuing, you should add JUnit tests for MyStack and MyLinkedQueue that performs testing on your data structures. 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, stacks and queues, and the confidence that they are fully functional, let's get down to business! You'll next be implementing the application portion of this lab, writing up MazeSolver classes which 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 squares. If at some point you visit the finish Square, it was reachable. If not, 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 finish is unreachable; terminate the algorithm.
  2. Otherwise, grab the "next" location from the worklist.
  3. Does the location correspond to the finish 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 neighbours have been put on the worklist. The neighbours themselves are not "explored" until they are removed from the worklist and checked for their neighbours.

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 private 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)
a (non-abstract) constructor that takes a Maze as a paramter and stores it in a variable that the children 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 is there no path.
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 and 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.

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 MyLinkedQueue<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.

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 new GUI for this semester, 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.

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 affirm that I have adhered to the Honor Code in this assignment.

handin

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

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.