A comprehensive maze generation and visualization application built with C# .NET 8 and WPF, featuring 10 distinct algorithms including a custom hybrid algorithm. The application provides real-time, step-by-step visualization of how each algorithm constructs mazes, making algorithmic behavior transparent and educational.
This project demonstrates professional software engineering practices including clean architecture, design patterns (Strategy, Observer, MVVM), and event-driven programming. The separation between MazeLogic (core algorithms) and MazeVisualiser (presentation layer) ensures modularity, testability, and maintainability - making it an excellent showcase of software craftsmanship.
The application implements 10 distinct maze generation algorithms, each producing unique maze characteristics and patterns:
All algorithms implement the IMazeGenerator interface and use an event-driven architecture,
raising OnCurrentCellChanged and OnWallRemoved events that allow the visualizer to
display the generation process in real-time without tight coupling between algorithm and UI layers.
My Custom Algorithm is a unique hybrid approach that combines elements of random walks, path finding, and intelligent navigation. Unlike traditional algorithms, it selects random pairs of visited and unvisited cells, then creates connections by navigating toward the target cell while preferring unvisited paths.
How it works: The algorithm maintains visited and unvisited cell sets. It randomly selects a start cell from the visited set and a target from the unvisited set, then navigates toward the target using Manhattan distance-based direction (preferring horizontal or vertical movement). When it encounters an unvisited cell, it carves a path. If it hits already-visited territory after carving has started, it connects to the existing maze structure. This creates mazes with interesting characteristics - longer corridors than Prim's algorithm but more branching than pure Recursive Backtracking.
public class CustomMazeAlgorithm : IMazeGenerator
{
public void Generate(Maze maze)
{
List<Cell> visited = [];
List<Cell> unvisited = [];
HashSet<Cell> visitedSet = [];
// Initialize all cells as unvisited
for (int x = 0; x < maze.Width; x++)
for (int y = 0; y < maze.Height; y++)
unvisited.Add(maze.GetCell(x, y));
// Pick random starting cell
Cell startCell = maze.GetCell(rng.Next(maze.Width), rng.Next(maze.Height));
visited.Add(startCell);
visitedSet.Add(startCell);
unvisited.Remove(startCell);
while (unvisited.Count > 0)
{
// Select random visited cell and random unvisited target
Cell start = visited[rng.Next(visited.Count)];
Cell target = unvisited[rng.Next(unvisited.Count)];
// Calculate direction toward target
IntVector2 direction = new(
target.X > start.X ? 1 : (target.X < start.X ? -1 : 0),
target.Y > start.Y ? 1 : (target.Y < start.Y ? -1 : 0)
);
int currentX = start.X, currentY = start.Y;
bool carvingStarted = false;
// Navigate toward target
while (currentX != target.X || currentY != target.Y)
{
// Build possible moves based on direction
List<IntVector2> possibleMoves = [];
if (direction.X != 0) possibleMoves.Add(new(direction.X, 0));
if (direction.Y != 0) possibleMoves.Add(new(0, direction.Y));
IntVector2 move = possibleMoves[rng.Next(possibleMoves.Count)];
Cell current = maze.GetCell(currentX, currentY);
Cell next = maze.GetCell(currentX + move.X, currentY + move.Y);
if (!visitedSet.Contains(next))
{
// Carve through unvisited cell
maze.RemoveWallBetween(current, next);
visited.Add(next);
visitedSet.Add(next);
unvisited.Remove(next);
carvingStarted = true;
currentX += move.X;
currentY += move.Y;
}
else if (carvingStarted)
{
// Connect to existing maze and stop
maze.RemoveWallBetween(current, next);
break;
}
else
{
// Navigate through visited cells
currentX += move.X;
currentY += move.Y;
}
}
}
}
}
This algorithm demonstrates creative problem-solving by blending concepts from multiple classic algorithms into a novel approach with unique maze generation characteristics.
Recursive Backtracking: This algorithm uses a depth-first search approach with a stack. It starts at a random cell, randomly chooses an unvisited neighbor, removes the wall between them, and repeats. When it reaches a dead end (no unvisited neighbors), it backtracks to the previous cell. This creates mazes with long, winding corridors and relatively few dead ends.
RecursiveBacktracking:
stack.push(startCell)
while (stack is not empty) {
current = stack.peek()
unvisitedNeighbors = getUnvisitedNeighbors(current)
if (unvisitedNeighbors.length > 0) {
neighbor = random(unvisitedNeighbors)
removeWall(current, neighbor)
stack.push(neighbor)
} else {
stack.pop()
}
}
Kruskal's Algorithm: A minimum spanning tree algorithm that treats the maze as a graph. It creates a list of all possible walls, shuffles them, and processes them randomly. For each wall, if removing it would connect two separate regions (using a union-find data structure), the wall is removed. This creates mazes with many short dead ends and a more uniform distribution of passages.
Kruskal:
walls = getAllWalls()
shuffle(walls)
for each wall in walls {
cellA = wall.cellA
cellB = wall.cellB
if (find(cellA) != find(cellB)) {
removeWall(wall)
union(cellA, cellB)
}
}
Wilson's Algorithm: Uses loop-erased random walks to generate uniform spanning trees. It starts with one cell in the maze, then performs random walks from unvisited cells until they hit the existing maze. Any loops formed during the walk are erased. This creates unbiased mazes where every possible maze has an equal probability of being generated.
Wilson:
addRandomCellToMaze()
while (unvisitedCells exist) {
current = randomUnvisitedCell()
path = [current]
while (current not in maze) {
neighbor = randomNeighbor(current)
if (neighbor in path) {
// Erase loop: remove path from neighbor to end
index = path.indexOf(neighbor)
path = path.slice(0, index + 1)
} else {
path.add(neighbor)
}
current = neighbor
}
// Add entire loop-erased path to maze
for (i = 0; i < path.length - 1; i++) {
removeWall(path[i], path[i+1])
addToMaze(path[i])
}
}
This project demonstrates professional software engineering principles with a focus on clean code, maintainability, and extensibility. Multiple design patterns work together to create a robust architecture.
All maze generation algorithms implement the IMazeGenerator interface, allowing them to be
used interchangeably. This enables runtime selection of algorithms without coupling the client code to
specific implementations.
public interface IMazeGenerator
{
event EventHandler<CellEventArgs>? OnCurrentCellChanged;
event EventHandler<WallEventArgs>? OnWallRemoved;
void Generate(Maze maze);
}
Each algorithm publishes events (OnCurrentCellChanged, OnWallRemoved)
that observers (the UI) can subscribe to. This decouples the algorithm logic from the visualization
layer, enabling real-time updates without tight coupling.
OnCurrentCellChanged?.Invoke(
this, new CellEventArgs(x, y));
OnWallRemoved?.Invoke(
this, new WallEventArgs(x1, y1, x2, y2));
Explore key components of the codebase that demonstrate clean coding practices and design patterns.
using MazeLogic.Events;
using MazeLogic.Models;
namespace MazeLogic.Interfaces;
/// <summary>
/// Defines the contract for maze generation algorithms.
/// Implements Strategy Pattern for interchangeable algorithms.
/// </summary>
public interface IMazeGenerator
{
/// <summary>
/// Raised when the algorithm moves to a new cell during generation.
/// </summary>
event EventHandler<CellEventArgs>? OnCurrentCellChanged;
/// <summary>
/// Raised when a wall is removed between two cells.
/// </summary>
event EventHandler<WallEventArgs>? OnWallRemoved;
/// <summary>
/// Generates a maze using the specific algorithm implementation.
/// </summary>
void Generate(Maze maze);
}
namespace MazeLogic.Models;
public class Maze
{
public int Width { get; }
public int Height { get; }
public Cell[,] Grid { get; }
public Maze(int width, int height)
{
Width = width;
Height = height;
Grid = new Cell[height, width];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
Grid[y, x] = new Cell(x, y);
}
}
}
public Cell GetCell(int x, int y) => Grid[y, x];
public List<Cell> GetNeighboringCells(int x, int y)
{
var neighbors = new List<Cell>();
if (x > 0) neighbors.Add(GetCell(x - 1, y));
if (x < Width - 1) neighbors.Add(GetCell(x + 1, y));
if (y > 0) neighbors.Add(GetCell(x, y - 1));
if (y < Height - 1) neighbors.Add(GetCell(x, y + 1));
return neighbors;
}
public void RemoveWallBetween(Cell a, Cell b)
{
if (a.X == b.X) // Vertical neighbors
{
if (a.Y == b.Y + 1)
{
a.TopWall = false;
b.BottomWall = false;
}
else if (a.Y == b.Y - 1)
{
a.BottomWall = false;
b.TopWall = false;
}
}
else if (a.Y == b.Y) // Horizontal neighbors
{
if (a.X == b.X + 1)
{
a.LeftWall = false;
b.RightWall = false;
}
else if (a.X == b.X - 1)
{
a.RightWall = false;
b.LeftWall = false;
}
}
}
}
using MazeLogic.Events;
using MazeLogic.Interfaces;
using MazeLogic.Models;
using System.Numerics;
namespace MazeLogic.GenerationAlgorithms;
public class RecursiveBacktracking : IMazeGenerator
{
public event EventHandler<CellEventArgs>? OnCurrentCellChanged;
public event EventHandler<WallEventArgs>? OnWallRemoved;
public void Generate(Maze maze)
{
int startX = Random.Shared.Next(0, maze.Width);
int startY = Random.Shared.Next(0, maze.Height);
GenerateWithStack(maze, startX, startY);
}
private void GenerateWithStack(Maze maze, int startX, int startY)
{
var stack = new Stack<Vector2>();
stack.Push(new Vector2(startX, startY));
while (stack.Count > 0)
{
Vector2 currentPosition = stack.Peek();
int x = (int)currentPosition.X;
int y = (int)currentPosition.Y;
// Notify observers of current cell
OnCurrentCellChanged?.Invoke(this, new CellEventArgs(x, y));
Cell currentCell = maze.GetCell(x, y);
if (!currentCell.IsVisited)
currentCell.IsVisited = true;
// Find unvisited neighbors
var unvisitedNeighbors = maze.GetNeighboringCells(x, y)
.Where(c => !c.IsVisited)
.ToList();
if (unvisitedNeighbors.Count > 0)
{
// Choose random neighbor and carve path
Cell randomNeighbor = unvisitedNeighbors[Random.Shared.Next(unvisitedNeighbors.Count)];
maze.RemoveWallBetween(currentCell, randomNeighbor);
OnWallRemoved?.Invoke(this, new WallEventArgs(x, y, randomNeighbor.X, randomNeighbor.Y));
stack.Push(new Vector2(randomNeighbor.X, randomNeighbor.Y));
}
else
{
// Backtrack
stack.Pop();
}
}
}
}
using MazeLogic.Events;
using MazeLogic.Interfaces;
using MazeLogic.Models;
namespace MazeLogic.GenerationAlgorithms;
public class Kruskal : IMazeGenerator
{
public event EventHandler<CellEventArgs>? OnCurrentCellChanged;
public event EventHandler<WallEventArgs>? OnWallRemoved;
private readonly Dictionary<Cell, Cell> parent = [];
public void Generate(Maze maze)
{
Random rng = new();
var walls = new List<Wall>();
// Initialize union-find structure
for (int y = 0; y < maze.Height; y++)
{
for (int x = 0; x < maze.Width; x++)
{
var cell = maze.GetCell(x, y);
parent[cell] = cell;
}
}
// Create list of all possible walls with random weights
for (int y = 0; y < maze.Height; y++)
{
for (int x = 0; x < maze.Width; x++)
{
var cell = maze.GetCell(x, y);
if (x < maze.Width - 1)
{
var rightNeighbor = maze.GetCell(x + 1, y);
walls.Add(new Wall(cell, rightNeighbor, rng.NextDouble()));
}
if (y < maze.Height - 1)
{
var bottomNeighbor = maze.GetCell(x, y + 1);
walls.Add(new Wall(cell, bottomNeighbor, rng.NextDouble()));
}
}
}
// Sort walls by weight (randomized)
walls = walls.OrderBy(w => w.Weight).ToList();
// Process walls in random order
foreach (var wall in walls)
{
var root1 = Find(wall.Cell1);
var root2 = Find(wall.Cell2);
// If cells are in different sets, join them
if (root1 != root2)
{
Union(root1, root2);
maze.RemoveWallBetween(wall.Cell1, wall.Cell2);
OnCurrentCellChanged?.Invoke(this, new CellEventArgs(wall.Cell1.X, wall.Cell1.Y));
OnWallRemoved?.Invoke(this, new WallEventArgs(
wall.Cell1.X, wall.Cell1.Y, wall.Cell2.X, wall.Cell2.Y));
}
}
}
// Union-Find with path compression
private Cell Find(Cell cell)
{
if (parent[cell] != cell)
parent[cell] = Find(parent[cell]); // Path compression
return parent[cell];
}
private void Union(Cell root1, Cell root2) => parent[root2] = root1;
}
namespace MazeLogic.Events;
/// <summary>
/// Event data for cell-related events during maze generation.
/// Uses init-only properties for immutability.
/// </summary>
public class CellEventArgs : EventArgs
{
public int X { get; init; }
public int Y { get; init; }
public CellEventArgs(int x, int y)
{
X = x;
Y = y;
}
}
/// <summary>
/// Event data for wall removal events during maze generation.
/// </summary>
public class WallEventArgs : EventArgs
{
public int X1 { get; init; }
public int Y1 { get; init; }
public int X2 { get; init; }
public int Y2 { get; init; }
public WallEventArgs(int x1, int y1, int x2, int y2)
{
X1 = x1;
Y1 = y1;
X2 = x2;
Y2 = y2;
}
}