#!/usr/bin/env python # Draw a cylindrical maze suitable for plotting with the Eggbot # The maze itself is generated using a depth first search (DFS) # Written by Daniel C. Newman for the Eggbot Project # Improvements and suggestions by W. Craig Trader # 20 September 2010 # Update 26 April 2011 by Daniel C. Newman # # 1. Address Issue #40 # The extension now draws the maze by columns, going down # one column of cells and then up the next column. By using # this technique, the impact of slippage is largely limited # the the West and East ends of the maze not meeting. Otherwise, # the maze will still look quite well aligned both locally and # globally. Only very gross slippage will impact the local # appearance of the maze. # # Note that this new drawing technique is nearly as fast as # the prior method. The prior method has been preserved and # can be selected by setting self.hpp = True. ("hpp" intended # to mean "high plotting precision".) # # 2. Changed the page dimensions to use a height of 800 rather # than 1000 pixels. # # 3. When drawing the solution layer, draw the ending cell last. # Previously, the starting and ending cells were first drawn, # and then the solution path itself. That caused the pen to # move to the beginning, the end, and then back to the beginning # again to start the solution path. Alternatively, the solution # path might have been drawn from the end to the start. However, # just drawing the ending cell last was easier code-wise. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import sys import array import random import math import inkex import simplestyle # Initialize the psuedo random number generator random.seed() PLOT_WIDTH = int( 3200 ) # Eggbot plot width in pixels PLOT_HEIGHT = int( 800 ) # Eggbot plot height in pixels TARGET_WIDTH = int( 3200 ) # Desired plot width in pixels TARGET_HEIGHT = int( 600 ) # Desired plot height in pixels # Add a SVG path element to the document # We could do this just as easily as a polyline def draw_SVG_path( pts, c, t, parent ): if ( pts is None ) or len( pts ) == 0: # Nothing to draw return if isinstance( pts, list ): assert len( pts ) % 3 == 0, "len(pts) must be a multiple of three" d = "%s %d,%d" % ( pts[0], pts[1], pts[2] ) for i in range( 3, len( pts ), 3 ): d += " %s %d,%d" % ( pts[i], pts[i+1], pts[i+2] ) elif isinstance( pts, str ): d = pts else: return style = { 'stroke':c, 'stroke-width':str( t ), 'fill':'none' } line_attribs = { 'style':simplestyle.formatStyle( style ),'d':d } inkex.etree.SubElement( parent, inkex.addNS( 'path','svg' ), line_attribs ) # Add a SVG rect element to the document def draw_SVG_rect( x, y, w, h, c, t, fill, parent ): style = { 'stroke':c, 'stroke-width':str( t ), 'fill':fill } rect_attribs = { 'style':simplestyle.formatStyle( style ), 'x':str( x ), 'y':str( y ), 'width':str( w ), 'height':str( h ) } inkex.etree.SubElement( parent, inkex.addNS( 'rect', 'svg' ), rect_attribs ) class Maze( inkex.Effect ): # Each cell in the maze is represented using 9 bits: # # Visited -- When set, indicates that this cell has been visited during # construction of the maze # # Border -- Four bits indicating which if any of this cell's walls are # part of the maze's boundary (i.e., are unremovable walls) # # Walls -- Four bits indicating which if any of this cell's walls are # still standing # # Visited Border Walls # x x x x x x x x x # W S E N W S E N _VISITED = 0x0100 _NORTH = 0x0001 _EAST = 0x0002 _SOUTH = 0x0004 _WEST = 0x0008 def __init__( self ): inkex.Effect.__init__( self ) self.OptionParser.add_option( "--tab", action="store", type="string", dest="tab", default="controls", help="The active tab when Apply was pressed" ) self.OptionParser.add_option( "--mazeSize", action="store", type="string", dest="mazeSize", default="MEDIUM", help="Difficulty of maze to build" ) #self.OptionParser.add_option( # "--hpp", action="store", type="inkbool", dest="hpp", default=False, # help="Use a faster plotting technique that requires much better plotting precision" ) #self.hpp = self.options.hpp self.hpp = False self.w = int( 0 ) self.h = int( 0 ) self.solved = int( 0 ) self.start_x = int( 0 ) self.start_y = int( 0 ) self.finish_x = int( 0 ) self.finish_y = int( 0 ) self.solution_x = None self.solution_y = None self.cells = None # Drawing information self.scale = float( 25.0 ) self.last_point = None self.path = '' def effect( self ): # These dimensions are chosen so as to maintain integral dimensions # with a ratio of width to height of TARGET_WIDTH to TARGET_HEIGHT. # Presently that's 3200 to 600 which leads to a ratio of 5 and 1/3. if self.options.mazeSize == 'SMALL': self.w = int( 32 ) self.h = int( 6 ) elif self.options.mazeSize == 'MEDIUM': self.w = int( 64 ) self.h = int( 12 ) elif self.options.mazeSize == 'LARGE': self.w = int( 96 ) self.h = int( 18 ) else: self.w = int( 128 ) self.h = int( 24 ) # The large mazes tend to hit the recursion limit limit = sys.getrecursionlimit() if limit < ( 4 + self.w * self.h ): sys.setrecursionlimit( 4 + self.w * self.h ) maze_size = self.w * self.h self.finish_x = int( self.w - 1 ) self.finish_y = int( self.h - 1 ) self.solution_x = array.array( 'i', range( 0, maze_size ) ) self.solution_y = array.array( 'i', range( 0, maze_size ) ) self.cells = array.array( 'H', range( 0, maze_size ) ) # Remove any old maze for node in self.document.xpath( '//svg:g[@inkscape:label="1 - Maze"]', namespaces=inkex.NSS ): parent = node.getparent() parent.remove( node ) # Remove any old solution for node in self.document.xpath( '//svg:g[@inkscape:label="2 - Solution"]', namespaces=inkex.NSS ): parent = node.getparent() parent.remove( node ) # Remove any empty, default "Layer 1" for node in self.document.xpath( '//svg:g[@id="layer1"]', namespaces=inkex.NSS ): if not node.getchildren(): parent = node.getparent() parent.remove( node ) # Start a new maze self.solved = 0 self.start_x = random.randint( 0, self.w - 1 ) self.finish_x = random.randint( 0, self.w - 1 ) # Initialize every cell with all four walls up for i in range( 0, maze_size ): self.cells[i] = Maze._NORTH | Maze._EAST | Maze._SOUTH | Maze._WEST # Now set our borders -- borders being walls which cannot be removed. # Since we are a maze on the surface of a cylinder we only have two # edges and hence only two borders. We consider our two edges to run # from WEST to EAST and to be at the NORTH and SOUTH. z = ( self.h - 1 ) * self.w for x in range( 0, self.w ): self.cells[x] |= Maze._NORTH << 4 self.cells[x + z] |= Maze._SOUTH << 4 # Build the maze self.handle_cell( 0, self.start_x, self.start_y ) # Now that the maze has been built, remove the appropriate walls # associated with the start and finish points of the maze # Note: we have to remove these after building the maze. If we # remove them first, then the lack of a border at the start (or # finish) cell will allow the handle_cell() routine to wander # outside of the maze. I.e., handle_cell() doesn't do boundary # checking on the cell cell coordinates it generates. Instead, it # relies upon the presence of borders to prevent it wandering # outside the confines of the maze. self.remove_border( self.start_x, self.start_y, Maze._NORTH ) self.remove_wall( self.start_x, self.start_y, Maze._NORTH ) self.remove_border( self.finish_x, self.finish_y, Maze._SOUTH ) self.remove_wall( self.finish_x, self.finish_y, Maze._SOUTH ) # Now draw the maze # The following scaling and translations scale the maze's # (width, height) to (TARGET_WIDTH, TARGET_HEIGHT), and translates # the maze so that it centered within a document of dimensions # (width, height) = (PLOT_WIDTH, PLOT_HEIGHT) # Note that each cell in the maze is drawn 2 x units wide by # 2 y units high. A width and height of 2 was chosen for # convenience and for allowing easy identification (as the integer 1) # of the centerline along which to draw solution paths. It is the # abstract units which are then mapped to the TARGET_WIDTH eggbot x # pixels by TARGET_HEIGHT eggbot y pixels rectangle. scale_x = float( TARGET_WIDTH ) / float( 2 * self.w ) scale_y = float( TARGET_HEIGHT ) / float( 2 * self.h ) translate_x = float( PLOT_WIDTH - TARGET_WIDTH ) / 2.0 translate_y = float( PLOT_HEIGHT - TARGET_HEIGHT ) / 2.0 # And the SVG transform is thus t = 'translate(%f,%f)' % ( translate_x, translate_y ) + \ ' scale(%f,%f)' % ( scale_x, scale_y ) # For scaling line thicknesses. We'll typically draw a line of # thickness 1 but will need to make the SVG path have a thickness # of 1 / scale so that after our transforms are applied, the # resulting thickness is the 1 we wanted in the first place. if scale_x > scale_y: self.scale = scale_x else: self.scale = scale_y self.last_point = None self.path = '' if not self.hpp: # To draw the walls, we start at the left-most column of cells, draw down drawing # the WEST and NORTH walls and then draw up drawing the EAST and SOUTH walls. # By drawing in this back and forth fashion, we minimize the effect of slippage. for x in range( 0, self.w, 2 ): self.draw_vertical( x ) else: # The drawing style of the "high plotting precision" / "faster plotting" mode # is such that it minimizes the number of pen up / pen down operations # but at the expense of requiring higher drawing precision. It's style # of drawing works best when there is very minimal slippage of the egg # Draw the horizontal walls self.draw_horizontal_hpp( 0, Maze._NORTH ) for y in range( 0, self.h - 1 ): self.draw_horizontal_hpp( y, Maze._SOUTH ) self.draw_horizontal_hpp( self.h - 1, Maze._SOUTH ) # Draw the vertical walls # Since this is a maze on the surface of a cylinder, we don't need # to draw the vertical walls at the outer edges (x = 0 & x = w - 1) for x in range( 0, self.w ): self.draw_vertical_hpp( x, Maze._EAST ) # Maze in layer "1 - Maze" attribs = { inkex.addNS( 'label', 'inkscape' ) : '1 - Maze', inkex.addNS( 'groupmode', 'inkscape' ) : 'layer', 'transform' : t } maze_layer = inkex.etree.SubElement( self.document.getroot(), 'g', attribs ) draw_SVG_path( self.path, "#000000", float( 1 / self.scale ), maze_layer ) # Now draw the solution in red in layer "2 - Solution" attribs = { inkex.addNS( 'label', 'inkscape' ) : '2 - Solution', inkex.addNS( 'groupmode', 'inkscape' ) : 'layer', 'transform' : t } maze_layer = inkex.etree.SubElement( self.document.getroot(), 'g', attribs ) # Mark the starting cell draw_SVG_rect( 0.25 + 2 * self.start_x, 0.25 + 2 * self.start_y, 1.5, 1.5, "#ff0000", 0, "#ff0000", maze_layer ) # And now generate the solution path itself # To minimize the number of plotted paths (and hence pen up / pen # down operations), we generate as few SVG paths as possible. # However, for aesthetic reasons we stop the path and start a new # one when it runs off the edge of the document. We could keep on # drawing as the eggbot will handle that just fine. However, it # doesn't look as good in Inkscape. So, we end the path and start # a new one which is wrapped to the other edge of the document. pts = [] end_path = False i = 0 while i < self.solved: x1 = self.solution_x[i] y1 = self.solution_y[i] i += 1 x2 = self.solution_x[i] y2 = self.solution_y[i] if math.fabs( x1 - x2 ) > 1: # We wrapped horizontally... if x1 > x2: x2 = x1 + 1 else: x2 = x1 - 1 end_path = True if i == 1: pts.extend( [ 'M', 2 * x1 + 1, 2 * y1 + 1 ] ) pts.extend( [ 'L', 2 * x2 + 1, 2 * y2 + 1 ] ) if not end_path: continue x2 = self.solution_x[i] y2 = self.solution_y[i] pts.extend( [ 'M', 2 * x2 + 1, 2 * y2 + 1 ] ) end_path = False # Put the solution path into the drawing draw_SVG_path( pts, '#ff0000', float( 8 / self.scale ), maze_layer ) # Now mark the ending cell draw_SVG_rect( 0.25 + 2*self.finish_x, 0.25 + 2 * self.finish_y, 1.5, 1.5, "#ff0000", 0, "#ff0000", maze_layer ) # Restore the recursion limit sys.setrecursionlimit( limit ) # Set some document properties node = self.document.getroot() node.set( 'width', '3200' ) node.set( 'height', '800' ) # The following end up being ignored by Inkscape.... node = self.getNamedView() node.set( 'showborder', 'false' ) node.set( inkex.addNS( 'cx', u'inkscape' ), '1600' ) node.set( inkex.addNS( 'cy', u'inkscape' ), '500' ) node.set( inkex.addNS( 'showpageshadow', u'inkscape' ), 'false' ) # Mark the cell at (x, y) as "visited" def visit( self, x, y ): self.cells[y * self.w + x] |= Maze._VISITED # Return a non-zero value if the cell at (x, y) has been visited def is_visited( self, x, y ): if self.cells[y * self.w + x] & Maze._VISITED: return -1 else: return 0 # Return a non-zero value if the cell at (x, y) has a wall # in the direction d def is_wall( self, x, y, d ): if self.cells[y * self.w + x] & d: return -1 else: return 0 # Remove the wall in the direction d from the cell at (x, y) def remove_wall( self, x, y, d ): self.cells[y * self.w + x] &= ~d # Return a non-zero value if the cell at (x, y) has a border wall # in the direction d def is_border( self, x, y, d ): if self.cells[y * self.w + x] & ( d << 4 ): return -1 else: return 0 # Remove the border in the direction d from the cell at (x, y) def remove_border( self, x, y, d ): self.cells[y * self.w + x] &= ~( d << 4 ) # This is the DFS algorithm which builds the maze. We start at depth 0 # at the starting cell (self.start_x, self.start_y). We then walk to a # randomly selected neighboring cell which has not yet been visited (i.e., # previously walked into). Each step of the walk is a recursive descent # in depth. The solution to the maze comes about when we walk into the # finish cell at (self.finish_x, self.finish_y). # # Each recursive descent finishes when the currently visited cell has no # unvisited neighboring cells. # # Since we don't revisit previously visited cells, each cell is visited # no more than once. As it turns out, each cell is visited, but that's a # little harder to show. Net, net, each cell is visited exactly once. def handle_cell( self, depth, x, y ): # Mark the current cell as visited self.visit( x, y ) # Save this cell's location in our solution trail / backtrace if not self.solved: self.solution_x[depth] = x self.solution_y[depth] = y if ( x == self.finish_x ) and ( y == self.finish_y ): # Maze has been solved self.solved = depth # Shuffle the four compass directions: this is the primary source # of "randomness" in the generated maze. We need to visit each # neighboring cell which has not yet been visited. If we always # did that in the same order, then our mazes would look very regular. # So, we shuffle the list of directions we try in order to find an # unvisited neighbor. # HINT: TRY COMMENTING OUT THE shuffle() BELOW AND SEE FOR YOURSELF directions = [Maze._NORTH, Maze._SOUTH, Maze._EAST, Maze._WEST] random.shuffle( directions ) # Now from the cell at (x, y), look to each of the four # directions for unvisited neighboring cells for i in range( 0, 4 ): # If there is a border in direction[i], then don't try # looking for a neighboring cell in that direction. We # Use this check and borders to prevent generating invalid # cell coordinates. if self.is_border( x, y, directions[i] ): continue # Determine the cell coordinates of a neighboring cell # NOTE: we trust the use of maze borders to prevent us # from generating invalid cell coordinates if directions[i] == Maze._NORTH: nx = x ny = y - 1 opposite_direction = Maze._SOUTH elif directions[i] == Maze._SOUTH: nx = x ny = y + 1 opposite_direction = Maze._NORTH elif directions[i] == Maze._EAST: nx = x + 1 ny = y opposite_direction = Maze._WEST else: nx = x - 1 ny = y opposite_direction = Maze._EAST # Wrap in the horizontal dimension if nx < 0: nx += self.w elif nx >= self.w: nx -= self.w # See if this neighboring cell has been visited if self.is_visited( nx, ny ): # Neighbor has been visited already continue # The neighboring cell has not been visited: remove the wall in # the current cell leading to the neighbor. And, from the # neighbor remove its wall leading to the current cell. self.remove_wall( x, y, directions[i] ) self.remove_wall( nx, ny, opposite_direction ) # Now recur by "moving" to this unvisited neighboring cell self.handle_cell( depth + 1, nx, ny ) def draw_line( self, x1, y1, x2, y2 ): if self.last_point is not None: if ( self.last_point[0] == x1 ) and ( self.last_point[1] == y1 ): self.path += ' L %d,%d' % ( x2, y2 ) self.last_point = [ x2, y2 ] elif ( self.last_point[0] == x2 ) and ( self.last_point[1] == y2 ): self.path += ' L %d,%d L %d,%d' % ( x1, y1, x2, y2 ) # self.last_point unchanged else: self.path += ' M %d,%d L %d,%d' % ( x1, y1, x2, y2 ) self.last_point = [ x2, y2 ] else: self.path = 'M %d,%d L %d,%d' % ( x1, y1, x2, y2 ) self.last_point = [ x2, y2 ] def draw_wall( self, x, y, d, dir ): if dir > 0: if d == Maze._NORTH: self.draw_line( 2*(x+1), 2*y, 2*x, 2*y ) elif d == Maze._WEST: self.draw_line( 2*x, 2*y, 2*x, 2*(y+1) ) elif d == Maze._SOUTH: self.draw_line( 2*(x+1), 2*(y+1), 2*x, 2*(y+1) ) else: # Mase._EAST self.draw_line( 2*(x+1), 2*y, 2*(x+1), 2*(y+1) ) else: if d == Maze._NORTH: self.draw_line( 2*x, 2*y, 2*(x+1), 2*y ) elif d == Maze._WEST: self.draw_line( 2*x, 2*(y+1), 2*x, 2*y ) elif d == Maze._SOUTH: self.draw_line( 2*x, 2*(y+1), 2*(x+1), 2*(y+1) ) else: # Maze._EAST self.draw_line( 2*(x+1), 2*(y+1), 2*(x+1), 2*y ) # Draw the vertical walls of the maze along the column of cells at # horizonal positions def draw_vertical( self, x ): # Drawing moving downwards from north to south if self.is_wall( x, 0, Maze._NORTH ): self.draw_wall( x, 0, Maze._NORTH, +1 ) for y in range( 0, self.h ): if self.is_wall( x, y, Maze._WEST ): self.draw_wall( x, y, Maze._WEST, +1 ) if self.is_wall( x, y, Maze._SOUTH ): self.draw_wall( x, y, Maze._SOUTH, +1 ) # Now, return drawing upwards moving from south to north x += 1 if x >= self.w: return for y in range( self.h - 1, -1, -1 ): if self.is_wall( x, y, Maze._SOUTH ): self.draw_wall( x, y, Maze._SOUTH, -1 ) if self.is_wall( x, y, Maze._WEST ): self.draw_wall( x, y, Maze._WEST, -1 ) if self.is_wall( x, 0, Maze._NORTH ): self.draw_wall( x, 0, Maze._NORTH, -1 ) # Draw the horizontal walls of the maze along the row of # cells at "height" y: "high plotting precision" version def draw_horizontal_hpp(self, y, wall ): # Cater to Python 2.4 and earlier # dy = 0 if wall == Maze._NORTH else 1 if wall == Maze._NORTH: dy = 0 else: dy = 1 tracing = False for x in range( 0, self.w ): if self.is_wall( x, y, wall ): if not tracing: # Starting a new segment segment = x tracing = True else: if tracing: # Reached the end of a segment self.draw_line( 2 * segment, 2 * (y + dy), 2 * x, 2 * (y + dy) ) tracing = False if tracing: # Draw the last wall segment self.draw_line( 2 * segment, 2 * (y + dy), 2 * self.w, 2 * (y + dy) ) # Draw the vertical walls of the maze along the column of cells at # horizonal position x: "high plotting precision" version def draw_vertical_hpp(self, x, wall ): # Cater to Python 2.4 and earlier # dx = 0 if wall == Maze._WEST else 1 if wall == Maze._WEST: dx = 0 else: dx = 1 # We alternate the direction in which we draw each vertical wall. # First, from North to South and then from South to North. This # reduces pen travel on the Eggbot if x % 2 == 0: # North-South y_start, y_finis, dy, offset = 0, self.h, 1, 0 else: # South-North y_start, y_finis, dy, offset = self.h - 1, -1, -1, 2 tracing = False for y in range( y_start, y_finis, dy ): assert 0 <= y and y < self.h, "y (%d) is out of range" % y if self.is_wall( x, y, wall ): if not tracing: # Starting a new segment segment = y tracing = True else: if tracing: # Hit the end of a segment self.draw_line( 2 * ( x + dx ), 2 * segment + offset, 2 * ( x + dx ), 2 * y + offset ) tracing = False if tracing: # complete the last wall segment self.draw_line( 2 * ( x + dx ), 2 * segment + offset, 2 * ( x + dx ), 2 * y_finis + offset ) if __name__ == '__main__': e = Maze() e.affect()