Mazes are a challenging and satisfying computer science problem. The algorithms for generating and solving them have deep roots in computer science and come up again and again in different disguises. But the best part of this project is that we'll apply these algorithms to generate mazes! Take a look at an example of the kind of output you'll be able to generate from this program.
There are lots of people who have done maze-related programming out in the world. Here are some links that might be of interest if you want to learn about mazes:
+-+-+-+-+-+-+-+ | |A|B| | | +-+ + + +-+ + + | |C | | +-+ + +-+-+-+ + | | +-+-+-+-+-+-+-+The cells
A
and C
are connected with a path, but
A
and B
are separated by a wall.
Of course, not every configuration of walls and paths will be a very
satisfying maze. Here are two examples of "bad" mazes:
+-+-+-+-+-+ +-+-+-+-+-+ | | | | | | +-+-+-+-+-+ + + + + + + | | | | | | +-+ +-+-+-+ + + + +-+ + | | | | | +-+-+-+-+-+ +-+-+-+-+-+The maze on the left is "too hard" (impossible, really) because there is no unblocked path from start to finish. The one on the right is "too easy" because there aren't enough walls.
To complete the definition of a maze, we need to specify that for every two cells in the maze, there is a unique path between those cells that doesn't retrace any steps. It turns out that this will guarantee that there are neither too few nor too many walls.
+-+-+-+ | | | | +-+-+-+ | | | | +-+=+-+ | | | | +-+-+-+
+-+-+-+ | | | +-+-+-+ | | | | +-+-+-+ | | | | +-+-+-+
+-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ | | | | | | | | | | | | | | | | | | | | | | | | +-+-+-+ + +-+-+ + +-+-+ + +-+-+ + +-+ + + +-+ + + +-+ + + + + + | | | | -> | | | | -> | | | | -> | | | | -> | | | | -> | | | -> | | | -> | | | +-+-+-+ +-+-+-+ +-+-+-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ + + +-+ + + | | | | | | | | | | | | | | | | | | | | | | | | | | +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+ +-+-+-+
Given all the details above, here's a simple maze-generating algorithm:
For each wall in the maze (in random order)
Combine the groups of those two cells
To walk across all the walls in random order, it's convenient to put them all in a big collection and then walk through the collection in random order.
If you ever reach the goal cell, you can just unwind the stack, following the path in reverse, marking your steps as you go. When the stack is empty, you've returned to the start cell and the path to the solution should be marked.
Interestingly, this stack-based approach becomes an even more elegant recursive solution. We haven't talked about recursion yet in this course, so you're not required to attempt a recursive solution. But if you're comfortable with recursion, you're welcome to think about this approach.
dir.h
maze.h
, you'll notice
that functions like isClear
and move
refer to a Dir
type. In dir.h
, you'll
say exactly what the Dir
type is. You're free to
decide on any method for representing the Dir
type.
Remember - there are several ways of creating new types in C++.
Which one is right for this task? Of course, there are several
possible answers.
dir.cpp
Dir
. If you do define any functions, put them
in here.
cell.h
Cell
class is the class you should use to store
information about one cell in the maze (in other words, a maze will
be some sort of collection of Cell
s). A cell needs
to contain at least the following information:
We provide you with three member functions: inSameGroup
,
combineGroups
and getGroup
. These
functions rely on the private data field parent
.
These functions will help you implement the maze generation algorithm.
cell.cpp
Cell
class. This file comes
with the implementations of inSameGroup
,
combineGroups
and getGroup
. You should
fill in the rest of your Cell
implementation here.
maze.h
Maze
class. This class
provides a maze abstraction with a simple navigation facility.
Maze navigation is not unlike list navigation, except that
from any cell in the maze, it's possible to move in any of
four directions (with lists, it was only possible to move forward,
using List::advance()
).
The public
section of maze.h
is
fixed: you may not provide additional public members!
You may add anything you like to the remainder of the file. That
includes extra #include
directives, global variables
or functions, and private member data and functions.
At the bottom of maze.h
is an inline function
definition. The exact meaning of the keyword inline
is beyond the scope of the course. Suffice it to say that
by declaring the function to be inline
, you make
it possible to define it inside the header file.
maze.cpp
Maze
class. You are required
to implement the following methods of Maze
:
Maze::Maze( int width, int height )
Maze
class. This
constructor takes the desired width
and
height
of the maze as parameters, and constructs
a new, random maze with those dimensions.
void Maze::print( ostream& os )
os
. The representation of the maze
should be (2*width+1)
characters wide and
(2*height+1)
characters tall, and made up
entirely of the characters ' ', '|', '-', '+' and '#'.
The best way to understand exactly what needs to be drawn is to look at some sample output. For reference, here's a brief explanation. Each cell in the maze will be associated with nine characters in the printed representation:
+ | a | + |
b | c | d |
+ | e | + |
The corners will always be filled with plus signs. If there
is a passage to the north from this cell, then a will
be a space. If there's a wall to the north, a will
be a minus sign. e is similarly defined for
passages to the south. b and d are also
defined this way except that we would use a vertical bar
('|'
) for vertical walls.
Finally, if the given cell is marked,
c should contain a hash ('#'
).
Otherwise, c should contain a space.
Note that in the printed representation, a cell shares the eight characters forming its boundary with its neighbours in the maze.
void Maze::reset()
void Maze::start()
List::start()
, reset the maze cursor to
the start cell of the maze. The start of the maze is always
defined as the top left corner.
bool Maze::move( Dir dir )
dir
. Of course, moving in some direction might
not be possible at the current position in the maze. This
method should return a boolean indicating whether the move
was successful. If the move was unsuccessful, the maze
instance shouldn't change in any way.
Cell& Maze::getCell()
Cell
instance
associated with the current cursor position. You want to
return a reference so that the maze solver can mark that
cell when it's part of the solution path.
bool Maze::isClear( Dir dir )
dir
?
bool Maze::isGoal()
bot.h
Bot
. The
bot's job is to roam around a maze and eventually find the solution
path. It should make sure to mark every cell in the solution path
so that when the solved maze is printed out, the path is visible.
If you look at main.cpp
, you'll see how bots are used.
You'll need to provide at least two public functions inside the
Bot
class:
Maze
.
solve
method that takes no arguments,
and returns a boolean indicating whether it successfully
solved the maze.
You're free to define any other functions or data you choose inside
the Bot
class.
bot.cpp
Bot
class. Note that you'll
need to hold on to the Maze
reference passed to the
constructor inside the Bot
instance. I recommend that
you use a private variable of type Maze&
, and assign
to it from the constructor using memberwise initialization.
collections.h
collections.cpp
collections.h
in this file.
main.cpp
main.cpp
as much as you like while
working on the project. You might find it useful to change
main.cpp
to help with debugging, for instance by writing
the maze output to a file instead of to the screen.
The client accepts three numbers from the user. The first two
are the width and height (in cells, not in characters) of the maze.
The third number is a seed for the random number generator. The
seed is just an arbitrary number that's used to initialize
the function that generates random numbers. It's not important
to know how the seed works. What's important is that you call
srand
with that seed (main.cpp
does exactly
that). Using the same seed twice will result in the same sequence of
random numbers (and therefore the same mazes, which is useful for
debugging).
The client's job is simple: create an instance of Maze
,
print it without the solution, then solve it and print it out with
the solution.
One way to build up your intuition is definitely to try this stuff on paper. Get some graph paper and try to draw some mazes. Print out some mazes from the sampole executable and try to solve them. Think about how you solve a maze as you're solving it.
dir.h
and dir.cpp
. Choose a
way to build a type that represents compass directions, and write its
declaration. Then write any functions you think might make
manipulation of directions easier.
Cell
class so that it stores information about
what neighbouring cells are connected by passages.
Maze
class. Start by choosing
a representation of a grid of cells. Remember that your representation
has to allow for arbitrary width and height. If you want to put off
the difficulties of dynamic memory, try starting with a fixed
width and height for now.
Try getting constructor to create a maze with no broken walls --
a maze where every cell is disconnected from every other. Then
get the print
method working.
As was mentioned above, a good way to iterate across all the walls of the maze in random order is to put them all in a collection of some kind and randomly remove items from the collection until it's empty. What kind of collection would be good here?
A quick note about random numbers: computer-generated random numbers
are almost never really "random". In fact, they're usually completely
deterministic! But the important thing is that they don't follow
any predictable pattern. The rand()
function will
generate integers that are distributed uniformly across their range.
You can use it to generate numbers that are "random-like", or
random enough for our purposes. The srand
function
intializes the random number generator so that it produces a repeatable
sequence of random integers. This is really useful for debugging your
program.
collections.h
and
collections.cpp
. For the most part, this code can
be adapted directly from code presented in lecture.
bot.h
and bot.cpp
.
When you're finished writing your solution, you can use the electronic turn-in form to submit it. Follow the instructions on that form (especially with regards to file naming conventions), print the receipt, and submit the receipt at the start of section, Tuesday, August 4th, 1998. For full credit, you must turn in electronically, print the receipt, and hand in that receipt in your section. You may also be able to drop your receipt in the drop box located on the first floor of Sieg hall, across from room 127. More details on this will follow.
If you need help with the lab environment or the online resources, go talk to a consultant in the IPL.
If you can't make progress in the actual assignment, please talk to the instructor or a TA. They are available during posted office hours, and sometimes by appointment. They will also occasionally help you out over email. You are invited to seek help from any available TA, not just your own.
You can discuss the assignment in general terms with other students. You
can ask general questions on the student discussion list for this course,
cse143@cs
. But any code you write must be your own.
For more details about what is and is not allowed, see the
Software hygiene page.