Another project for my AI class. This program generates random mazes and then solves them using either a depth first or breadth first search.
The Maze Silo
The storage of a maze begins with the click of the 'Generation' button. A maze is generated by first parsing the user input. The input is then used to initialize the Maze object, this involves creation of an array of Cell objects, initializing each cell in the maze, and selecting a random cell as the first cell. Each cell knows its location in the maze, the state of its walls, and how to draw itself. For the solution, a cell knows if it was a part of the solution path, and which cell was previous to it in making the solution path.
To Make a Maze
The generation is done with a depth first based algorithm. The generation loops until each cell in the maze has been visited, the current cell is asked to find any neighbors it has that still have all their walls. If the cell has no such neighbors then that route has dead ended and a stack of cells that were visited to get to that particular cell is popped and the cell from the stack is checked to see if it has a neighbor that satisfies those conditions. When a cell has been found that has neighbors who have all their walls then a random neighboring cell from that list of neighbors is selected. The wall between the two cells is knocked down, the current cell is pushed to the stack, and the selected cell is made the current cell. If the new cell has not been visited it is set as visited and the visited cell count is incremented.
To Make the Perfect Imperfect
The mazes generated with the above algorithm have no loops and are always solvable. Placing a positive integer value in the Loop Percent or Block Permille field alters the way a maze is generated slightly. The Loop Percent is used to randomly select walls that are being knocked down as the generation moves through the maze and set the wall as down, but perceived to be up by neighboring cells. So when a cell asks a neighbor if it has all its walls it will report that it does even when one or more walls are already down. The Block Permille works in a similar fashion. It sets the chance that as walls are being knocked down a wall will be left up instead, but the generation algorithm will continue moving forward from that point as if the wall had been knocked down.
To Solve or Not to Solve
The DFS works a lot like the generation algorithm. It looks until it has either found the solution, or the solve stack is empty and the current cell has no unvisited cells to move to. The solver is started with the 'Start' position cell, the cell is set as visited. The cell finds which of its neighbors it can be moved to and has not yet been visited. One of these cells is randomly selected. The current cell is pushed onto the stack, the selected cell is made the current cell and it is marked as visited. The process continues until a cell with no unvisited neighbors is found, then the stack is popped until a cell with unvisited neighbors is found. If the maze cannot be solved all cells that were visited during the solve attempt are displayed. If the maze is solved the cell stack is popped and each cell in the stack is marked as part of the solution.
The BFS operates using a queue. The start cell is placed in the queue. While there are still cells in the queue and the solution has not been found the search continues. The cell finds all its unvisited neighbors and places each of them in the queue, each of these cells gets there PreviousCell reference set to the cell that added them to the queue, and they are marked as visited. The solver then dequeues the next cell and repeats the process. If no solution is found then all cells that were visited are displayed. If a solution is found the solution cell is taken and each cell in the list formed by the PreviousCell references is marked as part of the solution.
The BFS will always find the shortest solution, and generally does so faster than the DFS. Since the DFS randomly chooses among unvisited neighbors it will find different paths with each solve, if they exist.
When Drawing a Maze
A new bitmap is constructed using the dimensions of the maze multiplied by the size of each cell. The size of the picturebox that will hold the bitmap is then adjusted, if it is larger than the viewing area scroll bars automatically are added. A graphics object is created from this bitmap, which is then passed to the Maze object. The Maze loops through each cell and tells it to draw its walls, etc. using the graphics object. A cell first draws a filled rectangle if it is part of the solution, it then draws a line for each of its walls that are still up. When the process is complete the fully drawn bitmap is set as the image for the picturebox. |