Main Page - Log in -

Brain Maze

From Noah.org

Jump to: navigation, search

This is a fractal algorithm I created for generating mazes. I think it's pretty neat. The original was written in C for a computer science class I was taking at UCSC. This version is in Python. The original would print spaces and hashes (#) to display the maze.

In this version I also added HTML output. When I came up with the algorithm there were fewer than 1000 web sites -- it wasn't popular yet. My original C source has this comment explaining the limit to the number of recursive iterations that could be done:

/* Six is the max value that seems to work on a 640K DOS machine */

Nowadays, my Apple Mac Mini with a dual core 1 GHz CPU can run the interpreted Python version in less than 400 ms when set for 6 iterations. It can do 10 iterations in under 2 minutes producing an output of 2047 columns by 2047 rows, taking 4 MB of ASCII text (XPM format). This is easily converted to PNG. You may click on the following link to view the maze: Media:big_maze.png

This generates a random maze with no loops. It is a Lattice Graph spanning tree -- a connected, undirected graph that uses all the vertices in a graph with no cycles. This program will also optionally "solve" the maze. That means it will show you two points that are as far apart as possible. The corresponds to the diameter of the tree. There may be more than one path that has a length equal to the longest path length. The full path is not shown. Only the beginning and end points are shown... This is a very slow calculation.

You can run the brain maze algorithm by clicking here: Generate a Brainmaze

The mazes generated by this algorithm are made up of the following maze atoms. This is the number of trees in a 2x2 Grid Tree.

    # #    ###    ###    ###    # #    ###
    # #    # #      #    #      ###     #
    ###    # #    ###    ###    # #    ###

This can be simplified by removing rotated copies to these two maze atoms:

    ###    # #
    # #    ###
    # #    # #

Click here to download: brainmaze.py

#!/usr/bin/env python

"""
SYNOPSIS

    brainmaze.py [-h, --help] [-v,--verbose] [--version] [--html] [--solve] [NUMBER_ITERATIONS]

DESCRIPTION

    This generates square fractal mazes with no loops. The maze is a spanning
    tree -- a connected, undirected graph that uses all the vertices in a graph
    with no cycles.

    The algorithm works like this. First four sub-mazes are generated and
    arranged into four quadrants. Then they are joined in three random places
    along the horizontal and vertical boundaries of the four quandrants. This
    is repeated until you get tired.

        ### ###     ### ###
        # #  #      # #  # 
        # # ###     # # ###
                -->   # #  
        # # ###     # # ###
        # # # #     # # # #
        ### # #     ##### #

        ### ### ### # #     ### ### ### # #
        # #  #  #   ###     # #  #  #   ###
        # # ### ##### #     # # ### ##### #
          # #   #             # #   #      
        # # ### # ### #     # # ##### ### #
        # # # # ### ###     # # # # ### ###
        ##### # # # # #     ##### # # # # #
                        -->  #             
        ### ### # # ###     ### ### # # ###
        #     # ### # #     #     # ### # #
        ####### # ### #     ######### ### #
           #    #              #    #      
        # ##### ### # #     # ##### ### # #
        ###  #   #  ###     ###  #   #  ###
        # # ### ##### #     # # ### ##### #

    Note that all mazes are made up of these "atmomic" mazes:

        # #    ###    ###    ###    # #    ###
        # #    # #      #    #      ###     #
        ###    # #    ###    ###    # #    ###

    (c)Copyright 1991 Noah Spurrier
    CIS 118
    Monday, Wednesday class from 1:00 to 3:00
    Program 10 generates square mazes using a recursive algorithm
    that I designed. This algorithm works by dividing up a big maze
    into quadrants and then trying the make mazes in the smaller sections.

EXAMPLES

    ./brainmaze.py
    ./brainmaze.py 5
    ./brainmaze.py --solve 3
    ./brainmaze.py --html

AUTHOR

    Noah Spurrier<noah@noah.org>

LICENSE

    This script is in the public domain, free from copyrights or restrictions.

VERSION

    $Id: brainmaze.py 230 2008-03-24 20:49:37Z noah $
"""

import sys, os, traceback, optparse
import time
import random
import cgi

WALL = ' '
PASSAGE = '#' 

class Maze:

    """This class stores the state of a maze. It does not contain the algorithm
    that generates a maze. Note that this stores the maze "graphically" and not
    as a tree data structure, so it may seem a little confusing if you are
    expecting structures like nodes and edges. So the maze is literally stored
    in an array. For example:

     [['#', '#', '#'],
      ['#', ' ', '#'],
      ['#', ' ', '#']]
     
    The advantage of this method is that it is more "visual" -- you can see
    what is going on just by converting the maze to a string. Of course, later
    on this would bite me when I wanted to trace paths through the maze. I had
    to convert it to tree structure, but that becomes yet another fun exercise!
    
    The size is the number of fractal cells along one side. A single fractal
    cell is actually made up of up of 4 nodes. This algorithm assumes a square,
    so both sides get the same size."""

    def __init__ (self, size, init_value):

        self.size = size
        self.size_x = size
        self.size_y = size
        self.m = [init_value] * (self.size_x * self.size_y)

    def get (self, x, y):

        if (x < 0) or (x >= self.size_x):
            return WALL
        if (y < 0) or (y >= self.size_y):
            return WALL
        return self.m [self.size * y + x]
        
    def put (self, x, y, value):

        self.m [self.size * y + x] = value
        
    def __str__ (self):

        s = ''
        for y in range(self.size):
            for x in range(self.size):
                value = self.get(x,y)
                s = s + value
            s = s + '\n'
        return s
        
    def html_table (self, cell_size=9):

        """This returns the Maze as a string in HTML format."""

        s = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>A Brain Maze Fractal</title>
</head>
<body bgcolor="#000000" text="#ffffff">
'''
        s = s + '\n<!-- \n' + str (self) + '-->\n'

        s = s + '\n<table border="0" cellpadding="0" cellspacing="0">\n'
        for x in range (self.size):
            s = s + '<tr>\n'
            for y in range (self.size):
                value = self.get (x,y)
                if value == WALL:
                    s = s + '<td  width="%d" height="%d" bgcolor="#000000"></td>\n' % (cell_size, cell_size)
                elif value == PASSAGE:
                    s = s + '<td  width="%d" height="%d" bgcolor="#ffffff"></td>\n' % (cell_size, cell_size)
                elif value == 'B':
                    s = s + '<td  width="%d" height="%d" bgcolor="#ff0000"></td>\n' % (cell_size, cell_size)
                else:
                    s = s + '<td  width="%d" height="%d" bgcolor="#00ff00"></td>\n' % (cell_size, cell_size)
            s = s + '</tr>\n'
        s = s + '''</table>\n\n</body></html>'''
        return s
    
def mergemaze (g1, g2, g3, g4):

    """This merges four small mazes into a bigger maze. They are arranged into
    a 2x2 pattern. The small mazes, g1,g2,g3,g4, should all be the same size.
    """

    subsize = g1.size
    result = Maze (subsize * 2 + 1, PASSAGE)

    for x in range(g1.size):
        for y in range(g1.size):
            g1_value = g1.get(x,y)
            g2_value = g2.get(x,y)
            g3_value = g3.get(x,y)
            g4_value = g4.get(x,y)

            result.put(x, y, g1_value)
            result.put(x+subsize+1, y, g2_value)
            result.put(x, y+subsize+1, g3_value)
            result.put(x+subsize+1, y+subsize+1, g4_value)

    cc = subsize
    for c in range (result.size):
        result.m [(cc * result.size) + c] = WALL
        result.m [(c * result.size) + cc] = WALL          

    return result


def join_horiz_left (maze, subsize):

    """
    X X
    |
    X X
    """

    while 1:
        x = random.randrange (subsize + 1) 
        v1 = maze.get (x, subsize - 1)
        v2 = maze.get (x, subsize)
        v3 = maze.get (x, subsize + 1)
        if v1 == PASSAGE and v2 == WALL and v3 == PASSAGE:
            maze.put (x, subsize, PASSAGE)
            break

def join_horiz_right (maze, subsize):

    """
    X X
      |
    X X
    """
    
    while 1:
        x = random.randrange (subsize + 1) + subsize 
        v1 = maze.get (x, subsize - 1)
        v2 = maze.get (x, subsize)
        v3 = maze.get (x, subsize + 1)
        if v1 == PASSAGE and v2 == WALL and v3 == PASSAGE:
            maze.put (x, subsize, PASSAGE)
            break

def join_vert_top (maze, subsize):

    """
    X-X
    
    X X
    """
    
    while 1:
        y = random.randrange (subsize + 1)
        v1 = maze.get (subsize - 1, y)
        v2 = maze.get (subsize, y)
        v3 = maze.get (subsize + 1, y)
        if v1 == PASSAGE and v2 == WALL and v3 == PASSAGE:
            maze.put (subsize, y, PASSAGE)
            break
            
def join_vert_bottom (maze, subsize):

    """
    X X
    
    X-X
    """
    
    while 1:
        y = random.randrange (subsize + 1) + subsize 
        v1 = maze.get (subsize - 1, y)
        v2 = maze.get (subsize, y)
        v3 = maze.get (subsize + 1, y)
        if v1 == PASSAGE and v2 == WALL and v3 == PASSAGE:
            maze.put (subsize, y, PASSAGE)
            break

def join4 (g1, g2, g3, g4):

    """ This function joins together four little mazes into one big maze. This
    function calls mergemaze(). This function then tries to figure out a good
    place to connect the mazes. It does this by hunting through the middle
    horizontal or vertical until it finds a space that has a PASSAGE both above
    and below OR both left and right. Then it puts a PASSAGE between those two
    PASSAGEs. The reason there are two choices is because there must be a total
    of three connectors between the four mazes. Two connecting PASSAGEs must be
    on the same row OR column, the third PASSAGE must be in the line at right
    angles two the first two. (I NEED A DAMN PICTURE TO EXPLAIN THIS PROPERLY!)
    So you can have two connectors in a row and one connector in a column or
    you can have one connector in a row and two connectors in a column. It's
    YOUR choice. But they have to be in that configuration. In other words you
    just have to make sure that the row and column has at least one connector.
    Jeeze...
    
    To join four mazes together I connect them in three random places. The
    connections occur in the horizontal or vertical wall between the four
    mazes. They may also connect in the middle if there happens to be two
    connections on either side. That means that the third connection is
    dependant on the first two. And THAT means there is an order dependency. To
    remove this dependency I have to add another random step -- I have to
    randomly choose to connect horizontally or vertically first."""

    suppress_choice = random.randrange(4)
    result = mergemaze (g1, g2, g3, g4)
    subsize = g1.size

    # Vertical first if 0 or 1.
    if suppress_choice == 0 or suppress_choice == 1:
        join_vert_top (result, subsize)
        join_vert_bottom (result, subsize)
        if suppress_choice != 0:
            join_horiz_left (result, subsize)
        if suppress_choice != 1:
            join_horiz_right (result, subsize)

    # Horizontal first if 2 or 3.
    if suppress_choice == 2 or suppress_choice == 3:
        join_horiz_left (result, subsize)
        join_horiz_right (result, subsize)
        if suppress_choice != 2:
            join_vert_top (result, subsize)
        if suppress_choice != 3:
            join_vert_bottom (result, subsize)

    return result

def build (size):

    """This is the main recursive method used to build the maze. Note that this
    generates a square maze. """

    if size <= 1: # Terminate the recursive loop.
        g1 = Maze (1, PASSAGE)
        g2 = Maze (1, PASSAGE)
        g3 = Maze (1, PASSAGE)
        g4 = Maze (1, PASSAGE)
    else:
        g1 = build (size - 1)
        g2 = build (size - 1)
        g3 = build (size - 1)
        g4 = build (size - 1)

    result = join4 (g1, g2, g3, g4)
    return result

def build_tree_from_maze (maze):

    """This turns a maze array into a tree. The algorithm that builds mazes
    doesn't use a tree data structure. It builds them graphically which does not
    provide easy access to the data structure. This function produces a tree
    from the maze graphic representation. """

    tree = {}
    for y in range (0, maze.size_y):
        for x in range (0, maze.size_x):
            if maze.get(x,y) == PASSAGE:
                tree[(x,y)] = []
                if maze.get(x-1,y) == PASSAGE: tree[(x,y)].append((x-1,y))
                if maze.get(x+1,y) == PASSAGE: tree[(x,y)].append((x+1,y))
                if maze.get(x,y-1) == PASSAGE: tree[(x,y)].append((x,y-1))
                if maze.get(x,y+1) == PASSAGE: tree[(x,y)].append((x,y+1))
    return tree

def build_adjacency_distance_matrix (node_list):

    """This builds a dictionary that stores the distance from any point to any
    other point. This only initializes the matrix, so it just fills in the
    distance from a node to the immediate neighbor. Later the matrix will
    filled-in using fill_adjacency_distance_matrix. This is intended as an
    illustration of the algorithm, not as an illustration of an efficient data
    structure. """

    adj_matrix = {}
    for key_node in node_list:
        adj_matrix[key_node] = {}
        for node in node_list:
            dist = None
            if node == key_node:
                dist = 0
            elif node in node_list[key_node]:
                dist = 1
            adj_matrix[key_node][node] = dist
    return adj_matrix

def memoize (f):

    """This is a decorator which adds memoization to a function."""

    cache= {}
    def memoize_function (*x):
        if x not in cache:
            cache[x] = f(*x)
        return cache[x]
    return memoize_function

class MemoizeMutable:
    def __init__(self, fn):
        self.fn = fn
        self.memo = {}
    def __call__(self, *args, **kwds):
        import cPickle
        str = cPickle.dumps(args, 1)+cPickle.dumps(kwds, 1)
        if not self.memo.has_key(str):
            self.memo[str] = self.fn(*args, **kwds)
        return self.memo[str]

def fill_adjacency_distance_matrix (adj_matrix):

    """This takes the adjacency distance matrix and fills in all the None
    entries. When done this will yield a matrix that will tell you the distance
    between any two point in the maze tree. This is slow -- exponential,
    O(c^N)"""

    done = False
    while not done:
        done = True
        for key_node, dist_dict in adj_matrix.items():
            for node, dist in dist_dict.items():
                if dist is None:
                    dist = get_distance (adj_matrix, key_node, node)
                    if dist is not None:
                        dist_dict[node] = dist
                        adj_matrix[key_node] = dist_dict
                    else:
                        done = False
    return adj_matrix

def get_distance (adj_matrix, node1, node2):

    """If possible this returns the distance between two nodes. If the distance
    cannot be determined then it returns None. If the distance is known it is
    simply returned. Otherwise, this will look one level deep to see if the
    distance from the next node is known. """

    dist = adj_matrix[node1][node2]
    if dist is not None:
        return dist
    for node3 in adj_matrix[node1].keys():
        if node3 == node1:
            continue
        dist = adj_matrix[node1][node3]
        # Only consider the nodes directly connected to this node.
        if dist != 1:
            continue
        deeper_dist = adj_matrix[node3][node2] 
        if deeper_dist is None:
            continue
        return dist + deeper_dist
    return None

def main ():

    global options, args

    # If running as a CGI then force HTML output.
    if os.environ.has_key('REQUEST_METHOD'):
        options.html = True

    if options.html:
        print "Content-Type: text/html"
        print

    my_maze = build(args[0])

    if options.solve:
        if options.verbose: print "Solving maze..."
        tree = build_tree_from_maze (my_maze)
        adj = build_adjacency_distance_matrix (tree)
        print "fill_adjacency_distance_matrix"
        print time.time()
        adj = fill_adjacency_distance_matrix (adj)
        print time.time()
        max_d = None
        for key_node, dist_dict in adj.items():
            for node, dist in dist_dict.items():
                if dist is not None:
                    if max_d is None or adj[max_d[0]][max_d[1]] <= dist:
                        max_d=(key_node, node)
        if options.verbose: print "distance from node",max_d[0],"to node",max_d[1], "->", adj[max_d[0]][max_d[1]]
        # Mark the beginning and end of the two most distant nodes.
        my_maze.put(max_d[0][0],max_d[0][1],'B')
        my_maze.put(max_d[1][0],max_d[1][1],'E')

    if options.html:
        print my_maze.html_table(9)
    else:
        print str(my_maze)

if __name__ == '__main__':
    try:
        start_time = time.time()
        parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(), usage=globals()['__doc__'], version='$Id: brainmaze.py 230 2008-03-24 20:49:37Z noah $')
        parser.add_option ('-v', '--verbose', action='store_true', default=False, help='verbose output')
        parser.add_option ('--html', action='store_true', default=False, help='Generate output in HTML.')
        parser.add_option ('--solve', action='store_true', default=False, help='Show beginning and end of longest path through maze.')
        (options, args) = parser.parse_args()
        if len(args) < 1:
            args.append (4)
        else:
            args[0] = int(args[0])
        if options.verbose: print time.asctime()
        main()
        if options.verbose: print time.asctime()
        if options.verbose: print 'TOTAL TIME IN MINUTES:',
        if options.verbose: print (time.time() - start_time) / 60.0
        sys.exit(0)
    except KeyboardInterrupt, e: # Ctrl-C
        raise e
    except SystemExit, e: # sys.exit()
        raise e
    except Exception, e:
        print 'ERROR, UNEXPECTED EXCEPTION'
        print str(e)
        traceback.print_exc()
        os._exit(1)



-->