Snakes And Ladders Talk Through - Level Generation

Set up
To begin the level generation, we need to start by making a list that stores the level. We can do this by constructing an array:this.array = [] // an array that stores the level
this.width = 100 // the amount of columns
this.height = 100 // the amount of rows
The first thing to do to our array is to fill it in
with blocks, which we can mine out later. In the code
snippet below, a block is represented by 1.
// iterate through the amount of blocks in the world
for (let i = 0; i < this.width * this.height; i ++) {
this.array.push(1) // fill the level with blocks
}
To draw the blocks, you can write code similar to this:
// set the scale
const block_size = 50
// iterate through every square in the level
for (let i = 0; i < this.array.length; i ++) {
const block = this.array[i]
// get x and y pos of the square based on its position in the array
const x = i % this.width,
const y = Math.floor(i / this.width)
// if the current square is a block, draw it
if (block == 1) ctx.fillRect(x * block_size, y * block_size, 1 * block_size, 1 * block_size)
}
That's all you need to set up the map. Now we can start carving bits away.

Chambers
The next thing to do is to start mining rooms. Each chamber has a random width and height, and is positioned in a grid. Store the chambers in an array, so that the computer can know where to place the tunnels and actors later on.@this.array = []@
this.chambers = [] // make an array that stores the chambers
@this.width = 100
this.height = 100@
In the code below, 0 refers to plain air.
// this code makes a single chamber.
// note that chamber x, y, width and height have already been defined.
for(let x = chamber_x; x < chamber_x + chamber_width; x ++) {
for (let y = chamber_y; y < chamber_y + chamber_height; y ++) {
this.array[x + y * this.width] = 0
}
}

Tunnels
Making the tunnels is a complex part, so I will just give you a simplified overview. To begin, we need to know how each chamber should be connected. I went with the binary-tree maze structure which is the simplest procedural maze that anyone could hope to make. This is how it works: 1. Get your chambers set up2. Starting at the top left, connect your chamber to either:
1. The chamber on the right
2. The chamber underneath
With these rules applied, the maze should start to be taking shape. Note how the passages seem to point towards the bottom right corner:



Reverse generation
As you can see, the world still looks very different from the real game. What we are missing is reverse generation - a final "filter" that removes the pointless blocks. At the end of the generation code, I run through every block in the level and do the following: If the current block is surrounded by other blocks:Turn the current block into thin air The advantage of this final filter is it speeds up in-game performance, it makes the level look better (in my opinion), and it also gives the world a vast feeling of nothingness - meaning the player is not able to see the sharp boundaries of the map.

Side effects
This final section briefly discusses how each side effect is placed in the level and how the level adapts to them. To set everything up, put a side effect in each chamber, making sure that it does't get dropped above a tunnel hole. If you played the game, you may have noticed the average size of each chamber grew as the game went on. This is because the size for each side effect is stored from the start. It means that the computer can know exactly how big its chambers should be for the current level. In the game, there's a function called getLevelEffects(), which returns an array of all the enemy types for the level.function getLevelEffects() {
if (game.level <= 2) return [0]
if (game.level <= 3) return [1, 1, 0]
if (game.level <= 6) return [2, 2, 1, 0]
if (game.level <= 7) return [3, 2, 1]
if (game.level <= 8) return [4, 2, 0]
if (game.level <= 9) return [6, 4, 1]
if (game.level <= 10) return [7, 6, 2]
if (game.level <= 11) return [7, 6, 4]
}
You may notice that the largest side effects always come first in the array.
The reason for this is it means that when the map is being generated,
it can take the size of the biggest side effect into consideration.
It can thus make sure that its minimum chamber width and height is way larger
than the recorded enemy.
// get the biggest enemy sizes
const EFFECT = EFFECT_TYPES[getLevelEffects()[0]]
// minimum chamber width
const MIN_WIDTH = Math.ceil(EFFECT.width.max) * 2
// minimum chamber height
const MIN_HEIGHT = Math.ceil(EFFECT.height.max)
// result
const CHAMBER_W = {min: MIN_WIDTH + 1, max: MIN_WIDTH + 10}
const CHAMBER_H = {min: MIN_HEIGHT + 3, max: MIN_HEIGHT + 5}
That's it for this article, I hope it clarified things for you and maybe it will come in handy for projects in the months to come!