Labyrinth Game Session 2 of 2

Level: Advanced (11+) Duration: 2 × 1 Hour

Part 4: Auto-Generating The Maze 45 Minutes

It was a lot of work to create each wall of the maze, and it’s the same maze each time. Let’s look at an algorithm to create the maze for us.

For this session, it’s recommended that you have some previous JavaScript experience, specifically with arrays and objects.

Imagine the screen is divided into a grid.

Walls are placed between grid squares. If we place a wall between each pair of neighboring squares, the grid is divided into many one square sections and you can’t move from one square to another.

We'll start removing walls in a moment, but first let's look at how we can represent the grid and walls in our code.

Arrays

Arrays are used to store lists of things. Think of arrays as a box divided into sections, each storing a single item. Each section has a unique number, called an index, that we can use to reference it.

In JavaScript, we create an array using square brackets, and optionally including a number of initial items. For example, we could create a list of out favorite things.

let favs = ['pizza', 'rainbows', 'puppies']

We can access an individual item by providing the index, again using square brackets. Be aware that the first item is index 0, the second is index 1, and so on.

favs[0] // 'pizza'

To keep track of each grid square, we'll use a two dimensional array, or an array of arrays.

The following code first creates an empty array to represent the grid. It then uses a nested for loop to assign a section number to each square.

let grid = [] let section = 1 for (let x = 0; x < 8; x = x + 1) { grid[x] = [] for (let y = 0; y < 6; y = y + 1) { grid[x][y] = section section = section + 1 } }

Objects

Objects can be used to store more complex data. Instead of storing a single value, they can store a collection of properties and values. We'll use objects to represent the walls of our maze.

Each wall object will have properties that indicate the x and y coordinates of the squares on either side of the wall. We'll also include a property to indicate whether the wall is horizontal or vertical. This will come in handy later on.

The following code again uses a nested for loop to move through the squares in the grid and create a wall between it and the square to the right, and the square below.

let walls = [] for (let x = 0; x < 8; x = x + 1) { for (let y = 0; y < 6; y = y + 1) { if (x < 7) { walls.push({ ax: x, ay: y, bx: x + 1, by: y, orientation: 'vertical' }); } if (y < 5) { walls.push({ ax: x, ay: y, bx: x, by: y + 1, orientation: 'horizontal' }); } } }

The Algorithm

To generate the maze, we'll use a version of Kruskal's algorithm, developed by mathematician Joseph Kruskal. It goes a little somethinglike this:

  • Select a wall at random.
  • If the wall separates two different sections, remove it and merge the sections.
  • Otherwise keep the wall.
  • Repeat until we've gone through all the walls.

Let's look at how we can implement this in code. First create an array that will hold the final walls that make up our maze.

let maze = []

Pick a wall at random, and remove it from the list. We can use the splice() method for this.

let i = random(0, walls.length - 1) let wall = walls[i] walls.splice(i, 1)

The grid array contains the section each square currently belongs to. If the two squares separated by the wall are in different sections, merge the sections.

let a = grid[wall.ax][wall.ay] let b = grid[wall.bx][wall.by] if (a != b) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 6; y++) { if (grid[x][y] == b) { grid[x][y] = a } } }

Otherwise, keep the wall by adding it to the maze array.

} else { maze.push(wall) }

While Loop

Keep doing this until there are no walls left. We can use a while loop for this. A while loop will repeat a block of code as long as a certain condition is true.

let maze = [] while (walls.length > 0) { // Pick a wall at random let i = random(0, walls.length - 1) let wall = walls[i] // Remove the wall from the list walls.splice(i, 1) // Check section on either side of the wall let a = grid[wall.ax][wall.ay] let b = grid[wall.bx][wall.by] // If different sections, combine them if (a !== b) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 6; y++) { if (grid[x][y] == b) { grid[x][y] = a } } } // Otherwise include the wall in the maze } else { maze.push(wall) } }

Part 5: Drawing The Maze 15 Minutes

Finally, go through each wall in the maze array and draw it using the rect() function. We need to do a little math to figure out the exact size and location, based on the coordinates of the squares it separates, and the width and height of each square.

So first, let's calculate the width and height of each square. We can do this by dividing the width and height of the screen by the number of squares in each direction.

let squareWidth = width / 8 let squareHeight = height / 6

Now we'll loop through each wall.

for (let i = 0; i < maze.length; i = i + 1) { let wall = maze[i]

Our code will be slight different depending on whether the wall is horizontal or vertical. Luckily we included this bit of information in the object representing a wall.

The first two parameters we pass to the rect() function are the x and y coordinates of the center of the shape. For the x coordinate, we'll move over the number of squares specified by the x coordinate of the first square, then go half a square further.

if (wall.orientation == 'horizontal') { let x = wall.ax * squareWidth + (squareWidth / 2)

For the y coordinate, we'll move down the number of squares specified by the y coordinate of the second square.

let y = wall.by * squareHeight rect(x, y, squareWidth + 20, 20)

The code for the vertical wall is similar, just swapped around. It may be helpful to pull out a piece of paper and sketch out a few examples to help get your head around it.

} else { let x = wall.bx * squareWidth let y = wall.ay * squareHeight + (squareHeight / 2) rect(x, y, 20, squareHeight + 20) } }

Putting It All Together

let marble = circle(64, 64, 50) function movin() { marble.velocity.x = tiltX marble.velocity.y = tiltY if (marble.velocity.x > 20) marble.velocity.x = 20 if (marble.velocity.x < -20) marble.velocity.x = -20 if (marble.velocity.y > 20) marble.velocity.y = 20 if (marble.velocity.y < -20) marble.velocity.y = -20 } on('deviceorientation', movin) gravity = false // Create grid of squares let grid = [] let section = 1 for (let x = 0; x < 8; x = x + 1) { grid[x] = [] for (let y = 0; y < 6; y = y + 1) { grid[x][y] = section section = section + 1 } } // Create set of walls let walls = [] for (let x = 0; x < 8; x = x + 1) { for (let y = 0; y < 6; y = y + 1) { if (x < 7) { walls.push({ ax: x, ay: y, bx: x + 1, by: y, orientation: 'vertical' }); } if (y < 5) { walls.push({ ax: x, ay: y, bx: x, by: y + 1, orientation: 'horizontal' }); } } } // Build maze let maze = [] while (walls.length > 0) { // Pick a wall at random let i = random(0, walls.length - 1) let wall = walls[i] // Remove the wall from the list walls.splice(i, 1) // Check section on either side of the wall let a = grid[wall.ax][wall.ay] let b = grid[wall.bx][wall.by] // If different sections, combine them if (a !== b) { for (let x = 0; x < 8; x++) { for (let y = 0; y < 6; y++) { if (grid[x][y] == b) { grid[x][y] = a } } } // Otherwise include the wall in the maze } else { maze.push(wall) } } // Draw walls let squareWidth = width / 8 let squareHeight = height / 6 for (let i = 0; i < maze.length; i = i + 1) { let wall = maze[i] if (wall.orientation == 'vertical') { let x = wall.bx * squareWidth let y = wall.ay * squareHeight + (squareHeight / 2) rect(x, y, 20, squareHeight + 20) } else { let x = wall.ax * squareWidth + (squareWidth / 2) let y = wall.by * squareHeight rect(x, y, squareWidth + 20, 20) } }

Taking It Further

  • Try making the grid squares smaller.
  • Play a congratulatory sound when you reach the goal.
  • Generate a new maze after you finish the first one.
  • Add traps that send you back to the start.
  • Multiple marbles at once!