Binary tree algorithm


In Manual maze, we define the block data structure and use it to draw a maze. If you can generate the block data automatically, the maze will have a different path. There are many maze algorithms. Here, I'll introduce the easiest binary tree algorithm.

Binary tree algorithm

The concept behind binary tree algorithm is easy, but it also has an apparent defect which we'll see soon. However, it helps us to understand why we can generate a maze automatically. The generated maze is a perfect maze, which means it has exactly one path between any two blocks.

Take a 4x4 maze for example. First, draw a maze with no wall carved. We start from the lower-left block.

Binary tree algorithm

Now, let's flip a coin. If it's “heads”, we carve the right wall, otherwise, the upper wall. Then, we can move to the next block. For example, we get “tails” first.

Binary tree algorithm

It doesn't matter which one is the next block. Binary tree algorithm doesn't care about it. For convenience, we always move to the right block if possible. Suppose we get “heads” and “tails” after two coin flipping. The status of the maze is:

Binary tree algorithm

We've reached the rightmost column. What should we do now? You cannot carve the right wall if it's not an exit. For the rightmost column, we cut their upper walls directly without flipping a coin.

Binary tree algorithm

Similarly, we cannot remove the upper wall of the uppermost row. We directly cut their right walls.

Binary tree algorithm

Now, let's deal with the remaining blocks. We back to the (1, 2) block first.

Binary tree algorithm

Suppose we get “heads”, move to the right block, get “tails”, move to the right block, and then get “tails”.

Binary tree algorithm

There's still one row we have to carve. Let's start from the leftmost block. This time, we get three “heads”.

Binary tree algorithm

Ya, we create a maze!

Is it a binary tree?

We can draw paths on the generated maze.

Binary tree algorithm

Now remove all walls and rotate the paths 45 degrees.

Binary tree algorithm

It's a binary tree, right? Every time we decide to carve a wall, we select a parent node. If you look inversely, a parent node has at most two child nodes. It's undoubtedly a binary tree, and the upper-left block is the root. In a binary tree, any two nodes have exactly one path. No matter what maze algorithm, it'll form a tree structure if it generates a perfect maze. There's no loop on these mazes.

And you might find that binary tree algorithm has a bias. Suppose you start from the root, you can reach any child node if you only go left or down.

Binary tree algorithm

Inversely, suppose that the left-lower block is the starting point and the upper-right block is an exit, you can easily find a way through the maze by only going right or up.

We can add more rules or randoms to improve the bias. For example, I'll show how to walk randomly and carve the wall we encounter in the later doument.

Different algorithms will bring different mazes. If you are interested in maze algorithms, I suggest the book Mazes for Programmers.

Implementing binary tree algorithm

In OpenSCAD, we define a function to simulate coin flipping first.

function flip_coin() = round(rands(0, 1, 1)[0]) + 1;

The rands function is a built-in function of OpenSCAD. You can pass the random number range and the number of random numbers to return a vector. For example, rands(n, m, x) returns a vector containing x random numbers range from n to m. Therefore, rands(0, 1, 1)[0] generate a random number between 0 and 1.

The built-in round function returns the closest integer. Thus, round(rands(0, 1, 1)[0]) gets either 1 or 0. We add 1 to it so flip_coin returns either 1 or 2, which correspond with the wall constant UP_WALL or RIGHT_WALL respectively.

Then, we flip a coin for each block. Remember, you don't have to flip for the rightmost column and the uppermost row.

function binary_tree_block(x, y, rows, columns) = 
    y == rows ? block_data(x, y, 1) : (        // the uppermost row
        x == columns ? block_data(x, y, 2) :   // the rightmost column
            block_data(x, y, flip_coin())      // flip a coin
    );

We go through the maze from left to right and lower to upper to generate all blocks.

function binary_tree_algorithm(rows, columns) = 
    [
        for(y = [1:rows]) 
            for(x = [1:columns]) 
                binary_tree_block(x, y, rows, columns)
    ];

The following list all code for generating a maze. We don't change any code developed in Manual maze for representation and only use binary tree algorithm to generate the block data referred by maze_blocks.

module line(point1, point2, width = 1, cap_round = true) {
    angle = 90 - atan((point2[1] - point1[1]) / (point2[0] - point1[0]));
    offset_x = 0.5 * width * cos(angle);
    offset_y = 0.5 * width * sin(angle);

    offset1 = [-offset_x, offset_y];
    offset2 = [offset_x, -offset_y];

    if(cap_round) {
        translate(point1) circle(d = width, $fn = 24);
        translate(point2) circle(d = width, $fn = 24);
    }

    polygon(points=[
        point1 + offset1, point2 + offset1,  
        point2 + offset2, point1 + offset2
    ]);
}

module polyline(points, width = 1) {
    module polyline_inner(points, index) {
        if(index < len(points)) {
            line(points[index - 1], points[index], width);
            polyline_inner(points, index + 1);
        }
    }

    polyline_inner(points, 1);
}

// Wall constants

NO_WALL = 0;    
UP_WALL = 1;  
RIGHT_WALL = 2;  
UP_RIGHT_WALL = 3; 

function block_data(x, y, wall_type) = [x, y, wall_type];
function get_x(block_data) = block_data[0];
function get_y(block_data) = block_data[1];
function get_wall_type(block_data) = block_data[2];

module draw_block(wall_type, block_width, wall_thickness) {
    if(wall_type == UP_WALL || wall_type == UP_RIGHT_WALL) {
        // the upper wall
        polyline(
            [[0, block_width], [block_width, block_width]], wall_thickness
        ); 
    }

    if(wall_type == RIGHT_WALL || wall_type == UP_RIGHT_WALL) {
        // the right wall
        polyline(
            [[block_width, block_width], [block_width, 0]], wall_thickness
        ); 
    }
}

module draw_maze(rows, columns, blocks, block_width, wall_thickness) {
    for(block = blocks) {
        // move the block to its correspond position
        translate([get_x(block) - 1, get_y(block) - 1] * block_width) 
            draw_block(
                get_wall_type(block), 
                block_width, 
                wall_thickness
            );
    }

    // the lowermost wall
    polyline(
        [[0, 0], [block_width * columns, 0]], 
        wall_thickness);
    // the leftmost wall
    polyline(
        [[0, block_width], [0, block_width * rows]], 
        wall_thickness);
} 

block_width = 3;
wall_thickness = 1;
maze_rows = 10;
maze_columns = 10; 

function flip_coin() = round(rands(0, 1, 1)[0]) + 1;

function binary_tree_block(x, y, rows, columns) = 
    y == rows ? block_data(x, y, 1) : (        // the uppermost row
        x == columns ? block_data(x, y, 2) :   // the rightmost column
            block_data(x, y, flip_coin())      // flip a coin
    );

function binary_tree_algorithm(rows, columns) = 
    [
        for(y = [1:rows]) 
            for(x = [1:columns]) 
                binary_tree_block(x, y, rows, columns)
    ];

maze_blocks = binary_tree_algorithm(maze_rows, maze_columns);

draw_maze(
    maze_rows, 
    maze_columns, 
    maze_blocks, 
    block_width, 
    wall_thickness
);

Now, you can change the value of maze_rows or maze_columns to generate a bigger maze. If your friends don't know binary tree algorithm, it should be enough to deceive them :p

Binary tree algorithm