randomfox (randomfox) wrote,
randomfox
randomfox

Maze Generator in C#

This program generates mazes. It works by starting at an empty cell and doing a random walk, drawing a wall as it goes, until it collides with another maze wall. It does that until all the cells are full.


screenshot

// mazegen - Generates mazes.
//
// Author: Po Shan Cheah
// Last updated: August 19, 2003
// 
// Compile with: csc /doc:mazegen.xml mazegen.cs

namespace MazeGen {

using System;
using System.Drawing;
using System.Windows.Forms;

class Maze {
    const int UP = 1;
    const int LEFT = 2;
    const int RIGHT = 4;
    const int DOWN = 8;

    // The maze array.
    int[,] maze;
    int rows;
    int cols;

    // Number of cells remaining to be filled.
    int unused;

    public Maze(int rows, int cols) {
	this.rows = rows;
	this.cols = cols;

	maze = new int[cols, rows];

	for (int i = 0; i < cols; ++i)
	    for (int j = 0; j < rows; ++j)
		maze[i, j] = 0;

	// corners
	maze[0, 0] = DOWN | RIGHT;
	maze[cols-1, 0] = DOWN | LEFT;
	maze[0, rows-1] = UP | RIGHT;
	maze[cols-1, rows-1] = UP | LEFT;

	// Sides
	for (int i = 1; i < rows - 1; ++i) {
	    maze[0, i] = DOWN | UP;
	    maze[cols-1, i] = DOWN | UP;
	}
	for (int i = 1; i < cols - 1; ++i) {
	    maze[i, 0] = LEFT | RIGHT;
	    maze[i, rows-1] = LEFT | RIGHT;
	}

	// Openings
	maze[cols/2, 0] = UP | LEFT;
	maze[cols/2 + 1, 0] = UP | RIGHT;
	maze[cols/2, rows-1] = DOWN | LEFT;
	maze[cols/2 + 1, rows-1] = DOWN | RIGHT;

	unused = (cols - 2) * (rows - 2);

	Generate();
	Draw();
    }

    // Offscreen buffer.
    Bitmap image;
    Graphics graphics;

    /// <summary>Draw the maze on an offscreen image buffer.</summary>
    void Draw() {
	const int CELLSIZE = 8;

	int xsize = cols * CELLSIZE;
	int ysize = rows * CELLSIZE;

	image = new Bitmap(xsize, ysize);
	graphics = Graphics.FromImage(image);

	graphics.Clear(Color.DarkBlue);

	Pen pen = new Pen(Color.Yellow);

	for (int i = 0; i < cols; ++i)
	    for (int j = 0; j < rows; ++j) {
		int room = maze[i, j];
		if ((room & UP) != 0)
		    graphics.DrawLine(pen,
				      i * CELLSIZE + CELLSIZE/2,
				      j * CELLSIZE,
				      i * CELLSIZE + CELLSIZE/2,
				      j * CELLSIZE + CELLSIZE/2 - 1);
		if ((room & DOWN) != 0)
		    graphics.DrawLine(pen,
				      i * CELLSIZE + CELLSIZE/2,
				      j * CELLSIZE + CELLSIZE/2,
				      i * CELLSIZE + CELLSIZE/2,
				      j * CELLSIZE + CELLSIZE - 1);
		if ((room & RIGHT) != 0)
		    graphics.DrawLine(pen, 
				      i * CELLSIZE + CELLSIZE/2,
				      j * CELLSIZE + CELLSIZE/2,
				      i * CELLSIZE + CELLSIZE - 1,
				      j * CELLSIZE + CELLSIZE/2);
		if ((room & LEFT) != 0)
		    graphics.DrawLine(pen,
				      i * CELLSIZE,
				      j * CELLSIZE + CELLSIZE/2,
				      i * CELLSIZE + CELLSIZE/2 - 1,
				      j * CELLSIZE + CELLSIZE/2);
	    }
    }

    /// <summary>Paint the image onto the specified graphics
    /// context</summary>
    public void Paint(Graphics g, int x, int y) {
	g.DrawImage(image, x, y);
    }

    /// <summary>Generate a random maze.</summary>
    /// <remarks>We start at an empty cell in the maze. We create a wall by
    /// doing a random walk through the maze. When we hit an existing wall,
    /// we stop. 
    /// <para>Theory: Since the walk started on an empty cell, that path
    /// can never bisect the maze and so there should still be a solution
    /// to the maze after we add each wall.</para></remarks>

    void Generate() {
	// Maximum number of steps to save so that the maze crawler
	// does not close back on itself. Low values may affect the
	// quality of the maze.
	const int Nsave = 20;

	// Array keeping track of the directions the random walk took in
	// the past Nsave steps.
	int[] prevNdir = new int[Nsave];

	// Table stating what we should add to the current row
	// and column to move in that direction.
	int[,] moveTable = {{0, -1}, {-1, 0}, {1, 0}, {0, 1}};
	    
	// The location of the random walker.
	int curx, cury;

	Random random = new Random();

	while (unused > 0) {

	    // Find an empty cell.
	    do {
		curx = random.Next(cols);
		cury = random.Next(rows);
	    } while (maze[curx, cury] != 0);

	    for (int i = 0; i < Nsave; ++i)
		prevNdir[i] = 0;
	    --unused;

	    for (;;) {
		// 1 << dir will be UP, DOWN, LEFT, RIGHT.
		// Those direction constants are chosen so that
		// the dir values for UP and DOWN and the xor of
		// each other and so are the dir values for LEFT
		// and RIGHT.
		int dir;

		for (int i = 0; i < Nsave - 1; ++i)
		    prevNdir[i] = prevNdir[i+1];

		bool[] gotdir = new bool[4];

		do {
		    // Choose a direction that doesn't go back on the
		    // previous step.
		    do {
			dir = random.Next(4);
		    } while (dir == (prevNdir[Nsave - 2] ^ 3));
		    prevNdir[Nsave - 1] = dir;

		    for (int i = 0; i < 4; ++i)
			gotdir[i] = false;
		    for (int i = 0; i < Nsave; ++i)
			gotdir[prevNdir[i]] = true;

		    // Avoid loops within the past nsave steps by
		    // making sure not all four directions are used.
		    // This is more restrictive than necessary but 
		    // it works okay.
		} while (gotdir[0] && gotdir[1] && gotdir[2] && gotdir[3]);

		// Add the wall.
		maze[curx, cury] |= (1 << dir);

		// Go to the next cell.
		curx += moveTable[dir, 0];
		cury += moveTable[dir, 1];

		// Reached another wall?
		bool exitcond = (maze[curx, cury] != 0);

		// Add wall segment needed to join with the previous
		// cell.
		maze[curx, cury] |= (1 << (dir ^ 3));

		if (exitcond)
		    break;

		--unused;
	    }
	}
    } // Generate
	
} // class Maze

class MazeGen : Form {

    // How many pixels down is the maze drawing area from the top of the
    // client rectangle.
    const int TopMargin = 30;

    TextBox rowsfield;
    TextBox colsfield;

    Maze maze = null;

    /// <summary>Event handler for the Go button.</summary>
    void go_Click(object o, EventArgs e) {
	int rows;
	int cols;
	try {
	    rows = int.Parse(rowsfield.Text);
	    cols = int.Parse(colsfield.Text);
	}
	catch (FormatException) {
	    MessageBox.Show("Invalid numeric value entered.");
	    return;
	}

	// The maze breaks down for really small dimensions. The upper
	// limit is to put a cap on the run time.

	const int LowerBound = 10;
	const int UpperBound = 300;

	if (rows < LowerBound || cols < LowerBound) {
	    MessageBox.Show("Out of range\nNumber of rows and columns " +
			    "must be at least " + LowerBound.ToString());
	    return;
	}

	if (rows > UpperBound || cols > UpperBound) {
	    MessageBox.Show("Out of range\nNumber of rows and columns " +
			    "must be below " + UpperBound.ToString());
	    return;
	}

	maze = new Maze(rows, cols);

	Invalidate();
	Update();
    } // go_Click

    /// <summary>Paint event handler.</summary>
    protected override void OnPaint(PaintEventArgs e) {
	e.Graphics.FillRectangle(new SolidBrush(Color.DarkBlue), 
				 0, TopMargin, 
				 ClientRectangle.Width, 
				 ClientRectangle.Height - TopMargin);
	if (maze != null)
	    maze.Paint(e.Graphics, 0, TopMargin);
    }

    MazeGen() {
	Text = "MazeGen";
	Name = "MazeGen";

	// Activate double-buffering.
	SetStyle(ControlStyles.UserPaint, true);
	SetStyle(ControlStyles.AllPaintingInWmPaint, true);
	SetStyle(ControlStyles.DoubleBuffer, true);

	ClientSize = new Size(500, 500);

	Label l1 = new Label();
	l1.Text = "Rows:";
	l1.TextAlign = ContentAlignment.MiddleRight;
	l1.Location = new Point(0, 0);
	l1.Size = new Size(50, 25);

	Controls.Add(l1);

	rowsfield = new TextBox();
	rowsfield.Text = "54";
	rowsfield.MaxLength = 4;
	rowsfield.Location = new Point(60, 3);
	rowsfield.Size = new Size(50, 25);
	
	Controls.Add(rowsfield);

	Label l2 = new Label();
	l2.Text = "Columns:";
	l2.TextAlign = ContentAlignment.MiddleRight;
	l2.Location = new Point(110, 0);
	l2.Size = new Size(75, 25);

	Controls.Add(l2);

	colsfield = new TextBox();
	colsfield.Text = "61";
	colsfield.MaxLength = 4;
	colsfield.Location = new Point(195, 3);
	colsfield.Size = new Size(50, 25);

	Controls.Add(colsfield);

	Button goButton = new Button();
	goButton.Text = "Go";
	goButton.Location = new Point(275, 0);
	goButton.Size = new Size(50, 25);
	goButton.Click += new EventHandler(go_Click);

	Controls.Add(goButton);
	AcceptButton = goButton;

	// Call the event handler on startup so that the window appears
	// with a maze.
	go_Click(null, null);
    }

    static int Main() {
	Application.Run(new MazeGen());
	return 0;
    }
} // class MazeGen

}
// The End

Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments