Computer Science 15-112, Fall 2011
Optional Lecture Notes: 
Mazes

 


 

These notes were prepared by students attending the optional lecture on mazes.  As such, they may contain a small error here or there (standard disclaimer).  They are provided here as a service to those who may have missed the lecture.

 



First, here is the code that we wrote:  mazes.py

Also, here is a faster version that uses sets rather than island labels:  fasterMazes.py

Second, here is the header for the mazes.py file:

# mazes.py
# Code written during optional lecture on mazes.
# Given the group-based design and tight time constraints,
# there is a fine chance this code contains a bug here or there.
# Plus, there are a variety of ways to make this code much nicer
# (several of these were assigned as bonus for the lecture).
# Press "r" to reset to a new maze, and press "c" to switch
# to a circular view.
 


Making Mazes

 

Q. How do you make a maze?

 

First, we’re going to define a maze:

·   Collection of points connected by edges/nodes/vertices

·   Contains special privileged start and end points

·   Can get from any point to any point

 

So, a maze is essentially a series of dots with things connecting them…

:Screen shot 2011-10-29 at 5.46.45 PM.png

Q. How do we build this?

·   Start with a bunch of dots

·   Randomly pick one dot and a direction. Connect the chosen dot with the next dot in the specified direction

·   Repeat!

 

We’re going to connect things until everything is connected to everything else.

 

Here is an example that is valid but not optimal; you can remove any of the four edges of the box and still be fully connected.

:Screen shot 2011-10-29 at 5.54.26 PM.png

So we’re going to place an extra constraint: We want the minimum amount of links possible. Every link connects two distinct nodes, so we want to add one fewer links than the number of nodes.

 

For now on, we’re going to refer to nodes as islands and links as bridges.

 

For identification purposes, every island will have a name, initially unique. When two islands are connected by a bridge, they become one island and share the same name. From this, we know that if two islands have the same name, then they’re connected. Every time you want to add another bridge, you’ll need to find an island whose neighbor is a different island. When the maze is done, we’ll have only one island

 

What will we have to store for every island?

·   Label

·   Two True/False values for whether or not there is a bridge in the given direction (East, South)

(Note that you don’t need West/North because other islands store that information already)

 

First, let’s try to represent the maze.

 

def makeEmptyMaze(rows, cols):

    maze = [([0]*cols) for row in xrange(rows)] #create 2d list of islands

    label = 0

    for row in xrange(rows):

        for col in xrange(cols):

            maze[row][col] = Island(label)

            label += 1 #every single island receives a unique label

    return maze

 

def Island(label): #there are cleaner ways to do this but this works for our purposes

    island = Struct() #remember to have class Struct: pass

    island.label = label #pass in a label

    island.hasEastBridge = False #initially the island has no bridges

    island.hasSouthBridge = False

    return island

 

Now, we need a way to display the maze! Try printing it.

 

def printMaze(maze): #0 False False means the 0th node has no East and no South bridges

    rows = len(maze)

    cols = len(maze[0])

    for row in xrange(rows):

        for col in xrange(cols):

            island = maze[row][col]

            print "%2d" % island.label,

            print island.hasEastBridge, island.hasSouthBridge,

        print

 

Now we need to add our bridges. We will need rows*(cols-1) bridges…

 

def makeMaze(rows, cols):

    maze = makeEmptyMaze(rows, cols)

    for bridge in xrange(rows*cols-1):

        addRandomBridge(maze) # and we actually want to make them! see below…

    return maze

 

In order to add a bridge we need to first pick a random island and a random direction.

 

We know that if there is already a bridge, then the label on the target node will be the same as the label of the node we’re currently on. So, we just need to check if there’s a different island label on the node we’re trying to connect to.

 

def addRandomBridge(maze):

    rows = len(maze)

    cols = len(maze[0])

    while True:

        # keep trying to find a random island and random direction that

        # works (so we can add a bridge and keep the maze property)

        # and repeat infinitely until we get "lucky"

        islandRow = random.randint(0, rows-1) #random.randint() is inclusive

        islandCol = random.randint(0, cols-1)

        dir = random.randint(0, 3) #we will assign 0-3 to the directions North, South, East and West as global variables

        #note on global direction: if I refer to a variable that’s unbound to a function, it looks for a global variable

        #it’s usually not good to use these, but in this case it’s good to lock down the variables for consistency

        (targetRow, targetCol) = findTarget(islandRow, islandCol, dir)

        if ((targetRow >= 0) and (targetRow < rows) and

            (targetCol >= 0) and (targetCol < cols)): #we're on the board!

            if (maze[islandRow][islandCol].label != maze[targetRow][targetCol].label): #make sure target is different!

                addBridge(maze, islandRow, islandCol, dir, targetRow, targetCol) #establish a bridge

                break

 

A better way to do the above would be to create a LIST of all the bridges and pull out every bridge we’ve found. We’d then be able to choose from the shrinking list. However, our current strategy is good enough for our purposes.

 

Now we create our helper functions…

def findTarget(row, col, dir): #note that this is inferior to wordsearch’s drow and dcol

    if (dir == EAST): return (row, col+1) #nothing happens to the row when going East!

    elif (dir == WEST): return (row, col-1)

    elif (dir == NORTH): return (row-1, col)

    elif (dir == SOUTH): return (row+1, col)

    assert(False) # should never happen

def addBridge(maze, islandRow, islandCol, dir, targetRow, targetCol):

    # assume the bridge (and all parameters) are legal

    rows = len(maze)

    cols = len(maze[0])

    # 1. add the bridge itself by changing the hasFooBridge to True

    if (dir == EAST): maze[islandRow][islandCol].hasEastBridge = True

    elif (dir == SOUTH): maze[islandRow][islandCol].hasSouthBridge = True

 

Q. What if we’re moving West?

Every node is keeping track of East and South bridges. If we’re moving west, go to the target! West from the source = East from the target

    elif (dir == WEST): maze[targetRow][targetCol].hasEastBridge = True #west from the source = east from the target

    elif (dir == NORTH): maze[targetRow][targetCol].hasSouthBridge = True

    # 2. update all the labels replacing the larger label with the smaller label

    islandLabel = maze[islandRow][islandCol].label

    targetLabel = maze[targetRow][targetCol].label

    largerLabel = max(islandLabel, targetLabel)

    smallerLabel = min(islandLabel, targetLabel)

    for row in xrange(rows):

        for col in xrange(cols):

            if (maze[row][col].label == largerLabel):

                maze[row][col].label = smallerLabel

Now we need to display our islands and bridges!

 

We want to make our maze in init() so we only make one maze. In redrawAll, we need to display it. In order to display our maze, we need to at least display our islands!

 

We should represent islands as rectangles in the center of each square.

 

def drawMaze(maze):

    (rows, cols) = (len(maze), len(maze[0]))

    (width, height) = (canvas.data.width, canvas.data.height)

    r = 0.1*min(width/cols, height/rows)

    for row in xrange(rows):

        for col in xrange(cols):

            (cx, cy) = islandCenter(row, col) #we put this into a helper function because we’ll need the island center for bridges too

            # paint the island

            canvas.create_rectangle(cx-r, cy-r, cx+r, cy+r, fill="blue")

 

Now, we want to paint the bridges!

         # paint the bridges

            if (maze[row][col].hasEastBridge): #if there is an East bridge, we need to draw it

                (tx, ty) = islandCenter(row, col+1) #get the other island center

                canvas.create_line(cx, cy, tx, ty, width=2, fill="blue") #connect the two island centers with a line!

            if (maze[row][col].hasSouthBridge):

                (tx, ty) = islandCenter(row+1, col)

                canvas.create_line(cx, cy, tx, ty, width=2, fill="blue")           

 

And now onto our helper function for finding the center of each square... we’ll just look at the horizontal (meaning width and cols). The width of one cell will be width/cols. The center of col 0 is one half column over, so it can be represented as (col + 0.5)*(width/col). The symmetric formula can be used to find cy.

 

def islandCenter(row, col):

    maze = canvas.data.maze

    (rows, cols) = (len(maze), len(maze[0]))

    (width, height) = (canvas.data.width, canvas.data.height)

    return ((col+0.5)*width/cols, (row+0.5)*height/rows)

 

Now define reset in keyPressed() so it resets a new maze each time the user presses “r”. This simply calls init().

 

def keyPressed(event):

    if (event.char == "r"):

        init()

    redrawAll()

At this point we have a functioning maze creator. Now, what if we wanted to construct a polar maze? We will build the exact same maze, and display it circularly instead. Each row of our maze will be one of the concentric rings, and each column will be a spoke radiating out from the center.

First, we define “c” in keyPressed for circular mazes and add drawCMaze in redrawAll()

def keyPressed(event):

    if (event.char == "r"):

        init()

    elif (event.char == "c"):

        canvas.data.isCircular = not canvas.data.isCircular

    redrawAll()

def redrawAll():

    canvas.delete(ALL)

    maze = canvas.data.maze

    if (canvas.data.isCircular):

        drawCircularMaze(maze)

    else:

        drawMaze(maze)

So, how do we draw a circular maze? If it’s 4x4, we’ll have 4 rings and 4 spokes…

:Screen shot 2011-10-29 at 10.24.41 PM.png

·         Rows, cols = rings, spokes

·         South bridges point towards the middle

·         East bridges are the arcs

We didn’t reconstruct our maze generator to generate a truly radial maze. So we’ll have guaranteed missing edges from trying to take a radial projection from a maze that’s on a rectilinear grid. #Bonus! Add the missing edges to make it truly circular!

Now, we need to find the x and y values for a ring and spoke.

Q. How do I figure out the r for any given ring?

My outermost ring (largest r) should equal the minimum of (bx, by)

The innermost ring is 0. r = (ring+1)/rings * min(bx,by)

Here, you need to add one because otherwise ring 0 = 0!

 

Q. How do I find theta for spokes?

When the spoke is 0, theta should be 0. When the spoke is the max number of spokes, theta should be 2 pi.

Here, you don’t need to add one because you don’t want to go all the way around

Let’s also look at a little trig:

:Screen shot 2011-10-29 at 10.40.20 PM.png

In order to get the coordinates we want, we’ll need to add bx to rcos(theta) and subtract rsin(theta) from by. In this case, we’re subtracting because up is down!

We discard the idea of drawing arcs for now, so the only code we need to change AT ALL is islandCenter().

def circularIslandCenter(row, col):

    maze = canvas.data.maze

    (rows, cols) = (len(maze), len(maze[0]))

    (width, height) = (canvas.data.width, canvas.data.height)

    (bx, by) = (width/2, height/2)

    (ring, spoke) = (row, col) #renaming convention for self-documenting code

    (rings, spokes) = (rows, cols)

    maxRadius = 0.8*min(bx, by) #multiplying by 0.8 creates a buffer so no islands are clipped by the canvas

    r = float(maxRadius)*(ring+1)/rings #make sure you multiply first! Otherwise this amplifies the size of the roundoff

    theta = math.radians(360*spoke/spokes)

    return (bx+r*math.cos(theta), by-r*math.sin(theta)) #derived above

 

Look up how to create arcs with tkInter: http://www.pythonware.com/library/tkinter/introduction/canvas-arc.htm

We should change the East bridges to arcs since they’re the ones that are going from spoke to spoke. In order to do so, you need to specify a bounding box with some width, a fill of None, the center and radius that’s dependent on the ring, and the start and end angles. The radius will be the distance between x and y; however, the start and end angles will be more difficult. Our code that computed r theta should be returning the value instead of directly computing – change this and you should be able to draw actual arcs in your circular mazes!
 

Summary:

To make a maze…

We had a large list of islands and labeled them. We chose a random island and checked a random direction. If those two islands didn’t have the same label, we added a bridge and relabeled all the islands to the smaller of the two numbers. We repeated until everything was 0 (or repeat for rows*(cols-1) times). If we have really bad luck, we’ll keep guessing randomly forever -> this is a bad idea in principle. This works fine for the types of mazes we’re seeing but we can do better.