Friday, July 20, 2012

python: maze generation and solution

Here I'm posting a maze generation and resolve class written in python. I wanted to learn python and generating and solving mazes is a good exercise to start with.
In addition to the maze class I've written another script using pygame to show the maze and its solution path in a window.
This is my first try with python, there is for sure a best way of doing things. Please feel free to comment and suggest!

The generation algorithm creates a "Perfect" maze. This means that, assuming I wrote it correctly, for every pair of cells chosen there is always exactly one path connecting them. No loops and no isolated regions.
There are many ways for solving mazes, I choose a "Shortest Path Finder" here.

This is a very good website with tons of information about maze-types, generation algorithms, solution algorithms, etc.

Maze Class: Crate, solve and display on terminal

mymazeclass.py
  1 #=============================================================================#
  2 # Name        : mymazeclass.py                                                #
  3 # Autor       : Adrian Antonana                                               #
  4 # Date        : July, 20 2012                                                 #
  5 # Description : Maze class. Constructor generates random perfect maze. Maze   #
  6 #               can also be solved.                                           #
  7 #=============================================================================#
  8 
  9 
 10 # imports
 11 import random
 12 import sys
 13 
 14 
 15 # constants and help for list access
 16 BOTTOMWALL = 0
 17 RIGHTWALL = 1
 18 VISITED = 2
 19 UP = 0
 20 RIGHT = 1
 21 DOWN = 2
 22 LEFT = 3
 23 
 24 
 25 # maze class definition
 26 class Maze:
 27 
 28   #object constructor
 29   def __init__(self,rows,cols):
 30   
 31     # set maze size, walls and visited values
 32     self.rows = rows
 33     self.cols = cols
 34     self.maze = [[[True,True,False] for j in range(cols)] for i in range(rows)]
 35 
 36     # set current position, start and end point
 37     # start and end points are used to solve the maze
 38     # currrow is used to generate the maze
 39     self.startrow = random.randrange(rows)
 40     self.startcol = random.randrange(cols)
 41 
 42     self.endrow = random.randrange(rows)
 43     self.endcol = random.randrange(cols)
 44 
 45     currrow = self.startrow
 46     currcol = self.startcol
 47 
 48     # The searh can be quite deep
 49     if rows*cols > sys.getrecursionlimit():
 50       sys.setrecursionlimit(rows*cols+10)
 51 
 52     # generate the maze with depth-first algorithm
 53     self._gen_maze(currrow,currcol)
 54 
 55     # number matrix for solving
 56     self.numtable = [[-1 for j in range(cols)]for i in range(rows)]
 57 
 58     #solution path
 59     self.solutionpath = []
 60 
 61   #-----------------------------------------------------------------------------
 62 
 63   # returns the maze in ascii characters for printing on terminal
 64   def __str__(self):
 65 
 66     # the upper wall first
 67     outtable = '.'+self.cols*'_.'+'\n'
 68 
 69     for i in range(self.rows):
 70       outtable += '|'
 71 
 72       for j in range(self.cols):
 73         if self.maze[i][j][BOTTOMWALL]:
 74           outtable += '_'
 75         else:
 76           outtable += ' '
 77         if self.maze[i][j][RIGHTWALL]:
 78           outtable += '|'
 79         else:
 80           outtable += '.'
 81 
 82       outtable += '\n'
 83 
 84     outtable += 'Start Point   : ('+str(self.startrow)+','+str(self.startcol)+')\n'
 85     outtable += 'End Point     : ('+str(self.endrow)+','+str(self.endcol)+')\n'
 86 
 87     return outtable
 88 
 89   #------------------------------------------------------------------------------
 90 
 91   # get a list with posible directions from the current position
 92   def _get_dirs(self,r,c):
 93     dirlist = []
 94 
 95     # check limits
 96     if r-1 >= 0           : dirlist.append(UP)
 97     if r+1 <= self.rows-1 : dirlist.append(DOWN)
 98     if c-1 >= 0           : dirlist.append(LEFT)
 99     if c+1 <= self.cols-1 : dirlist.append(RIGHT)
100 
101     return dirlist
102 
103   #------------------------------------------------------------------------------
104 
105   # generates the maze with depth-first algorithm
106   def _gen_maze(self,r,c,d=None):
107     maze = self.maze
108 
109     # knock down the wall between actual and previous position
110     maze[r][c][VISITED] = True
111     if   d == UP    : maze[r]  [c]    [BOTTOMWALL] = False
112     elif d == DOWN  : maze[r-1][c]    [BOTTOMWALL] = False
113     elif d == RIGHT : maze[r]  [c-1]  [RIGHTWALL]  = False
114     elif d == LEFT  : maze[r]  [c]    [RIGHTWALL]  = False
115 
116     # get the next no visited directions to move
117     dirs = self._get_dirs(r,c)
118 
119     # random reorder directions
120     for i in range(len(dirs)):
121       j = random.randrange(len(dirs))
122       dirs[i],dirs[j] = dirs[j],dirs[i]
123 
124     # make recursive call if the target cell is not visited
125     for d in dirs:
126       if d==UP:
127         if not maze[r-1][c][VISITED]:
128           self._gen_maze( r-1,c,UP )
129       elif d==DOWN:
130         if not maze[r+1][c][VISITED]:
131           self._gen_maze( r+1,c,DOWN )
132       elif d==RIGHT:
133         if not maze[r][c+1][VISITED]:
134           self._gen_maze( r,c+1,RIGHT )
135       elif d==LEFT:
136         if not maze[r][c-1][VISITED]:
137           self._gen_maze( r,c-1,LEFT )
138 
139   #------------------------------------------------------------------------------
140 
141   # solve the maze by filling it with numbers(algorithm name?)
142   def _solve_maze_aux(self,r,c,n):
143     maze = self.maze
144     numtable = self.numtable
145     numtable[r][c] = n
146 
147     # check if the end has been reached
148     if (r,c) != (self.endrow,self.endcol):
149       directions = self._get_dirs(r,c)
150 
151       # recursive calls only if there is no wall between cells and
152       # targel cell is not marked (=-1)
153       for d in directions:
154         if   d==UP    and not maze[r-1][c][BOTTOMWALL] and numtable[r-1][c] == -1:
155           self._solve_maze_aux(r-1,c,n+1)
156         elif d==DOWN  and not maze[r][c][BOTTOMWALL]   and numtable[r+1][c] == -1:
157           self._solve_maze_aux(r+1,c,n+1)
158         elif d==RIGHT and not maze[r][c][RIGHTWALL]    and numtable[r][c+1] == -1:
159           self._solve_maze_aux(r,c+1,n+1)
160         elif d==LEFT  and not maze[r][c-1][RIGHTWALL]  and numtable[r][c-1] == -1:
161           self._solve_maze_aux(r,c-1,n+1)
162 
163   #------------------------------------------------------------------------------
164 
165   # get the solution path
166   def _get_solution_path(self):
167     actrow = self.endrow
168     actcol = self.endcol
169     startrow = self.startrow
170     startcol = self.startcol
171     path = []
172     numtable = self.numtable
173     path = self.solutionpath
174 
175     while (actrow,actcol) != (startrow,startcol):
176       path.append((actrow,actcol))
177       directions = self._get_dirs(actrow,actcol)
178       for d in directions:
179         if d== UP:
180           if numtable[actrow][actcol]-1 == numtable[actrow-1][actcol]:
181             actrow -=1
182             break
183         elif d== DOWN:
184           if numtable[actrow][actcol]-1 == numtable[actrow+1][actcol]:
185             actrow += 1
186             break
187         elif d== LEFT:
188           if numtable[actrow][actcol]-1 == numtable[actrow][actcol-1]:
189             actcol -= 1
190             break
191         elif d== RIGHT:
192           if numtable[actrow][actcol]-1 == numtable[actrow][actcol+1]:
193             actcol += 1
194             break
195 
196     path.append((actrow,actcol))
197     path.reverse()
198 
199   #------------------------------------------------------------------------------
200 
201   # solve the maze
202   def solve_maze(self):
203     self._solve_maze_aux(self.startrow,self.startcol,0)
204     self._get_solution_path()

Pygame: Show maze

It is possible to print the maze on the terminal with the above class but it doesn't look very good. So I defined a few functions using pygame to show mazes properly including the solution path.

mazeprogram.py
  1 #!/usr/bin/python2
  2 
  3 
  4 #=============================================================================#
  5 # Name        : mazeprogram.py                                                #
  6 # Autor       : Adrian Antonana                                               #
  7 # Date        : July, 20 2012                                                 #
  8 # Description : Function definitions for drawing a maze in a window using     #
  9 #               pygame and the maze class (mymazeclass.py) I made.            #
 10 #=============================================================================#
 11 
 12 # imports
 13 from mymazeclass import *
 14 from sys import *
 15 import pygame
 16 
 17 
 18 #-------------------------------------------------------------------------------------#
 19 #              function definitions for drawing the maze with pygame                  #
 20 #-------------------------------------------------------------------------------------#
 21 
 22 # generates a window with maze with all cells isolated from each other
 23 def base_window(m):
 24   winwidth = m.cols*CELLSIZE+(m.cols+1)*WALLSIZE
 25   winheight = m.rows*CELLSIZE+(m.rows+1)*WALLSIZE
 26   w = pygame.display.set_mode((winwidth,winheight))
 27   w.fill(BLACK)
 28 
 29   for i in range(m.rows):
 30     for j in range(m.cols):
 31       pygame.draw.rect(w,WHITE,(WALLSIZE+(j*CELLSIZE+j*WALLSIZE),WALLSIZE+(i*CELLSIZE+
 32       i*WALLSIZE),CELLSIZE,CELLSIZE))
 33 
 34   return w
 35 
 36 #--------------------------------------------------------------------------------------
 37 
 38 # knocks down walls from base_window to create the path
 39 def maze_window(m):
 40   w = base_window(m)
 41 
 42   for i in range(m.rows):
 43     for j in range(m.cols):
 44       if not m.maze[i][j][BOTTOMWALL]:
 45         pygame.draw.rect(w,WHITE,(j*CELLSIZE+(j+1)*WALLSIZE,(i+1)*CELLSIZE+(i+1)
 46         *WALLSIZE,CELLSIZE,WALLSIZE))
 47       if not m.maze[i][j][RIGHTWALL]:
 48         pygame.draw.rect(w,WHITE,((j+1)*CELLSIZE+(j+1)*WALLSIZE,i*CELLSIZE+(i+1)
 49         *WALLSIZE,WALLSIZE,CELLSIZE))
 50 
 51   pygame.display.update()
 52   return w
 53   
 54 #--------------------------------------------------------------------------------------
 55 
 56 # paints the solution path in the maze window
 57 def maze_path_window(m,w):
 58   path = m.solutionpath
 59 
 60   # print every cell within the solution path
 61   for index in range(len(path)-1):
 62     actrow = path[index][0]
 63     actcol = path[index][1]
 64     nextrow = path[index+1][0]
 65     nextcol = path[index+1][1]
 66     pygame.draw.rect(w,RED,(actcol*CELLSIZE+(actcol+1)*WALLSIZE,actrow*CELLSIZE+(actrow+
 67     1)*WALLSIZE,CELLSIZE,CELLSIZE))
 68 
 69     # also paint the white spaces between the cells
 70     if actrow == nextrow:
 71       if actcol < nextcol:
 72         pygame.draw.rect(w,RED,((actcol+1)*CELLSIZE+(actcol+1)*WALLSIZE,actrow*CELLSIZE+
 73         (actrow+1)*WALLSIZE,WALLSIZE,CELLSIZE))
 74       else:
 75         pygame.draw.rect(w,RED,(actcol*CELLSIZE+actcol*WALLSIZE,actrow*CELLSIZE+(actrow+
 76         1)*WALLSIZE,WALLSIZE,CELLSIZE))
 77     elif actcol == nextcol:
 78       if actrow < nextrow:
 79         pygame.draw.rect(w,RED,(actcol*CELLSIZE+(actcol+1)*WALLSIZE,(actrow+1)*CELLSIZE+
 80         (actrow+1)*WALLSIZE,CELLSIZE,WALLSIZE))
 81       else:
 82         pygame.draw.rect(w,RED,(actcol*CELLSIZE+(actcol+1)*WALLSIZE,actrow*CELLSIZE+
 83         actrow*WALLSIZE,CELLSIZE,WALLSIZE))
 84 
 85 
 86   # add a different color for start and end cells
 87   startrow = path[0][0]
 88   startcol = path[0][1]
 89   endrow = path[-1][0]
 90   endcol = path[-1][1]
 91 
 92   pygame.draw.rect(w,BLUE,(startcol*CELLSIZE+(startcol+1)*WALLSIZE,startrow*CELLSIZE+(
 93   startrow+1)*WALLSIZE,CELLSIZE,CELLSIZE))
 94   pygame.draw.rect(w,GREEN,(endcol*CELLSIZE+(endcol+1)*WALLSIZE,endrow*CELLSIZE+(endrow+
 95   1)*WALLSIZE,CELLSIZE,CELLSIZE))
 96   pygame.display.update()
 97   
 98 
 99 #================================================================================#
100 #                                Main Program                                    #
101 #================================================================================#
102 
103 # sizes and colors
104 CELLSIZE = 5
105 WALLSIZE = 2
106 
107 WHITE = (255,255,255)
108 BLACK = (0,0,0)
109 RED = (255,0,0)
110 GREEN = (0,255,0)
111 BLUE = (0,0,255)
112 
113 # get the maze size from program arguments
114 rows = int(argv[1])
115 cols = int(argv[2])
116 
117 # generate random maze, solve it
118 maze = Maze(rows,cols)
119 print maze
120 
121 maze.solve_maze()
122 #print maze
123 #print maze.solutionpath
124 
125 # show the maze with the solution path
126 pygame.init()
127 window = maze_window(maze)
128 maze_path_window(maze,window)
129 
130 while True:
131   for event in pygame.event.get():
132     if event.type == pygame.QUIT:
133       sys.exit()

Example Screenshot

Running "mazeprogram.py" an the shell as follows:
    $ ./mazeprogram.py 40 40

Generate + Solve Time

A graphic comparing maze size with generation + resolution time.

Nice Vs Ugly Code

In my first try I wrote some code without really thinking about what data structures I needed and how exactly I wanted to solve the maze generation and solution problems. As a result the "ugly" code is extremely slow when maze size increases. Here is a comparison between both codes execution times.
Good Code Vs Bad Code comparison:
Take care of your code!!!

3 comments:

  1. What graphing software is this? It doesn't look like matplotlib.

    ReplyDelete
  2. Hi, I'm new on this, how can I print on the terminal the first one, without using pygame?

    ReplyDelete