This week, you will write a program that generates random mazes and finds a path in there from a beginning to an end position, and then prints the maze on the screen. To give you an idea, this is how the output of your program is supposed to look like:
./mazegen 5 6
±–±–±–±–±–±–+
| . . . | |
±–±–+ ±–±–+ +
| | . . . | |
±–±–±–+ + +
| | . | |
±–±–+ + + +
| | | | . | |
±–+ + +
| | . . |
±–±–±–±–±–±–+
Your program should perform the following major steps:
Read from the program’s command line parameters either 2 or 3 integer values. The first one is the number of rows of the maze, the second one is the number of columns. The third parameter is optional. (But you must implement handling it.) If present, the third parameter must be used as seed value for the random generator. (So you can decide to always generate the same maze for testing purposes.) When called with two parameters only, the program must generate a different maze, each time it is run. Hint: you can convert a string from argv[] to an int using std::stoi.
Generate a random maze of the given size, using the algorithm called Recursive backtracker (see below). The core of this assignment is using classes in a meaningful way. Before you start implementing, first decide what are the “things” in your program, and create classes for them. (Example things are the maze, coordinates, cells in the maze, or even the command line parameters.) Your program must implement at least two classes. But more are likely helpful for achieving good structure.
Find a path inside your random maze, from the top-left corner – we refer to as position (0,0) --, to the bottom right corner. For details, see below.
Print your maze with the path to cout. (Details, see below)
Maze generation
The maze consists of walls and cells. The maze must be closed, so there must be no openings in the outer walls. Initially, all cells are separated from each other by walls. Inside the maze, some walls must be removed to connect the cells to each other. All cells must be connected to each other; from any cell it must be possible to reach any other cell by following paths that have been created by removing some of the internal walls. Cycles inside the maze are forbidden. So, the number of removed walls must be equal to the number of cells, minus 1. Fortunately, there is an algorithm, called Recursive backtracker, that does this for us. You can watch this video with a very nice explanation. You only need the first 8 minutes where he explains the algorithm. Please do not get carried away with the code he describes. While I love his explanation of the algorithm, his code is using his own graphics library and too many C++ features that are as debatable as advanced. (And if you take “too much inspiration” from his code, you will get in trouble anyways…) Not needed, but a nice read is the description of maze generation on wikipedia.
Path finding
Once you have generated your maze (and stored inside a beautiful, class-based data structure), it is time to find a path through your maze. By convention, the path must always start in the top left corner, and must end in the bottom right corner. The finally identified path must not include any detours. For path finding, not having cycles in the maze makes our lives much easier!
Path finding can be done by a (recursive) technique called backtracking. But all you need is implement the following pseudo code, using your own class data structures:
Function findPath, given a maze M, and two coordinates, from and to, returns true if a path could be found, false otherwise:
M.at(from).visited <- true
if from equals to, return true
neighbours <- list of all direct neighbours of from that can be reached (that are not blocked by walls)
for all n in neighbours:
if M.at(n).visited == false
if findPath(n,to) == true, return true
M.at(from).visited <- false
return false
Maze printing
Maze printing must be done very precisely, according to this description. Otherwise, the automated tests will fail. This means, no other characters, no extra characters, no extra spaces or newlines. Let’s consider the following 2-by-2 example maze:
±–±–+
| . . |
±–+ +
| . |
±–±–+
Each cell is 5 characters wide and 3 characters high. Walls and corners are shared among neighbouring cells. Each corner is denoted as +. A horizontal wall is denoted as 3 minus signs: — A vertical wall is denoted by |
The contents of a cell are either three spaces: ’ ’ ,
or two spaces with a . in the middle: ’ . ’ ,
the latter if the cell is part of the path from the top left corner to the bottom right corner.
Topics to exercise
classes and their interfaces
recursion, command line parameters
To use and not to use
Your program should:
use well-designed classes (at least two)
use exceptions for indicating error conditions
implement all functionality in the member functions of the classes; no other functions outside the classes
use main() as a minimalistic driver for the classes(plus error handling)
implement the recursive backtracker algorithm as outlined above (mandatory) implementing the recursive pseudocode shown above for finding the path is highly recommended
Using the following C++ features is not allowed
arrays (except argv[l)
structs
pointers
iterators
auto
#include <iostream>
#include <vector>
#include <random>
#include <ctime>
#include <string>
#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3
class Direction {
public:
int x;
int y;
int dir;
Direction(int dir) : dir(dir) {
if (dir == 0) { // UP
x = 0;
y = -1;
}
else if (dir == 1) { // DOWN
x = 0;
y = 1;
}
else if (dir == 2) { // LEFT
x = -1;
y = 0;
}
else if (dir == 3) { // RIGHT
x = 1;
y = 0;
}
}
};
class MazeCell {
public:
bool visited;
bool path;
Direction direction; // Direction for the cell
MazeCell() : visited(false), path(false), direction(0) {} // Initialize with direction 0 (UP)
};
class Maze {
public:
Maze(int rows, int cols, unsigned int seed = 0)
: rows(rows), cols(cols), maze(rows, std::vector<MazeCell>(cols)) {
if (seed == 0) {
std::random_device rd;
rng.seed(rd());
}
else {
rng.seed(seed);
}
}
void generateMaze() {
generateMazeRecursive(0, 0);
}
void generateMazeRecursive(int x, int y) {
maze[x][y].visited = true;
std::vector<int> directions = { 0, 1, 2, 3 };
std::shuffle(directions.begin(), directions.end(), rng);
for (int dir : directions) {
Direction direction(dir);
int newX = x + direction.x;
int newY = y + direction.y;
if (isValid(newX, newY) && !maze[newX][newY].visited) {
maze[x][y].direction = direction;
generateMazeRecursive(newX, newY);
}
}
}
bool findPath() {
return findPathRecursive(0, 0, rows - 1, cols - 1);
}
bool findPathRecursive(int x, int y, int targetX, int targetY) {
maze[x][y].visited = true;
if (x == targetX && y == targetY) {
return true;
}
int dx[] = { 1, -1, 0, 0 };
int dy[] = { 0, 0, 1, -1 };
for (int dir = 0; dir < 4; ++dir) {
int newX = x + dx[dir];
int newY = y + dy[dir];
if (isValid(newX, newY) && !maze[newX][newY].visited && maze[newX][newY].path) {
if (findPathRecursive(newX, newY, targetX, targetY)) {
return true;
}
}
}
return false;
}
void printMaze() {
// Define wall characters and path characters
char horizontalWall = '-';
char verticalWall = '|';
char corner = '+';
char space = ' ';
char pathCharacter = '.';
for (int i = 0; i < rows; ++i) {
// Print the top row of each cell
for (int j = 0; j < cols; ++j) {
std::cout << corner;
if (maze[i][j].direction.dir == UP || i == 0) {
for (int k = 0; k < 3; ++k) {
std::cout << horizontalWall;
}
}
else {
for (int k = 0; k < 3; ++k) {
std::cout << space;
}
}
}
std::cout << corner << "\n";
// Print the middle row of each cell
for (int j = 0; j < cols; ++j) {
if (maze[i][j].direction.dir == LEFT || j == 0) {
std::cout << verticalWall << space;
}
else {
std::cout << space << space;
}
if (maze[i][j].path) {
std::cout << pathCharacter << space;
}
else {
std::cout << space << space;
}
}
std::cout << verticalWall << "\n";
}
// Print the bottom row of the last row of cells
for (int j = 0; j < cols; ++j) {
std::cout << corner;
if (maze[rows - 1][j].direction.dir == DOWN) {
for (int k = 0; k < 3; ++k) {
std::cout << horizontalWall;
}
}
else {
for (int k = 0; k < 3; ++k) {
std::cout << space;
}
}
}
std::cout << corner << "\n";
}
private:
int rows, cols;
std::vector<std::vector<MazeCell>> maze;
std::mt19937 rng;
bool isValid(int x, int y) const {
return x >= 0 && x < rows && y >= 0 && y < cols;
}
};
class CommandLineParser {
public:
CommandLineParser(int argc, char* argv[]) {
if (argc != 3 && argc != 4) {
throw std::invalid_argument("Usage: ./mazegen <rows> <cols> [seed]");
}
rows = std::stoi(argv[1]);
cols = std::stoi(argv[2]);
seed = (argc == 4) ? std::stoi(argv[3]) : 0;
}
int getRows() const { return rows; }
int getCols() const { return cols; }
unsigned int getSeed() const { return seed; }
private:
int rows, cols;
unsigned int seed;
};
int main(int argc, char* argv[]) {
try {
CommandLineParser parser(argc, argv);
int rows = parser.getRows();
int cols = parser.getCols();
unsigned int seed = parser.getSeed();
Maze maze(rows, cols, seed);
maze.generateMaze();
maze.findPath();
maze.printMaze();
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}