Tuesday, May 21, 2019

A 3D Python Maze for Art of Illusion

I've always had a fascination with mazes. While messing with the Python plugin for Art of Illusion I decided to write a maze generator in Python. The image to the right is a rendering of the maze that is produced by that code. I did it as a Scripted Object, which was probably not the best idea since it recalculates the maze whenever something changes, but my excuse is that I was testing out some things related to Python scripted objects in Art of Illusion.

I'm including the code below, but with the caveat that it has a defect at one of the boundaries that I haven't figured out yet. It doesn't remove a wall for one or two of the cells. Aside from that, it will produce a good 3D maze.

import random
mazeWidth = 25
mazeLength = 25
wallLength = 1.2
wallHeight = 2.25
wallThickness = 0.05

WALL_UP = 0
WALL_DOWN = 1
WALL_FIXED = 2

class cell():
  X = 0
  Y = 0
  N = WALL_UP
  S = WALL_UP
  E = WALL_UP
  W = WALL_UP
  visited = False
 
def createmaze(width, length):
  m = []
  for l in range(length):
    row = []
    for w in range(width):
      _cell = cell()
      _cell.X = w
      _cell.Y = l
      row.append(_cell)
    m.append(row)
   
  for l in range(length):
    m[l][0].N = WALL_FIXED
    m[l][width-1].S = WALL_FIXED
   
  for w in range(width):
    m[0][w].W = WALL_FIXED
    m[length-1][w].E = WALL_FIXED
   
  return m
 
def createWall(length, height, thickness):
  wall = Cube(thickness, length, height)
  return ObjectInfo(wall, CoordinateSystem(), "")
 
def drawMaze():
  for i in range(mazeLength):
    for j in range(mazeWidth):
      if maze[i][j].W != WALL_DOWN:
        obj = createWall(wallLength, wallHeight, wallThickness)
        if not obj is None:
          obj.getCoords().setOrientation(90.0, 0.0, 0.0)
          obj.getCoords().setOrigin(Vec3(j*wallLength-(wallLength*0.5)-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(mazeLength*wallLength/2)))
          self.addObject(obj)
     
      if maze[i][j].N != WALL_DOWN:
        obj = createWall(wallLength, wallHeight, wallThickness)
        if not obj is None:
          obj.getCoords().setOrientation(90.0, 90.0, 0.0)
          obj.getCoords().setOrigin(Vec3(j*wallLength-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(wallLength*0.5)-(mazeLength*wallLength/2)))
          self.addObject(obj)
 
  for i in range(mazeLength):
    obj = createWall(wallLength, wallHeight, wallThickness)
    if not obj is None:
      obj.getCoords().setOrientation(90.0, 0.0, 0.0)
      obj.getCoords().setOrigin(Vec3(mazeWidth*wallLength-(wallLength*0.5)-(mazeWidth*wallLength/2), wallHeight/2, i*wallLength-(mazeLength*wallLength/2)))
      self.addObject(obj)
   
  for j in range(mazeWidth):
    obj = createWall(wallLength, wallHeight, wallThickness)
    if not obj is None:
      obj.getCoords().setOrientation(90.0, 90.0, 0.0)
      obj.getCoords().setOrigin(Vec3(j*wallLength-(mazeWidth*wallLength/2), wallHeight/2, mazeLength*wallLength-(wallLength*0.5)-(mazeLength*wallLength/2)))
      self.addObject(obj)
     
def chooseUnvisited():
  neighbors = []
  if (current.N == WALL_UP) and (current.Y-1 >=0) and (not maze[current.Y-1][current.X].visited):
    neighbors.append(maze[current.Y-1][current.X])
  if (current.S == WALL_UP) and (current.Y+1 < mazeLength) and (not maze[current.Y+1][current.X].visited):
    neighbors.append(maze[current.Y+1][current.X])
  if (current.W == WALL_UP) and (current.X-1 >=0) and (not maze[current.Y][current.X-1].visited):
    neighbors.append(maze[current.Y][current.X-1])
  if (current.E == WALL_UP) and (current.X+1 < mazeWidth) and (not maze[current.Y][current.X+1].visited):
    neighbors.append(maze[current.Y][current.X+1])
 
  if len(neighbors) > 0:
    return random.choice(neighbors)
  else:
    return current
   
def removeWall(cur, nxt):
  print("Cur "+str(cur.X)+" "+str(cur.Y))
  print("Nxt "+str(nxt.X)+" "+str(nxt.Y))
  if nxt != cur:
    if cur.Y < nxt.Y and cur.S != WALL_FIXED:
      print("removeWall 1")
      maze[cur.Y][cur.X].S = WALL_DOWN
      maze[nxt.Y][nxt.X].N = WALL_DOWN
    elif cur.Y > nxt.Y and cur.N != WALL_FIXED:
      print("removeWall 2")
      maze[cur.Y][cur.X].N = WALL_DOWN
      maze[nxt.Y][nxt.X].S = WALL_DOWN
    elif cur.X < nxt.X and cur.E != WALL_FIXED:
      print("removeWall 3")   
      maze[cur.Y][cur.X].E = WALL_DOWN
      maze[nxt.Y][nxt.X].W = WALL_DOWN
    elif cur.X > nxt.X and cur.W != WALL_FIXED:
      print("removeWall 4")   
      maze[cur.Y][cur.X].W = WALL_DOWN
      maze[nxt.Y][nxt.X].E = WALL_DOWN    
    else:
      print("removeWall 5")   

maze = createmaze(mazeWidth, mazeLength)
theStack = []
current = maze[0][0] # choose initial
theStack.append(current) # push current

# choose unvisited neighbor, pop if none found
next = chooseUnvisited()
next.visited = True
print("Next "+str(next.X)+" "+str(next.Y))
while len(theStack) > 0:
  while next == current and len(theStack) > 0:
    current = theStack.pop()
    next = chooseUnvisited()
    next.visited = True
    print("Next "+str(next.X)+" "+str(next.Y))
 
  removeWall(current, next)
 
  current = next
  next = chooseUnvisited()
  next.visited = True
  if next != current:
    theStack.append(current)  
   
drawMaze()