Recursive Randomized Maze Generator

Hello everyone!
It’s been a while since I’ve posted something here, and the fact is that I didn’t come up with very creative ideas for good Processing projects, and when I came up with them they just were too complicated for me to code or I did lose interest (or time) to work on them (I’m still a student and since last year I wasn’t even attending uni). Also, I’m more used to code in C++ for school, so I use Processing very rarely and when I need to do something very graphically-complex (something I’m still unable to do in C++ for lack of knowledge on graphic libraries).

Anyways, now I have a bunch of free time between university exams, and in this weekend I programed this little beauty: a recursive randomized maze generator! (Here’s an image of what it looks like:)


First of all, I’m going to say that this is not my first attempt on this project. I already coded a similar thing in the past (about 2 or 3 years ago), but it was much slower (it didn’t have the recursive function that allows this “beast” to be ultra-fast) and a little buggy too. Also, that project was kind of a ripoff from a tutorial I found on YouTube, so it wasn’t even my project.
Then I tried to redo my work, this time focusing on the logic besides it and working not to follow any tutorial. It had to be mine. But I was hopeless, after 2 months of head-scratching and buggy things I almost forgot of its existence and decided to abandon the project.
But now, after almost 3 years from its predecessors, I found that fossils on my PC and decided to give them new birth, and after just 3 days of serious coding the very good version of this work is DONE!

With this program, you’ll be able to run a completely randomized search through a quite large grid of square cells, and “dig” onto it a randomized maze that you’ll be able to solve using keyboard input (I kinda dislike working with sprites, and I really like when a program is clean and monochrome, so its graphics side is not on the fancy side).
The cool fact is that it’s programmed using a recursive function that searches over 800 cells, so I think I did a good job on keeping the code clean and fast (something I don’t necessarily do every time, really proud of that) and it’s almost instant-generated (which is a very good upside, you don’t have to wait for ages in order to get it to generate a new randomized maze).
Also, I kind of made it’s generation custom adding a slider (from the ControlP5 library) that allows to have a “linearity” parameter on the maze generation, which controls how much the single paths will tend to be straight instead of looping on itself resulting in a more complicated maze.

And most importantly, it will be very easy for me (and for you, if you’ll want to add something to it) to debug and maintain the code, because for once in my life I remembered to comment almost every single line! (Another great problem that keeps me out from almost every big project I take on: I always forget to comment code, so every time I forget what a function or a line does and I’m no longer able to expand it in a good way, and it always comes up as a big, not-working mess of code)

I’m also going to add that it will be my job to keep it updated with some cool new features that I want to add, the proudest of them all being able to create a file to save a good maze layout you’ve solved in a file and be able to load it and play it every time you want to. I think I already have kind of an idea on how to make it, but I don’t have implemented it yet.

Now, here’s the code for the project. I sincerely hope that no-one who sees this is about to steal it and copy the code somewhere else pretending it’s his work, because it would be just unfair, but I’m assuming everyone who sees this project is a good person enough not to do it.
In order to make it work, you’ll just put these 4 files (saved as .pde, obviously) in a single folder, and name the folder with the same name as the file containing the void setup() and void draw() functions. Then, you’ll open the first file (the one containing this 2 functions) and run the code. In this way there will be no issues on having all the files being misread from the compiler, and everything will work just fine.
Remember also to install the ControlP5 library if you didn’t install it before. It’s necessary for the main menu shown when you run the program.

Enough talking, here’s the files!

First file (main)

import controlP5.*;
ControlP5 controls;

final int gridOffset=25, sliderDim=120, buttonDim=80;
float linearity=0.5;
int xStart=0, yStart=0;
boolean startMenu=true, mazeSolved=false;

Cell grid[][], cell;  
ArrayList<Cell> path=new ArrayList<Cell>();
Player player, target;

void keyTyped() {

  if (!mazeSolved) player.move(grid, key);
}

void setup() {

  size(750, 750);
  textAlign(CENTER, CENTER);
  controls=new ControlP5(this);
  controls.addSlider("linearity").setPosition(width/2-sliderDim/2, height/2+80).setSize(sliderDim, 30).setLabel("").setRange(0, 0.99);
  controls.addButton("generate").setPosition(width/2-buttonDim/2, height/2+200).setSize(buttonDim, 20).setLabel("Generate Maze");

  //setting player at starting position, target at the
  //right-bottom corner of the maze
  player=new Player(xStart, yStart, gridOffset);
  target=new Player(width/gridOffset-1, height/gridOffset-1, gridOffset);

  //setup for the grid
  grid=new Cell[width/gridOffset][height/gridOffset];
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) grid[a][b]=new Cell(a, b, gridOffset);
} 

void draw() {

  background(40);
  stroke(128);
  if (startMenu) {

    background(255);
    fill(0);
    textSize(50);
    text("Maze Generator!", width/2, height/2);
    textSize(15);
    text("Use this slider to adjust the linearity of the maze\n(0 is more convoluted, 1 is more linear", width/2, height/2+135);
  } else if (!mazeSolved) {

    //displaying background and maze grid
    drawGrid(width, height, gridOffset);
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++)
        if (grid[a][b].visited) grid[a][b].display(color(100), color(255));

    if (!player.checkSolved(target)) {

      //displaying the player and the target
      player.display(color(0, 50, 200));
      if (grid[target.x][target.y].visited) target.display(color(200, 0, 0));

      //redrawing the borders for the occupied cells
      grid[player.x][player.y].displayBorders(color(255));
      grid[target.x][target.y].displayBorders(color(255));
    } else {

      //maze has been solved, change
      //display menu
      mazeSolved=true;
      textSize(75);
    }
  } else if (mazeSolved) {

    //drawing the background grid for the non-visited cells
    drawGrid(width, height, gridOffset);

    //drawing the maze
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++)
        if (grid[a][b].visited) grid[a][b].display(color(100), color(255));

    //drawing player and endwall
    player.display(color(0, 200, 50));
    grid[player.x][player.y].displayBorders(color(255));
    strokeText("Maze solved!", 2, width/2, height/2, color(0), color(255));
  }
}

//function to print a background grid (will
//not be visible most cases, because the maze
//will be printed above it)
void drawGrid(int w, int h, int off) {

  for (int a=0; a<w-1; a+=off)
    for (int b=0; b<h-1; b+=off) {

      line(0, a, width, a);
      line(b, 0, b, height);
    }
}

//function to print a text string with some border of different color
void strokeText(String t, int dim, int x, int y, color bCol, color tCol) {

  fill(bCol);
  text(t, x-dim, y);
  text(t, x+dim, y);
  text(t, x, y-dim);
  text(t, x, y+dim);
  fill(tCol);
  text(t, x, y);
}

//function called from the starting menu button
void generate() {

  //hiding button and slider
  controls.getController("linearity").hide();
  controls.getController("generate").hide();

  //setting as visited the first cell, adding it to the path
  grid[xStart][yStart].visited=true;
  path.add(grid[xStart][yStart]);

  //generating a new maze
  cell=mazeGen(grid, grid[xStart][yStart], (width/gridOffset)*(height/gridOffset)-1, 0); //the last number is the length of the path
  println("Maze generated correctly.");

  //checking for the number of non-visited cells
  int notVis=0;
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!grid[a][b].visited) notVis++;

  //buffering the number of non visited cells
  //if there's more than 0, display a warning message
  if (notVis!=0) print("WARNING: ");
  println(notVis+" cells have not been visited.");
  
  //change display menu
  startMenu=false;
}

Second file (Cell class)

class Cell {

  int x, y; //position
  int dim; //dimension
  boolean visited=false; //check for being visited (on the grid)
  boolean[] walls={true, true, true, true}; //east, south, west, north

  //used to display the single cell on the grid
  void display(color f, color s) {

    fill(f);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);

    //I need to separate the cell from its borders
    //because it might not have every border marked as "true"
    this.displayBorders(s);
  }

  //used to display the borders of a particular cell
  void displayBorders(color s) {

    stroke(s);

    //if a particular border is true (it appears on the actual maze),
    //I print it on the screen in the correct position
    if (this.walls[0]) line(this.x*this.dim+this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[1]) line(this.x*this.dim, this.y*this.dim+this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[2]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim, this.y*this.dim+this.dim);
    if (this.walls[3]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim);
  }

  //used to create a new Cell object that is the
  //copy of another object with some properties
  Cell copyCell() {

    Cell b=new Cell(this.x, this.y, this.dim);
    b.visited=this.visited;
    for (int a=0; a<this.walls.length; a++) b.walls[a]=this.walls[a];
    return b;
  }

  Cell(int a, int b, int c) { //constructor

    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

//this calculates the number of free available
//adjacent cells to generate the path on
int numNb(Cell c, Cell[][] g) {

  int n=0; //start at 0
  for (int a=(c.x<1 ? c.x : c.x-1); a<=(c.x>=width/c.dim-1 ? c.x : c.x+1); a++)
    for (int b=(c.y<1 ? c.y : c.y-1); b<=(c.y>=height/c.dim-1 ? c.y : c.y+1); b++)

      //if the cell on the same X or Y axis has been visited, add it to
      //the list of occupied adjacent cells
      if ((a==c.x || b==c.y) && !g[a][b].visited) n++;

  //return the number of non-free cells
  return n;
}

Third file (function that generates the maze)

Cell mazeGen(Cell[][] g, Cell c, int depth, int direction) {

  Cell next=new Cell(-1, -1, c.dim);
  int dir=0, neighbours=numNb(c, g);
  boolean blindSpot=false;
  boolean tried0=false, tried1=false, tried2=false, tried3=false;

  //if I can go in any direction from the cell I'm now in,
  //I start a random search for a new cell available
  if (neighbours>=0) do {

    //random chance to keep the direction I was coming from
    if (random(0, 1)<linearity) dir=direction;
    else dir=(int)random(0, 4); //generate random direction

    //checking for already visited directions
    if (dir==0) tried0=true;
    else if (dir==1) tried1=true;
    else if (dir==2) tried2=true;
    else if (dir==3) tried3=true;

    //check for boundaries conditions and generate new cell
    if (dir==0 && c.x<width/c.dim-1 && !g[c.x+1][c.y].visited) next=g[c.x+1][c.y].copyCell();
    else if (dir==1 && c.y<height/c.dim-1 && !g[c.x][c.y+1].visited) next=g[c.x][c.y+1].copyCell();
    else if (dir==2 && c.x>0 && !g[c.x-1][c.y].visited) next=g[c.x-1][c.y].copyCell();
    else if (dir==3 && c.y>0 && !g[c.x][c.y-1].visited) next=g[c.x][c.y-1].copyCell();

    //if I do not reach a new cell, I've hit a blindspot
    if (tried0 && tried1 && tried2 && tried3) {

      blindSpot=true;
      break;
    }
  } while (next.x==-1 && next.y==-1);

  //otherwise, I've hit a blindspot
  //and I need to backtrack more
  else blindSpot=true;

  //if the cell has not updated, I reached
  //a spot in which I have no available adjacent cells;
  //I start backtracking on the path I built in order to get
  //to the last spot in which there is an available adjacent cell
  if (blindSpot) {

    next=path.get(path.size()-1).copyCell();
    path.remove(path.size()-1);
  } else {

    //update next cell
    g[c.x][c.y].walls[dir]=false;
    g[next.x][next.y].walls[(dir>=2 ? dir-2 : dir+2)]=false;
    g[next.x][next.y].visited=true;

    path.add(g[c.x][c.y]); //add the cell to the tracked path
    depth--; //go deeper in the generation process
  }

  if (checkFinished(g) || depth==0 || (next.x==0 && next.y==0)) {

    //finished generate maze for the desired depth
    return next;
  } else return mazeGen(g, next, depth, dir);
}

//checks if every cell of the grid has been visited
boolean checkFinished(Cell[][] g) {

  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!g[a][b].visited) return false;

  //return true if it doesn't find
  //any non-visited cell
  return true;
}

Fourth file (class to handle Player movements)

NOTE: This class is inherited from another project I took over,
it was a similar version of this program but it was much more slow and buggy.
I’m not intentioned to post it there, unless you ask me so :slight_smile:

/*
  CLASS INHERITED FROM ANOTHER PROJECT
  (modified to fit the purpose of the project)
*/

class Player {
  
  int x, y; //position
  int dim; //dimension
  
  //used to check if two Player objects have the same position
  boolean checkSolved(Player other) {
    
    return (this.x==other.x && this.y==other.y);
  }
  
  //used to move a single object using keyboard input (WASD or arrows)
  void move(Cell[][] grid, char k) {
    
    if ((k=='w' || (k==CODED && k==UP)) && !grid[this.x][this.y].walls[3]) this.y--;
    if ((k=='a' || (k==CODED && k==LEFT)) && !grid[this.x][this.y].walls[2]) this.x--;
    if ((k=='s' || (k==CODED && k==DOWN)) && !grid[this.x][this.y].walls[1]) this.y++;
    if ((k=='d' || (k==CODED && k==RIGHT)) && !grid[this.x][this.y].walls[0]) this.x++;
  }
  
  //used to display a Player objet on the screen
  void display(color c) {
    
    fill(c);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);
  }
  
  Player(int a, int b, int c) { //constructor
    
    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

Hope you’ll find interesting how I made it work, how I thought of coding every function inside it and most importantly you’ll enjoy solving paths over and over until this things has new functionalities I want to add. But for now, that’s it!

PS: For the very kind people out there who want to help me, I found out a little bug for which sometimes the maze won’t cover every single cell on the grid. You won’t have any issues on running the code, because I wanted this project to be published and I kind of hard-coded a solution for it, but you sometimes may generate an impossible maze (in which the ending cell that you need to reach, in order to solve the maze, is not generated) or find some “holes” here and there.
If you’re into help me, I will open another topic related to this bug fix, so you won’t need to spam this one of coding-related issues and use this one as a reference for the project. Thanks in advance!

4 Likes

Update 1.1 is already here!

Yep, you heard it right: I worked quite hard today because I basically had nothing better to do, so I just finished cleaning the code and making it ready for the first major update!
In this one there’s the addiction of 2 great functionalities, that I’m sure they fit perfectly for the kind of project this has become and are surely very useful: level creation and level saving!

With the first one, you’ll be able to open a unique level editor in which it’s really easy to track out a custom-made layout for the maze! There’s obviously no rules to imagination, so it’s possible also to create loops in the course (which are actually impossible to obtain in a random-generated level) or have incomplete layouts in which not every tile is generated. After you’ve finished designing your maze, you’ll obviously be able to play it and see if you can beat your own level.

The second phase to creation is level saving: every time you complete a random-generated level, or when you create a custom level (pressing the c key on the keyboard, before completing the maze) you’ll open the saving screen, in which you will have a text field where you’ll put the name of the level and, clicking the “Save” button below it, you’ll save the level in a .mgx file (I chose a brand new file format, invented by me, to avoid overlapping with existing format files and possible bad reading from the program or the computer itself, but if you open it with a text editing program you’ll discover it’s just plain text formatted in a clever way to allow the program to load it easier in future).

As always, I have trust in you not to steal my work and publish it as your own work, pretending it’s yours, so I’m putting the plain code for the compiler to be executed. From yesterday to now, I already added 5 files, and I’m sure they’ll begin to grow more and more to the point I won’t be able to fit them all in the same update notification post.
But for now, here’s the work I’ve done:

First file (main):

import controlP5.*;
ControlP5 controls;

//used font in the sketch
PFont font, c1Font, c2Font;

//global variables
final int gridOffset=25, sliderDim=120;
float linearity=0.5;
int xStart=0, yStart=0, prevDir=0, xPos, yPos;
boolean startMenu=true, mazeSolved=false, autoSolve=false, savingScreen=false, inCreation=false, inTrial=false;
String autoMode="Manual solve", savedList="", mazeName="";
String[] loadedFiles;

//global objects to handle the maze generation and solving process
Cell grid[][], genCell, currCell, prevCell;  
ArrayList<Cell> path=new ArrayList<Cell>();
Player player, target;
Solver autoSolver;

void setup() {

  //creating a new window
  size(750, 750);

  //setup for the used font
  font=loadFont("Formula1-Regular.vlw");
  c1Font=loadFont("Formula1-Display-Regular-10.vlw");
  c2Font=loadFont("Formula1-Display-Regular-15.vlw");
  textFont(font);
  textAlign(CENTER, CENTER);

  //setup for every ControlP5 object
  controls=new ControlP5(this);
  float dimension=width/2-sliderDim/2;
  controls.addSlider("linearity").setPosition(dimension, height/2+30).setSize(sliderDim, 30).setLabel("").setRange(0, 0.99);
  controls.addButton("generate").setPosition(width/2-15-sliderDim, height/2+140).setSize(sliderDim, 20).setLabel("Generate Maze").setFont(c1Font);
  controls.addButton("create").setPosition(width/2+15, height/2+140).setSize(sliderDim, 20).setLabel("Create Maze").setFont(c1Font);
  controls.addToggle("autoSolve").setPosition(dimension, height/2+220).setSize(sliderDim, 40).setMode(ControlP5.SWITCH).setLabel("").hide();
  controls.addTextfield("nameMaze").setPosition(width/2-sliderDim, height/2+60).setSize(2*sliderDim, 50).setFont(c2Font).setLabel("Insert maze name here:").hide();
  controls.addButton("saveMaze").setPosition(dimension, height/2+160).setSize(sliderDim, 40).setLabel("Save").setFont(c2Font).hide();
  controls.addButton("exit").setPosition(width-35, 5).setSize(30, 30);

  //setting player at starting position, target at the
  //right-bottom corner of the maze
  player=new Player(xStart, yStart, gridOffset);
  target=new Player(width/gridOffset-1, height/gridOffset-1, gridOffset);
  autoSolver=new Solver(player);
  currCell=new Cell(0, 0, gridOffset);
  prevCell=new Cell(0, 0, gridOffset);

  //setup for the grid
  grid=new Cell[width/gridOffset][height/gridOffset];
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) grid[a][b]=new Cell(a, b, gridOffset);
} 

void draw() {

  background(40);
  stroke(128);
  if (startMenu) {

    //setup for the main menu screen
    background(255);

    //title
    textSize(70);
    strokeText("Maze Generator!", 3, width/2, height/2-120, color(0, 50, 200), color(0, 200, 0));

    //text line 1
    textSize(15);
    fill(color(0, 80, 200));
    text("Use this slider to adjust the linearity of the maze\n(0 is more convoluted, 1 is more linear)", width/2, height/2);

    //text line 2
    textSize(20);
    fill(color(0, 80, 200));
    text("Currently on: "+autoMode, width/2, height/2+275);

    //updating automatic mode solution display
    if (autoSolve) autoMode="Automatic solve";
    else autoMode="Manual solve\n(WARNING: Automatic solve not working yet.)";
  } else if (inCreation || inTrial) {

    drawGrid(width, height, gridOffset);
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++)
        if (grid[a][b].visited) {

          if (grid[a][b].isRed) grid[a][b].display(color(100, 0, 0), color(255));
          else grid[a][b].display(color(100), color(255));
        }

    if (inTrial) {

      //drawing player and target position
      player.display(color(0, 50, 200));
      target.display(color(200, 0, 0));

      //redrawing the borders for the occupied cells
      grid[player.x][player.y].displayBorders(color(255));
      grid[target.x][target.y].displayBorders(color(255));
    }
  } else if (savingScreen) {

    background(255);
    strokeText("Insert the maze\nname here:", 2, width/2, height/2-80, color(0), color(0, 100, 255));
  } else if (!mazeSolved) {

    //displaying background and maze grid
    drawGrid(width, height, gridOffset);
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++)
        if (grid[a][b].visited && !inCreation) grid[a][b].display(color(100), color(255));

    //if the maze is not solved
    if (!player.checkSolved(target)) {
      if (!inCreation) {

        //displaying the player and the target
        player.display(color(0, 50, 200));
        if (grid[target.x][target.y].visited) target.display(color(200, 0, 0));

        //redrawing the borders for the occupied cells
        grid[player.x][player.y].displayBorders(color(255));
        grid[target.x][target.y].displayBorders(color(255));
      }
    } else {

      mazeSolved=true;
      controls.getController("nameMaze").show();
      controls.getController("saveMaze").show();
      textSize(70);
    }
  } else if (mazeSolved) {

    //drawing the background grid for the non-visited cells
    drawGrid(width, height, gridOffset);

    //drawing the maze
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++)
        if (grid[a][b].visited) grid[a][b].display(color(100), color(255));

    //drawing player and endwall
    player.display(color(0, 200, 50));
    grid[player.x][player.y].displayBorders(color(255));
    strokeText("Maze solved!", 3, width/2, height/2, color(0), color(255));
  }
}

//function to print a background grid (will
//not be visible most cases, because the maze
//will be printed above it)
void drawGrid(int w, int h, int off) {

  for (int a=0; a<w-1; a+=off)
    for (int b=0; b<h-1; b+=off) {

      line(0, a, width, a);
      line(b, 0, b, height);
    }
}

//function to print a text string with some border of different color
void strokeText(String t, float dim, int x, int y, color bCol, color tCol) {

  fill(bCol);
  text(t, x-dim, y);
  text(t, x+dim, y);
  text(t, x, y-dim);
  text(t, x, y+dim);
  fill(tCol);
  text(t, x, y);
}

Second file:

//function called from the starting menu button
//to create a custom maze
void create() {

  //hiding button and slider
  controls.getController("linearity").hide();
  controls.getController("generate").hide();
  controls.getController("create").hide();
  controls.getController("autoSolve").hide();
  inCreation=true;

  //change display menu
  startMenu=false;
}

//function called from the starting menu button
//to create a random maze and play it
void generate() {

  //hiding button and slider
  controls.getController("linearity").hide();
  controls.getController("generate").hide();
  controls.getController("autoSolve").hide();
  controls.getController("create").hide();

  //setting as visited the first cell, adding it to the path
  grid[xStart][yStart].visited=true;
  path.add(grid[xStart][yStart]);

  //generating a new maze
  genCell=mazeGen(grid, grid[xStart][yStart], (width/gridOffset)*(height/gridOffset)-1, 0); //the last number is the length of the path
  println("Maze generated correctly.");

  //checking for the number of non-visited cells
  int notVis=0;
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!grid[a][b].visited) notVis++;

  //buffering the number of non visited cells
  //if there's more than 0, display a warning message
  if (notVis!=0) print("WARNING: ");
  println(notVis+" cells have not been visited.");

  //clearing the path used to generate the maze
  path.clear();

  //change display menu
  startMenu=false;
}

Third file:

class Cell {

  int x, y; //position
  int pX, pY; //temporary position (used only during custom maze creation)
  int dim; //dimension
  boolean visited=false, isRed=false, created=false; //check for being visited (on the grid)
  boolean[] walls={true, true, true, true}; //east, south, west, north

  //used to display the single cell on the grid
  void display(color f, color s) {

    fill(f);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);

    //I need to separate the cell from its borders
    //because it might not have every border marked as "true"
    this.displayBorders(s);
  }

  //used to display the borders of a particular cell
  void displayBorders(color s) {

    stroke(s);

    //if a particular border is true (it appears on the actual maze),
    //I print it on the screen in the correct position
    if (this.walls[0]) line(this.x*this.dim+this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[1]) line(this.x*this.dim, this.y*this.dim+this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[2]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim, this.y*this.dim+this.dim);
    if (this.walls[3]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim);
  }

  //used to create a new Cell object that is the
  //copy of another object with some properties
  Cell copyCell() {

    Cell b=new Cell(this.x, this.y, this.dim);
    b.visited=this.visited;
    for (int a=0; a<this.walls.length; a++) b.walls[a]=this.walls[a];
    return b;
  }
  
  Cell (int a, int b) {
    
    this.pX=a;
    this.pY=b;
  }

  Cell(int a, int b, int c) { //constructor

    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

//this function is used to store temporary
//position of the cells
void saveCell(Cell c) {
  
  c.pX=c.x;
  c.pY=c.y;
}

//this function is used to update
//the position of the cells
void restoreCell(Cell c) {
  
  c.x=c.pX;
  c.y=c.pY;
}

//this calculates the number of free available
//adjacent cells to generate the path on
int numNb(Cell c, Cell[][] g) {

  int n=0; //start at 0
  for (int a=(c.x<1 ? c.x : c.x-1); a<=(c.x>=width/c.dim-1 ? c.x : c.x+1); a++)
    for (int b=(c.y<1 ? c.y : c.y-1); b<=(c.y>=height/c.dim-1 ? c.y : c.y+1); b++)

      //if the cell on the same X or Y axis has been visited, add it to
      //the list of occupied adjacent cells
      if ((a==c.x || b==c.y) && !g[a][b].visited) n++;

  //return the number of non-free cells
  return n;
}

Fourth file:

void mousePressed() {

  if (inCreation && mouseButton==RIGHT) { //deleting a cell on the screen

    //saving current mouse position on the grid
    if (mouseX>0 && mouseX<width) xPos=mouseX/gridOffset;
    if (mouseY>0 && mouseY<height) yPos=mouseY/gridOffset;

    //deleting cell
    grid[xPos][yPos].visited=false;

    //removing walls from the cell and from the adjacent ones
    for (int a=0; a<4; a++) grid[xPos][yPos].walls[a]=true;
    if (grid[xPos-1][yPos].visited) grid[xPos-1][yPos].walls[0]=true;
    if (grid[xPos][yPos-1].visited) grid[xPos][yPos-1].walls[1]=true;
    if (grid[xPos+1][yPos].visited) grid[xPos+1][yPos].walls[2]=true;
    if (grid[xPos][yPos+1].visited) grid[xPos][yPos+1].walls[3]=true;
  }
}

void mouseDragged() {

  if (inCreation) { //custom maze generation

    //saving current mouse position on the grid
    if (mouseX>0 && mouseX<width) xPos=mouseX/gridOffset;
    if (mouseY>0 && mouseY<height) yPos=mouseY/gridOffset;

    //if currCell has moved on the screen
    if (currCell.x!=xPos || currCell.y!=yPos) {

      //calculating position
      currCell.pX=xPos;
      currCell.pY=yPos;
      prevCell.pX=currCell.x;
      prevCell.pY=currCell.y;

      //updating position
      restoreCell(currCell);
      restoreCell(prevCell);
    }

    //updating the cell in the position of the mouse
    if (mouseButton==LEFT) grid[xPos][yPos].visited=true;

    //updating walls
    if (prevCell.x==currCell.x+1 && prevCell.y==currCell.y) {

      grid[currCell.x][currCell.y].walls[0]=false;
      grid[prevCell.x][prevCell.y].walls[2]=false;
    } else if (prevCell.x==currCell.x && prevCell.y==currCell.y+1) {

      grid[currCell.x][currCell.y].walls[1]=false;
      grid[prevCell.x][prevCell.y].walls[3]=false;
    } else if (prevCell.x==currCell.x-1 && prevCell.y==currCell.y) {

      grid[currCell.x][currCell.y].walls[2]=false;
      grid[prevCell.x][prevCell.y].walls[0]=false;
    } else if (prevCell.x==currCell.x && prevCell.y==currCell.y-1) {

      grid[currCell.x][currCell.y].walls[3]=false;
      grid[prevCell.x][prevCell.y].walls[1]=false;
    }

    //setting prevCell as red
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++) grid[a][b].isRed=false;
    grid[currCell.x][currCell.y].isRed=true;
  }
}

//used for keyboard input
void keyTyped() {

  if (!mazeSolved && !autoSolve) player.move(grid, key);
  if (inCreation) {
    if (key=='p') {
      
      //play mode: the custom maze is
      //no longer editable and is ready to play
      inCreation=false;
      player.x=0;
      player.y=0;
      target.x=width/gridOffset-1;
      target.y=height/gridOffset-1;
    } else if (key=='c') {

      //create mode: the maze is ready to be saved in
      //a .mgx file and exported (as simple text)
      textSize(60);
      inCreation=false;
      savingScreen=true;
      controls.getController("nameMaze").show();
      controls.getController("saveMaze").show();
    } 
    
    //enable/disable trial mode: you can playtest the maze
    //while creating it to help you create the maze
    else if (key=='t') inTrial=!inTrial;
  }
}

Fifth file:

Cell mazeGen(Cell[][] g, Cell c, int depth, int direction) {

  Cell next=new Cell(-1, -1, c.dim);
  int dir=0, neighbours=numNb(c, g);
  boolean blindSpot=false;
  boolean tried0=false, tried1=false, tried2=false, tried3=false;

  //if I can go in any direction from the cell I'm now in,
  //I start a random search for a new cell available
  if (neighbours>=0) do {

    //random chance to keep the direction I was coming from
    if (random(0, 1)<linearity) dir=direction;
    else dir=(int)random(0, 4); //generate random direction

    //checking for already visited directions
    if (dir==0) tried0=true;
    else if (dir==1) tried1=true;
    else if (dir==2) tried2=true;
    else if (dir==3) tried3=true;

    //check for boundaries conditions and generate new cell
    if (dir==0 && c.x<width/c.dim-1 && !g[c.x+1][c.y].visited) next=g[c.x+1][c.y].copyCell();
    else if (dir==1 && c.y<height/c.dim-1 && !g[c.x][c.y+1].visited) next=g[c.x][c.y+1].copyCell();
    else if (dir==2 && c.x>0 && !g[c.x-1][c.y].visited) next=g[c.x-1][c.y].copyCell();
    else if (dir==3 && c.y>0 && !g[c.x][c.y-1].visited) next=g[c.x][c.y-1].copyCell();

    //if I do not reach a new cell, I've hit a blindspot
    if (tried0 && tried1 && tried2 && tried3) {

      blindSpot=true;
      break;
    }
  } while (next.x==-1 && next.y==-1);

  //otherwise, I've hit a blindspot
  //and I need to backtrack more
  else blindSpot=true;

  //if the cell has not updated, I reached
  //a spot in which I have no available adjacent cells;
  //I start backtracking on the path I built in order to get
  //to the last spot in which there is an available adjacent cell
  if (blindSpot) {

    next=path.get(path.size()-1).copyCell();
    path.remove(path.size()-1);
  } else {

    //update next cell
    g[c.x][c.y].walls[dir]=false;
    g[next.x][next.y].walls[(dir>=2 ? dir-2 : dir+2)]=false;
    g[next.x][next.y].visited=true;

    path.add(g[c.x][c.y]); //add the cell to the tracked path
    depth--; //go deeper in the generation process
  }

  if (checkFinished(g) || depth==0 || (next.x==0 && next.y==0)) {

    //finished generate maze for the desired depth
    return next;
  } else return mazeGen(g, next, depth, dir);
}

//checks if every cell of the grid has been visited
boolean checkFinished(Cell[][] g) {

  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!g[a][b].visited) return false;

  //return true if it doesn't find
  //any non-visited cell
  return true;
}

Sixth file (still not implemented):

/*

WARNING: This functionality is currently "work in progress",
so it still isn't implemented in the code and will not be
accessible in any way.

*/

//used to load a pre-saved maze from the folder "savedFiles"
//(it gets a strings array as input because that's the output
//you get when you save a text file with Processing)
void loadMaze(String[] s) {
  
  
}

Seventh file:

/*
  CLASS INHERITED FROM ANOTHER PROJECT
  (modified to fit the purpose of the project)
*/

class Player {
  
  int x, y; //position
  int dim; //dimension
  
  //used to check if two Player objects have the same position
  boolean checkSolved(Player other) {
    
    return (this.x==other.x && this.y==other.y);
  }
  
  //used to move a single object using keyboard input (WASD or arrows)
  void move(Cell[][] grid, char k) {
    
    if (k=='w' && !grid[this.x][this.y].walls[3]) this.y--;
    if (k=='a' && !grid[this.x][this.y].walls[2]) this.x--;
    if (k=='s' && !grid[this.x][this.y].walls[1]) this.y++;
    if (k=='d' && !grid[this.x][this.y].walls[0]) this.x++;
  }
  
  //used to display a Player objet on the screen
  void display(color c) {
    
    fill(c);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);
  }
  
  Player(int a, int b, int c) { //constructor
    
    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

Eighth file:

//used to save a new .mgx file to store the layout of the maze
//(if you want to manually edit the file after saving it, it is stored
//as simple text so you can open it with standard text editing programs)
String saveProcess(Cell[][] g) {

  String maze="Name: \""+controls.get(Textfield.class, "nameMaze").getText()+".mgx\""; //the string containing the maze data
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) {

      maze+=("["+a+"]["+b+"]={");
      if (g[a][b].visited) for (int c=0; c<4; c++) {

        //adding "true" if there is a wall, "false" if not;
        //returning an empty cell if it is not visited
        if (g[a][b].walls[c]) maze+="true";
        else maze+="false";
        if (c!=3) maze+=", ";
      }
      maze+="}\n";
    }

  //return the generated string containing the maze data
  return maze;
}

void clearMaze(Cell[][] g, Player p) {

  //clearing the maze
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) {
      g[a][b].visited=false;
      for (int c=0; c<4; c++) g[a][b].walls[c]=true;
    }

  //resetting player position
  p.x=0;
  p.y=0;
}

void saveMaze() {

  //saving the maze as a .mgx file
  //(name starts with the path of the "savedFiles" directory, in which
  //every custom-created maze is stored)
  saveStrings("./savedFiles/"+controls.get(Textfield.class, "nameMaze").getText()+".mgx", split(saveProcess(grid), '\n'));

  //updating the list of saved files in this list, which will be saved as plain text
  //in the same directory but in a different folder
  loadedFiles=loadStrings("./savedFiles/savedList/savedList.txt");
  loadedFiles=expand(loadedFiles, loadedFiles.length+1);
  loadedFiles[loadedFiles.length-1]="./savedFiles/"+controls.get(Textfield.class, "nameMaze").getText()+".mgx";
  saveStrings("./savedFiles/savedList/savedList.txt", loadedFiles);

  //updating visibility for the ControlP5 objects
  controls.getController("linearity").show();
  controls.getController("generate").show();
  controls.getController("create").show();
  controls.getController("saveMaze").hide();
  controls.getController("nameMaze").hide();
  
  //clearing the textfield for the maze name
  controls.get(Textfield.class,"nameMaze").clear();

  //changing menu and clearing the maze
  savingScreen=false;
  startMenu=true;
  clearMaze(grid, player);
}

Ninth file (still not implemented):

/*

WARNING: This functionality is currently "work in progress",
so it won't work and it has been manually hard-coded to not
be used while running the sketch.

*/

class Solver {

  //the automatic solver is driven by a Player object
  Player ai;
  Cell prevCell;

  Solver(Player p) { //constructor

    this.ai=p;
  }
}

//used to move the automatic solver
void moveSolver(Solver s, Cell[][] g, int dir) {

  if (!g[s.ai.x][s.ai.y].walls[dir]) 
    if (dir==0) s.ai.move(g, 'd');
    else if (dir==0) s.ai.move(g, 'w');
    else if (dir==2) s.ai.move(g, 'a');
    else if (dir==3) s.ai.move(g, 's');
}

I will do the possible in order to maintain this program updated every time I have a new cool addition for it, but I don’t promise they’ll be daily like this one, firstly because I’m a single person to work on a quite big project and second because I don’t aim to spend my whole life working on it, I already planned what I want to add from now onwards and that’s my objective for it, when I’ll have implemented every feature I want this program to have I will have to abandon the project and leave it out of maintenance long-term (I might think to leave it open-source and leave the freedom to work on it and not “put copyright” under its code, but we’ll see). But for now, there’s a bunch of things I want to learn to code, that are my objectives for this project to be implemented, so expect another update soon or later!

SEE YA!


Update 1.1 - Added Features:
-Added Exit button in order to close manually the program.
-Added Level Creation: Drag the mouse on the grid to create a custom-generated maze. (Use the c key to save the level, or the p key to play it.)
-(Creation Mode only) Added Trial Mode: enable or disable it with the t key to playlets the level while creating it.
-Minor bug fixes and graphics improvements.

3 Likes

Update 1.2 is here!
Long time ago since we’ve seen, yeah? Anyways, I’ve been very busy with uni and ha a lot to do, so had little, little time to work on the project. But now, we have a very cool feature added, that I think was one of the most necessary for the project itself: maze loading!

Now, from the new menu “Load maze”, you’ll have the full list of every maze you save, linked to a button that will automatically load the saved maze. Maybe you have randomly generated a very cool maze, or you created one of the most complex and intricate mazes ever, and you want to play it over and over, maybe to try and set the best time to solve it. (There’s no “timer function” in there yet, so don’t expect to time-trial anything without an external clock. If you’ll ask me I could try and add something like that, but it’s not in my plans at the moment). Well, here’s your chance to do that!

Anyways, the feature is not fully implemented yet, so it won’t be possible to edit a custom maze after it’s saved. Also, there’s no possibility to scroll the saved file list, or something like that, so when you save too much files you won’t be able to play the most recent ones (that will be saved too far away and won’t be visible on the screen). I will do the possible and fix this problem asap, also I will add something to improve its functionalities and add maybe something like a tutorial or some menu to see the how to access every feature, and maybe add even a remove/reset button for the saved list (as there’s currently no way to clean the saved list, the only way for now is go in the saved files folder and manually delete the files).

Enough talking, here’s the code!

First file:

//using library ControlP5 to have usable GUI elements
import controlP5.*;

//ConrtolP5 objects to handle GUI
ControlP5 startControls, saveControls, navControls, loadedMazes;

//used font in the sketch
PFont font, c1Font, c2Font;

//global variables
final int gridOffset=25, ctrlDim=120;
float linearity=0.5;
int xStart=0, yStart=0, prevDir=0, xPos, yPos;
boolean startMenu=true, mazeSolved=false, autoSolve=false, savingScreen=false, loadingScreen=false, inCreation=false, inTrial=false, loaded=false, isLoaded=false;
String autoMode="Manual solve", savedList="", mazeName="", loadedFiles[], selectedMaze[];

//global objects to handle the maze generation and solving process
Cell grid[][], genCell, currCell, prevCell;  
ArrayList<Cell> path=new ArrayList<Cell>();
Player player, target;
Solver autoSolver;

void setup() {

  //creating a new window
  size(750, 750);

  //setup for the used font
  font=loadFont("Formula1-Regular.vlw");
  c1Font=loadFont("Formula1-Display-Regular-10.vlw");
  c2Font=loadFont("Formula1-Display-Regular-15.vlw");
  textFont(font);
  textAlign(CENTER, CENTER);

  //setup for every ControlP5 object
  startControls=new ControlP5(this);
  saveControls=new ControlP5(this);
  navControls=new ControlP5(this);
  loadedMazes=new ControlP5(this);
  float dimension=width/2-ctrlDim/2;

  //controls in starting menu
  startControls.addSlider("linearity").setPosition(dimension, height/2+30).setSize(ctrlDim, 30).setLabel("").setRange(0, 0.99);
  startControls.addButton("generate").setPosition(width/2-15-ctrlDim, height/2+110).setSize(ctrlDim, 20).setLabel("Generate Maze").setFont(c1Font);
  startControls.addButton("create").setPosition(width/2+15, height/2+110).setSize(ctrlDim, 20).setLabel("Create Maze").setFont(c1Font);
  startControls.addButton("loadMaze").setPosition(dimension, height/2+140).setSize(ctrlDim, 20).setLabel("Load Maze").setFont(c1Font);

  //switch for auto solving mode (NOT WORKING)
  startControls.addToggle("autoSolve").setPosition(dimension, height/2+220).setSize(ctrlDim, 40).setMode(ControlP5.SWITCH).setLabel("").hide();

  //controls in saving menu
  saveControls.addTextfield("nameMaze").setPosition(width/2-ctrlDim, height/2+60).setSize(2*ctrlDim, 50).setFont(c2Font).setLabel("Insert maze name here:").hide();
  saveControls.addButton("saveMaze").setPosition(dimension, height/2+160).setSize(ctrlDim, 40).setLabel("Save").setFont(c2Font).hide();

  //exit button and home button (to go to starting menu)
  navControls.addButton("exit").setPosition(width-35, 5).setSize(30, 30);
  navControls.addButton("home").setPosition(width-70, 5).setSize(30, 30);

  //setting player at starting position, target at the
  //right-bottom corner of the maze
  player=new Player(xStart, yStart, gridOffset);
  target=new Player(width/gridOffset-1, height/gridOffset-1, gridOffset);
  autoSolver=new Solver(player);
  currCell=new Cell(0, 0, gridOffset);
  prevCell=new Cell(0, 0, gridOffset);

  //setup for the grid
  grid=new Cell[width/gridOffset][height/gridOffset];
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) grid[a][b]=new Cell(a, b, gridOffset);
} 

void draw() {

  background(40);
  stroke(128);
  if (startMenu) startMenu();
  else if (loadingScreen) loadingScreen();
  else if (inCreation || inTrial) inCreation();
  else if (savingScreen) savingScreen();
  else if (!mazeSolved) drawMaze();
  else if (mazeSolved) mazeSolved();
}

//function to print a background grid (will
//not be visible most cases, because the maze
//will be printed above it)
void drawGrid(int w, int h, int off) {

  for (int a=0; a<w-1; a+=off)
    for (int b=0; b<h-1; b+=off) {

      line(0, a, width, a);
      line(b, 0, b, height);
    }
}

//function to print a text string with some border of different color
void strokeText(String t, float dim, int x, int y, color bCol, color tCol) {

  fill(bCol);
  text(t, x-dim, y);
  text(t, x+dim, y);
  text(t, x, y-dim);
  text(t, x, y+dim);
  fill(tCol);
  text(t, x, y);
}

Second file:

//function called from the starting menu button
//to create a custom maze
void create() {

  //change display menu
  savingScreen=false;
  inTrial=false;
  loadingScreen=false;
  startMenu=false;
  inCreation=true;
  mazeSolved=false;

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").hide();
  startControls.getController("generate").hide();
  startControls.getController("create").hide();
  startControls.getController("autoSolve").hide();
  startControls.getController("loadMaze").hide();
  loadedMazes.hide();
}

//function called from the starting menu button
//to create a random maze and play it
void generate() {

  //setting as visited the first cell, adding it to the path
  grid[xStart][yStart].visited=true;
  path.add(grid[xStart][yStart]);

  //generating a new maze
  genCell=mazeGen(grid, grid[xStart][yStart], (width/gridOffset)*(height/gridOffset)-1, 0); //the last number is the length of the path
  println("Maze generated correctly.");

  //checking for the number of non-visited cells
  int notVis=0;
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!grid[a][b].visited) notVis++;

  //buffering the number of non visited cells
  //if there's more than 0, display a warning message
  if (notVis!=0) print("WARNING: ");
  println(notVis+" cells have not been visited.");

  //clearing the path used to generate the maze
  path.clear();

  //change display menu
  savingScreen=false;
  inCreation=false;
  inTrial=false;
  loadingScreen=false;
  startMenu=false;
  mazeSolved=false;
  isLoaded=false;

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").hide();
  startControls.getController("generate").hide();
  startControls.getController("autoSolve").hide();
  startControls.getController("create").hide();
  startControls.getController("loadMaze").hide();
  loadedMazes.hide();
}

void saveMaze() {

  //saving the maze as a .mgx file
  //(name starts with the path of the "savedFiles" directory, in which
  //every custom-created maze is stored)
  saveStrings("./savedFiles/"+saveControls.get(Textfield.class, "nameMaze").getText()+".mgx", split(saveProcess(grid), '\n'));

  //updating the list of saved files in this list, which will be saved as plain text
  //in the same directory but in a different folder
  loadedFiles=loadStrings("./savedFiles/savedList/savedList.txt");
  loadedFiles=expand(loadedFiles, loadedFiles.length+1);
  loadedFiles[loadedFiles.length-1]="./savedFiles/"+saveControls.get(Textfield.class, "nameMaze").getText()+".mgx";
  saveStrings("./savedFiles/savedList/savedList.txt", loadedFiles);

  //clearing the textfield for the maze name
  saveControls.get(Textfield.class, "nameMaze").clear();

  //changing display menu and clearing the maze
  inCreation=false;
  inTrial=false;
  loadingScreen=false;
  savingScreen=false;
  loaded=false;
  startMenu=true;
  isLoaded=false;
  mazeSolved=false;

  clearMaze(grid, player, target);
  textAlign(CENTER, CENTER);

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").show();
  startControls.getController("generate").show();
  startControls.getController("create").show();
  startControls.getController("loadMaze").show();
  saveControls.getController("saveMaze").hide();
  saveControls.getController("nameMaze").hide();
  loadedMazes.hide();
}

void home() {

  //clearing the path used to generate the maze
  //and the generated maze
  path.clear();
  clearMaze(grid, player, target);

  //aligning text
  textAlign(CENTER, CENTER);

  //changing display menu (going to startMenu)
  savingScreen=false;
  inCreation=false;
  inTrial=false;
  loadingScreen=false;
  startMenu=true;
  mazeSolved=false;
  isLoaded=false;

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").show();
  startControls.getController("generate").show();
  startControls.getController("create").show();
  startControls.getController("loadMaze").show();
  saveControls.getController("saveMaze").hide();
  saveControls.getController("nameMaze").hide();
  loadedMazes.hide();
}

void loadMaze() {

  //loading saved files list
  loadedFiles=loadStrings("./savedFiles/savedList/savedList.txt");

  if (!loaded) for (int a=0; a<loadedFiles.length; a++) {

    String name=loadedFiles[a].replace("./savedFiles/", "");
    loadedMazes.addButton(name).setPosition(width/2-2*ctrlDim, a*35+60).setSize(4*ctrlDim, 30).setFont(c1Font).setId(a+1);
  }
  textSize(25);
  loaded=true;

  //changing display menu
  savingScreen=false;
  inCreation=false;
  inTrial=false;
  startMenu=false;
  loadingScreen=true;
  isLoaded=false;
  mazeSolved=false;

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").hide();
  startControls.getController("generate").hide();
  startControls.getController("autoSolve").hide();
  startControls.getController("create").hide();
  startControls.getController("loadMaze").hide();
  loadedMazes.show();
}

//used to trigger the loading of a particular maze
//(uses the ID system implemented in the ControlP5 library)
void controlEvent(ControlEvent event) {
  
  //getting the ID of the pressed button
  int id = event.getController().getId();
  if (event.isController() && id>0) {
    
    //getting the file content of the selected maze
    selectedMaze=loadStrings("./savedFiles/"+event.getName());
    println("Loaded maze file: \"./savedFiles/"+event.getName()+"\"");
    loadMaze(selectedMaze, grid); //generating maze in the file
  }
}

Third file:

class Cell {

  int x, y; //position
  int pX, pY; //temporary position (used only during custom maze creation)
  int dim; //dimension
  boolean visited=false, isRed=false; //check for being visited (on the grid)
  boolean[] walls={true, true, true, true}; //east, south, west, north

  //used to display the single cell on the grid
  void display(color f, color s) {

    fill(f);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);

    //I need to separate the cell from its borders
    //because it might not have every border marked as "true"
    this.displayBorders(s);
  }

  //used to display the borders of a particular cell
  void displayBorders(color c) {

    stroke(c);

    //if a particular border is true (it appears on the actual maze),
    //I print it on the screen in the correct position
    if (this.walls[0]) line(this.x*this.dim+this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[1]) line(this.x*this.dim, this.y*this.dim+this.dim, this.x*this.dim+this.dim, this.y*this.dim+this.dim);
    if (this.walls[2]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim, this.y*this.dim+this.dim);
    if (this.walls[3]) line(this.x*this.dim, this.y*this.dim, this.x*this.dim+this.dim, this.y*this.dim);
  }

  //used to create a new Cell object that is the
  //copy of another object with some properties
  Cell copyCell() {

    Cell b=new Cell(this.x, this.y, this.dim);
    b.visited=this.visited;
    for (int a=0; a<this.walls.length; a++) b.walls[a]=this.walls[a];
    return b;
  }
  
  Cell (int a, int b) {
    
    this.pX=a;
    this.pY=b;
  }

  Cell(int a, int b, int c) { //constructor

    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

//this function is used to store temporary
//position of the cells
void saveCell(Cell c) {
  
  c.pX=c.x;
  c.pY=c.y;
}

//this function is used to update
//the position of the cells
void restoreCell(Cell c) {
  
  c.x=c.pX;
  c.y=c.pY;
}

//this calculates the number of free available
//adjacent cells to generate the path on
int numNb(Cell c, Cell[][] g) {

  int n=0; //start at 0
  for (int a=(c.x<1 ? c.x : c.x-1); a<=(c.x>=width/c.dim-1 ? c.x : c.x+1); a++)
    for (int b=(c.y<1 ? c.y : c.y-1); b<=(c.y>=height/c.dim-1 ? c.y : c.y+1); b++)

      //if the cell on the same X or Y axis has been visited, add it to
      //the list of occupied adjacent cells
      if ((a==c.x || b==c.y) && !g[a][b].visited) n++;

  //return the number of non-free cells
  return n;
}

Fourth file:

//called while any mouse button is pressed
void mousePressed() {

  if (inCreation && mouseButton==RIGHT) { //deleting a cell on the screen

    //saving current mouse position on the grid
    if (mouseX>0 && mouseX<width) xPos=mouseX/gridOffset;
    if (mouseY>0 && mouseY<height) yPos=mouseY/gridOffset;

    //deleting cell
    grid[xPos][yPos].visited=false;

    //removing walls from the cell and from the adjacent ones
    for (int a=0; a<4; a++) grid[xPos][yPos].walls[a]=true;
    if (xPos>0 && grid[xPos-1][yPos].visited) grid[xPos-1][yPos].walls[0]=true;
    if (yPos>0 && grid[xPos][yPos-1].visited) grid[xPos][yPos-1].walls[1]=true;
    if (xPos<width/gridOffset-1 && grid[xPos+1][yPos].visited) grid[xPos+1][yPos].walls[2]=true;
    if (yPos<height/gridOffset-1 && grid[xPos][yPos+1].visited) grid[xPos][yPos+1].walls[3]=true;
  }
}

//called every time any mouse button is released from pressing
//(used only to get input from GUI buttons, due to library bug)
void mouseReleased() {
  
  if (startControls.getController("generate").isMouseOver()) generate();
  if (startControls.getController("create").isMouseOver()) create();
  if (startControls.getController("loadMaze").isMouseOver()) loadMaze();
}

//called every time the mouse is dragged
//(moved while pressed)
void mouseDragged() {

  if (inCreation) { //custom maze generation

    //saving current mouse position on the grid
    if (mouseX>0 && mouseX<width) xPos=mouseX/gridOffset;
    if (mouseY>0 && mouseY<height) yPos=mouseY/gridOffset;

    //if currCell has moved on the screen
    if (currCell.x!=xPos || currCell.y!=yPos) {

      //calculating position
      currCell.pX=xPos;
      currCell.pY=yPos;
      prevCell.pX=currCell.x;
      prevCell.pY=currCell.y;

      //updating position
      restoreCell(currCell);
      restoreCell(prevCell);
    }

    //updating the cell in the position of the mouse
    if (mouseButton==LEFT) grid[xPos][yPos].visited=true;

    //updating walls
    if (prevCell.x==currCell.x+1 && prevCell.y==currCell.y) {

      grid[currCell.x][currCell.y].walls[0]=false;
      grid[prevCell.x][prevCell.y].walls[2]=false;
    } else if (prevCell.x==currCell.x && prevCell.y==currCell.y+1) {

      grid[currCell.x][currCell.y].walls[1]=false;
      grid[prevCell.x][prevCell.y].walls[3]=false;
    } else if (prevCell.x==currCell.x-1 && prevCell.y==currCell.y) {

      grid[currCell.x][currCell.y].walls[2]=false;
      grid[prevCell.x][prevCell.y].walls[0]=false;
    } else if (prevCell.x==currCell.x && prevCell.y==currCell.y-1) {

      grid[currCell.x][currCell.y].walls[3]=false;
      grid[prevCell.x][prevCell.y].walls[1]=false;
    }

    //setting prevCell as red
    for (int a=0; a<width/gridOffset; a++)
      for (int b=0; b<height/gridOffset; b++) grid[a][b].isRed=false;
    grid[currCell.x][currCell.y].isRed=true;
  }
}

//used for keyboard input
void keyTyped() {

  if (!mazeSolved && !autoSolve) player.move(grid, key);
  if (inCreation) {
    if (key=='p') {

      //play mode: the custom maze is
      //no longer editable and is ready to play
      inCreation=false;
      player.x=0;
      player.y=0;
      target.x=width/gridOffset-1;
      target.y=height/gridOffset-1;
    } else if (key=='c') {

      //create mode: the maze is ready to be saved in
      //a .mgx file and exported (as simple text)
      textSize(60);
      inCreation=false;
      savingScreen=true;
      saveControls.getController("nameMaze").show();
      saveControls.getController("saveMaze").show();
    } 

    //enable/disable trial mode: you can playtest the maze
    //while creating it to help you create the maze
    else if (key=='t') inTrial=!inTrial;
  }
}

Fifth file:

Cell mazeGen(Cell[][] g, Cell c, int depth, int direction) {

  Cell next=new Cell(-1, -1, c.dim);
  int dir=0, neighbours=numNb(c, g);
  boolean blindSpot=false;
  boolean tried0=false, tried1=false, tried2=false, tried3=false;

  //if I can go in any direction from the cell I'm now in,
  //I start a random search for a new cell available
  if (neighbours>=0) do {

    //random chance to keep the direction I was coming from
    if (random(0, 1)<linearity) dir=direction;
    else dir=(int)random(0, 4); //generate random direction

    //checking for already visited directions
    if (dir==0) tried0=true;
    else if (dir==1) tried1=true;
    else if (dir==2) tried2=true;
    else if (dir==3) tried3=true;

    //check for boundaries conditions and generate new cell
    if (dir==0 && c.x<width/c.dim-1 && !g[c.x+1][c.y].visited) next=g[c.x+1][c.y].copyCell();
    else if (dir==1 && c.y<height/c.dim-1 && !g[c.x][c.y+1].visited) next=g[c.x][c.y+1].copyCell();
    else if (dir==2 && c.x>0 && !g[c.x-1][c.y].visited) next=g[c.x-1][c.y].copyCell();
    else if (dir==3 && c.y>0 && !g[c.x][c.y-1].visited) next=g[c.x][c.y-1].copyCell();

    //if I do not reach a new cell, I've hit a blindspot
    if (tried0 && tried1 && tried2 && tried3) {

      blindSpot=true;
      break;
    }
  } while (next.x==-1 && next.y==-1);

  //otherwise, I've hit a blindspot
  //and I need to backtrack more
  else blindSpot=true;

  //if the cell has not updated, I reached
  //a spot in which I have no available adjacent cells;
  //I start backtracking on the path I built in order to get
  //to the last spot in which there is an available adjacent cell
  if (blindSpot) {

    next=path.get(path.size()-1).copyCell();
    path.remove(path.size()-1);
  } else {

    //update next cell
    g[c.x][c.y].walls[dir]=false;
    g[next.x][next.y].walls[(dir>=2 ? dir-2 : dir+2)]=false;
    g[next.x][next.y].visited=true;

    path.add(g[c.x][c.y]); //add the cell to the tracked path
    depth--; //go deeper in the generation process
  }

  if (checkFinished(g) || depth==0 || (next.x==0 && next.y==0)) {

    //finished generate maze for the desired depth
    return next;
  } else return mazeGen(g, next, depth, dir);
}

//checks if every cell of the grid has been visited
boolean checkFinished(Cell[][] g) {

  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (!g[a][b].visited) return false;

  //return true if it doesn't find
  //any non-visited cell
  return true;
}

Sixth file:

/*

 WARNING: This functionality is currently "work in progress",
 so it works just fine but, for the moment, it has no way to
 keep editing an existing project. So, if you're building a
 custom maze, you'll have to finish it before saving it, or
 you won't be able to edit it in the future.
 
 */

//used to load a pre-saved maze from the folder "savedFiles"
//(it gets a strings array as input because that's the output
//you get when you save a text file with Processing)
void loadMaze(String[] s, Cell[][] g) {

  String newCell;
  String[] cellData, tMatch, fMatch;
  
  //starting from 1 because line 0 contains file name
  for (int a=1; a<s.length-1; a++) {

    //saving new cell data in a single string
    newCell=s[a];
    cellData=splitTokens(newCell, "[]{}=, ");

    //searching for walls data (if not found, cell is not visited)
    tMatch=match(newCell, "true");
    fMatch=match(newCell, "false");

    //new Cell object, initialized with string data
    Cell c=new Cell(int(cellData[0]), int(cellData[1]), gridOffset);
    if (tMatch!=null || fMatch!=null) {
      
      //setting cell data only if visited
      c.visited=true;
      for (int b=0; b<4; b++) c.walls[b]=boolean(cellData[b+2]);
    }
    if (c.visited) g[int(cellData[0])][int(cellData[1])]=c.copyCell();
  }

  //change display menu
  savingScreen=false;
  inCreation=false;
  inTrial=false;
  loadingScreen=false;
  startMenu=false;
  
  //using isLoaded to handle saving process
  //(if the maze has been loaded, it's already in
  //memory, so the saving screen won't be shown
  //after completing the maze)
  isLoaded=true;

  //updating visibility for the ControlP5 objects
  startControls.getController("linearity").hide();
  startControls.getController("generate").hide();
  startControls.getController("autoSolve").hide();
  startControls.getController("create").hide();
  startControls.getController("loadMaze").hide();
  loadedMazes.hide();
}

Seventh file:

/*

  CLASS INHERITED FROM ANOTHER PROJECT
  (modified to fit the purpose of the project)
  
*/

class Player {
  
  int x, y; //position
  int dim; //dimension
  
  //used to check if two Player objects have the same position
  boolean checkSolved(Player other) {
    
    return (this.x==other.x && this.y==other.y);
  }
  
  //used to move a single object using keyboard input (WASD)
  void move(Cell[][] g, char k) {
    
    if (k=='w' && !g[this.x][this.y].walls[3]) this.y--;
    if (k=='a' && !g[this.x][this.y].walls[2]) this.x--;
    if (k=='s' && !g[this.x][this.y].walls[1]) this.y++;
    if (k=='d' && !g[this.x][this.y].walls[0]) this.x++;
  }
  
  //used to display a Player objet on the screen
  void display(color c) {
    
    fill(c);
    noStroke();
    square(this.x*this.dim, this.y*this.dim, this.dim);
  }
  
  Player(int a, int b, int c) { //constructor
    
    this.x=a;
    this.y=b;
    this.dim=c;
  }
}

Eighth file:

//used to save a new .mgx file to store the layout of the maze
//(if you want to manually edit the file after saving it, it is stored
//as simple text so you can open it with standard text editing programs)
String saveProcess(Cell[][] g) {

  String maze="Name: \""+saveControls.get(Textfield.class, "nameMaze").getText()+".mgx\"\n"; //the string containing the maze data
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) {

      maze+=("["+a+"]["+b+"]={");
      if (g[a][b].visited) for (int c=0; c<4; c++) {

        //adding "true" if there is a wall, "false" if not;
        //returning an empty cell if it is not visited
        if (g[a][b].walls[c]) maze+="true";
        else maze+="false";
        if (c!=3) maze+=", ";
      }
      maze+="}\n";
    }

  //return the generated string containing the maze data
  return maze;
}

void clearMaze(Cell[][] g, Player p, Player t) {

  //clearing the maze
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++) {
      g[a][b].visited=false;
      for (int c=0; c<4; c++) g[a][b].walls[c]=true;
    }

  //resetting player position
  p.x=0;
  p.y=0;
  t.x=width/gridOffset-1;
  t.y=height/gridOffset-1;
}

Ninth file:

void startMenu() {

  //setup for the main menu screen
  background(255);

  //title
  textSize(70);
  strokeText("Maze Generator!", 3, width/2, height/2-120, color(0, 80, 200), color(0, 200, 0));

  //text line 1
  textSize(15);
  fill(color(0, 80, 200));
  text("Use this slider to adjust the linearity of the maze\n(0 is more convoluted, 1 is more linear)", width/2, height/2);

  //updating automatic mode solution display
  if (autoSolve) autoMode="Automatic solve";
  else autoMode="Manual solve\n(WARNING: Automatic solve not working yet.)";

  //text line 2
  textSize(20);
  fill(color(0, 80, 200));
  text("Currently on: "+autoMode, width/2, height/2+275);
}

void inCreation() {

  drawGrid(width, height, gridOffset);
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (grid[a][b].visited) {

        if (grid[a][b].isRed) grid[a][b].display(color(100, 0, 0), color(255));
        else grid[a][b].display(color(100), color(255));
      }

  if (inTrial) {

    //drawing player and target position
    player.display(color(0, 50, 200));
    target.display(color(200, 0, 0));

    //redrawing the borders for the occupied cells
    grid[player.x][player.y].displayBorders(color(255));
    grid[target.x][target.y].displayBorders(color(255));
  }
}

void savingScreen() {

  background(255);
  strokeText("Insert the maze\nname here:", 2, width/2, height/2-80, color(0), color(0, 100, 255));
}

void loadingScreen() {

  background(255);
  strokeText("Select maze to load:", 1, width/2, 35, color(0), color(0, 100, 255));
}

void drawMaze() {

  //displaying background and maze grid
  drawGrid(width, height, gridOffset);
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (grid[a][b].visited && !inCreation) grid[a][b].display(color(100), color(255));

  //if the maze is not solved
  if (!player.checkSolved(target)) {

    //if not in creation process
    if (!inCreation) {

      //displaying the player and the target
      player.display(color(0, 50, 200));
      if (grid[target.x][target.y].visited) target.display(color(200, 0, 0));

      //redrawing the borders for the occupied cells
      grid[player.x][player.y].displayBorders(color(255));
      grid[target.x][target.y].displayBorders(color(255));
    }
  } else {

    //maze have been solved
    mazeSolved=true;
    if (!isLoaded) {
      
      //if the maze has been loaded from memory,
      //don't show the saving screen at the end
      saveControls.getController("nameMaze").show();
      saveControls.getController("saveMaze").show();
    }
    textSize(70);
  }
} 

void mazeSolved() {

  //drawing the background grid for the non-visited cells
  drawGrid(width, height, gridOffset);

  //drawing the maze
  for (int a=0; a<width/gridOffset; a++)
    for (int b=0; b<height/gridOffset; b++)
      if (grid[a][b].visited) grid[a][b].display(color(100), color(255));

  //drawing player and endwall
  player.display(color(0, 200, 50));
  grid[player.x][player.y].displayBorders(color(255));
  strokeText("Maze solved!", 3, width/2, height/2, color(0), color(255));
}

There’s also the tenth file, containing the class Solver file, but I won’t add it there because it had no changes since the last update. I still find it very hard to come up with a great solution that can successfully take a good route (possibly the most efficient one) that makes it to the end, I’m not very good on this kind of things, but I promise that, soon or later, I will add an automatic solver for the maze. For now, it’s over, hope it will take less time to the next update, but I still can’t say anything.
SEE YA!


Update 1.2 - Added Features:
-Added Home button in order to get to the starting menu at any point.
-Added Maze Loading: select a custom .mgx file from the saved list and load the maze to play it.
-Fixed a major bug for which sometimes the button won’t press, and it’s not possible to go in the selected menu.
-Minor bug fixes and graphics improvements.

4 Likes