Data Structures and Algorithms

Lab 6: Labyrinth

Due on Sunday, March 20th at 11:59 PM. This is a team lab. You and your assigned lab partner(s) will complete this lab together. Make sure that you are familiar with the Partner Etiquette guidelines. You may discuss the concepts of this lab with other classmates, but you may not share your code with anyone other than course staff and your lab partner(s). Do not look at solutions written by students other than your team. If your team needs help, please post on the Piazza forum or contact the instructor or ninjas. If you have any doubts about what is okay and what is not, it’s much safer to ask than to risk violating the Academic Integrity Policy.

Overview

For this lab, you will write two data structures: a stack and a queue. You will then write a program which uses these data structures to find a path through a maze. Like the last team lab, your repository URL will be git@github.swarthmore.edu:cs35-s16/lab06-<team-name>. You can see a list of all of your repositories on the Swarthmore GitHub in the lower-right side of the main page; make sure to switch your view to the organization for this class to see them.

Tasks in This Lab

Completing this lab involves the following steps:

This webpage will give hints on how to implement and test each of these features and show examples of execution.

Implementing a CircularArrayList

Relevant files:

You can use the lecture notes regarding both templated classes and array lists to complete this part of your assignment.

Since you are implementing a form of ArrayList, remember that it will be your responsibility to keep track of an array (e.g. contents), the size of that array (e.g. capacity), and the number of elements in the list which occupy positions in that array (e.g. size). You’ll also have to keep track of the offset (e.g. head_offset) which tells you the position of the first element in the array. Finally, it will be your responsibility to replace the contents array when the list grows beyond that array’s capacity. You should create an ensure_capacity method just like we did in class, which does the following:

Testing the CircularArrayList

Ouroboros

You should make sure to test the CircularArrayList thoroughly. It’s especially important that you exercise the ensure_capacity method and the head_offset functionality of the list.

One test to exercise ensure_capacity could be written something like this:

This shouldn’t be the only way that you test ensure_capacity, but this testing algorithm is provided here to give you an idea of things to try. Note: this algorithm never adds things to the beginning of the list. This way, we’ll be able to see if there’s a bug in ensure_capacity (this test would fail) or if there’s a bug in e.g. how head_offset is handled (this test would pass but another test would fail).

To test head_offset, you can perform a similar test to the above but add the elements to the front of the list rather than to the end. You can also use a simpler algorithm like this:

These are just a few of the many tests that you should write. Remember: each test helps you build confidence that your implementation of CircularArrayList is correct. Once you believe that the implementation is complete, you’ll have a much easier time isolating bugs in the rest of your code.

Implementing CircularArrayStack and CircularArrayQueue

Relevant files:

In completing the stack and queue implementations, please remember: most of the work is already finished. Your queue won’t have to worry about things like head offset or capacity; the CircularArrayList is taking care of that for you! Just make sure your object has an appropriate CircularArrayList field that you can use to implement its methods.

Note that both Stack and Queue are subtypes of OrderedCollection. The implementation of the OrderedCollection methods should be very easy: you just make your object call one of its own methods to perform the operation. To give you an example of this (and a starting point), part of CircularArrayStack has been written for you. You will have to write CircularArrayQueue from scratch, but you can use CircularArrayStack as a guide.

Testing CircularArrayStack and CircularArrayQueue

Remember to test these data structures thoroughly so that you can rely on them when you write your labyrinth solver. You don’t want to try to debug them by running your labyrinth_main!

When testing these data structures, make sure to examine the behavior that is specific to the data structure. If your tests for CircularArrayList are good, for instance, then you shouldn’t be trying to add lots of elements to a stack to see if ensure_capacity works; that’s not part of the implementation of CircularArrayStack. Instead, make sure that e.g. elements are returned in the opposite order that they were added (last-in-first-out).

Solving a Labyrinth

Map

Relevant files:

In this part of the lab, you will write a program which helps you to solve labyrinths. To solve them, you will use two strategies: a depth-first search and a breadth-first search. The OrderedCollection interface will make it possible to switch between these algorithms simply by changing which type of data structure (stack or queue) you create.

The Layout of a Labyrinth

Each labyrinth is a rectangular grid. Each spaces in the grid is assumed to be connected to the orthogonally adjacent spaces (left, right, up, and down) but not the diagonally adjacent spaces. The starting point of every labyrinth is the upper left corner; the exit is in the bottom right.

The layout of a particular labyrinth will be stored in a text data file with the extension .map; you can find several examples in your test_data directory. We have already written the code which loads files in this format for you; this explanation is just for reference. A map file contains the following:

For instance, the following is a valid map:

5 3
...##
.#...
...#.

The Position Class

We will represent a labyrinth as a two-dimensional grid of Position objects. Each Position object contains relevant information for one place in the labyrinth: the (X,Y) coordinates (where (0,0) is in the upper-left), whether that position is a wall, and some fields which will be used during the search to construct an appropriate path through the labyrinth. The constructor of the Position class should take the X and Y coordinate values and initialize the other fields to suitable default values (like NULL or false).

The two-dimensional grid that you create for your labyrinth should take the form of a member field positions of type Position***: an array of arrays of Position pointers. This way, you can write e.g. positions[0][0] to get the Position* which points to the Position object in the upper-left corner. You’ll have to use nested for-loops to initialize positions properly.

The Labyrinth Class

Labyrinth

A Labyrinth object represents our knowledge of a particular labyrinth; we can use this information to find a path through the labyrinth using various search strategies. You will write two methods – solve_breadth_first and solve_depth_first – which are extremely similar. To avoid duplicating a lot of code, both of these methods should call a solve method which takes an OrderedCollection* as an argument; this makes it possible to write solve_breadth_first and solve_depth_first in a single line each. All three methods will return a List<Position*>*: a pointer to a new list of positions which constitute a correct path through the labyrinth from start to finish. If no such path exists, the method should instead return NULL.

You have been provided the declaration of the Labyrinth class; you merely need to implement each of the methods. The real magic of making the program work happens in these methods, culminating in the solve method which finds a path through the labyrinth. Here’s an algorithm you can use to implement your solve method:

At the end of the loop, you can determine whether you found a path by looking at the exit position; if that position has been visited, you just need to follow the previous position pointers backward from it until you reach the start to describe the path to take (in backwards order). If the exit position has not been visited, then there is no possible path through the labyrinth. The above algorithm works for both depth-first and breadth-first search, depending on which kind of OrderedCollection you use.

Testing the Labyrinth Class

Make sure to stop and test your code at this point! Using your Labyrinth class, you should be able to write some simple tests that find valid paths. One test has been provided for you in test_labyrinth.cpp, but you should write more to be sure that there’s nothing you’re missing. Just like with the data structures above, it’ll be easier to find most bugs in your Labyrinth implementation by direct testing than it will be by trying to find them through labyrinth_main.

Some of the provided map files in test_data have multiple possible solutions. As a result, your output may differ from the provided solutions depending on how you explore the labyrinth. To get the same results as the provided solutions, you should explore neighboring spaces in the following order in your get_neighbors method: up, left, right, and then down.

Implementing labyrinth_main

The implementation for labyrinth_main is pretty easy; it just involves assembling the pieces you’ve constructed above. This is made simpler by the fact that the load_map and save_answer functions have been written for you. load_map will read a map file; it will return a Labyrinth* if load is successful and will throw a runtime_error otherwise. save_answer will write a solution file; a solution looks like the map from the map file but contains @ characters on the path that you found. For instance, one possible output for the above map file would be:

@@@##
.#@@@
...#@

If there is no solution, the save_answer function will write a file containing no @ symbols. Solutions to the labyrinth are given in the .solution.txt files in the test_data directory.

Your program will take three command-line arguments:

For instance, the following is a run of your program you might want to try:

./labyrinth test_data/easy.map test_output/easy.solution.txt depth

Testing utilities (CHECK_FILES_EQUAL and RUN_MAIN) have been provided in this lab in the same style as before.

Memory

Your program is required to run without any memory leaks or errors; you must free up all of the memory that you allocate and you must only use memory that you have allocated and initialized. We will use valgrind to determine if you have done this correctly, so you should as well. You should also consider using valgrind to find bugs in your program as you proceed; it’s one of two very powerful C++ debugging tools that we’ve discussed.

Coding Style Requirements

You are required to observe some good coding practices:

Well-dressed people (style)

Peer Review

As this is a team assignment, you are required to complete a peer review of your lab partner or partners. You must do this even if everything is fine. You should complete this peer review after you have turned in your assignment. The peer review comes in the form of a simple Google Form and can be accessed here; in most cases, it will likely take less than a minute to complete. If you have trouble accessing this peer review, please make sure that you are logged into Google using your Swarthmore credentials.

Your peer review will be kept private; only course staff will have access to this information. You can also update your peer review after you have submitted it. However, if you do not submit a peer review, you will lose points on your lab. Please don’t forget!

Summary of Requirements

When you are finished, you should have