#!/usr/bin/pythonw # # Tiltmaze Generator, John W. Peterson, March 2005 # # In 2004, NEC Electronics ran the "Corneilus van Drebble's Mad Design Contest". # For the contest you're given a small microcontroller board with a 48x48 pixel # LCD display (an EV9835). The goal is to do "something cool" with it. # # For my contest entry, I hooked up a Memsic accelerometer (tilt sensor) and # used it to create a game. Rod Bogart pointed me to Andrea Gilbert's # Clickmazes.com, in particular the "Tilt mazes". These were a perfect match! # See # http://www.clickmazes.com/newtilt/ixtilt2d.htm # for an explanation and some java-based games you can play. # # To code the mazes on the microcontroller, I allocate one byte per maze cell, # and set bits if there are walls on the North, South, East or West sides. # additional bits record the goal and starting positions. I defined some # constants to describe the cell bits (i.e., you code "_N + _W" for a cell # with walls on the top and left). # # The constants helped, but after coding a couple mazes by hand, it was a real # pain. So I wrote this app to do code them up interactively. And heck, # since you've got the maze there, you might as well be able to play it too... # # Usage: # # Clicking on a cell wall toggles it on or off. Point to a cell and type 's' # to set the startpoint, and 'g' to set the goal. 'd' deletes it. Then the # arrow keys roll the ball. Right now only one start/goal is supported. # Clicking "Clear" clears the maze to the size specified. "Generate" spits # out the C code data structure for use by the microcontroller. "Load" # reads these in, either from a filename you specify or stdin if no file # is specified. "Reset" (or hitting space) resets the ball to the start. # # This code runs out of the box on Windows and Linux machines with python # installed. On the Mac (OS X 10.3), you need to do a couple installs # and some other voodoo. See # http://www.pythonmac.org/wiki/FAQ # Look at question 5.5, "How do I install Tkinter? How do I get IDLE to work?" # # If you want to experiment with the code, do NOT use Pythonwin.exe on Windows # Using Pythonwin and Tkinter invariably leads to a crash or hang as the two # get confused about event handling. Idle runs the client code in a separate # process and doesn't have this problem. # # Unlike the C code, this uses Tkinter "tags" to store the maze information. # Each wall stores it's coordinates, and 'H' for horizontal walls, 'V' for # vertical. This made more sense for editing. The use of the "Maze" class # is a bit redundant but helps clarify "global" (self.xxxx) vs. local variables. # from Tkinter import * import sys, string class Maze: def __init__(self, parent=None): self.margin = 6 self.pixelWidth = 500 self.thickLine = 4 self.ballMoving = False self.inputFile = None self.startPosition = None self.canvas = Canvas(parent, width=self.pixelWidth + self.margin * 2,height=self.pixelWidth + self.margin * 2) self.canvas.bind('', self.onClick) # For some odd reason, the canvas can't do key events... parent.bind_all('', self.onStart) parent.bind('', self.onDelete) parent.bind('', self.onGoal) parent.bind('', self.onArrow) parent.bind('', self.onArrow) parent.bind('', self.onArrow) parent.bind('', self.onArrow) parent.bind('', self.onReset) self.canvas.pack(side=TOP) self.msg = Label(text='Size:') self.msg.pack(side=LEFT) self.numEntry = Entry(width=3) self.numEntry.insert(0,'6') self.numEntry.pack(side=LEFT, padx=4) Button(text='Clear', command=self.onClearMaze).pack(side=LEFT) Button(text='Generate', command=self.onDumpMaze).pack(side=LEFT) Button(text='Load', command=self.onLoadMaze).pack(side=LEFT) Button(text='Reset', command=self.onReset).pack(side=LEFT, padx=4) Label(text="'s'->Start, 'g'->Goal, 'd'->Delete Click to set/clear wall").pack(side=BOTTOM) def erase(self): self.canvas.delete('all') self.startPosition = None def addshape( self, cellPos, kindtag, drawmethod, color ): "Add a shape to the cell" cellsize = self.pixelWidth / self.numcells # Note conversion from row,col to x,y cenx = cellPos[1]*cellsize + self.margin + cellsize/2 ceny = cellPos[0]*cellsize + self.margin + cellsize/2 rad = cellsize/2 - 6 if (kindtag == 'S'): self.startPosition = cellPos self.lastObj = drawmethod( cenx-rad, ceny-rad, cenx+rad, ceny+rad, width=3, fill=color ) self.addtags( kindtag, cellPos[0], cellPos[1] ) def addshapeEvent( self, event, kindtag, drawmethod, color ): "Add a shape to the cell event points at, drawn with drawmethod" self.onDelete( event ) # Clear the space first oldObj = self.canvas.find_withtag( kindtag ) if (len(oldObj) > 0): self.canvas.delete(oldObj) cellPos = self.findCellEvent( event ) self.addshape( cellPos, kindtag, drawmethod, color ) # The following tags are used: # "(i,j)" - is the cell coordinates # "H","V" - Horizontal / Vertical boundary # "S","G" - Start, Goal def addtags(self,kind,row,col): "Add tags for kind and position to last object drawn" self.canvas.addtag_withtag( "(%d,%d)" % (row,col), self.lastObj ) self.canvas.addtag_withtag( "%c" % kind, self.lastObj ) def getcellcontents(self,row,col): "Get the contents at the given cell indicies" return self.canvas.find_withtag( "(%d,%d)" % (row,col) ) def findCellEvent(self,where): "Given an event, return the correpsonding cell indicies" return self.findCellxy( where.x, where.y ) def findCellObj(self, obj): coords = self.canvas.coords(obj) return self.findCellxy( (coords[0]+coords[2])/2, (coords[1]+coords[3])/2 ) def findCellxy(self,centerx, centery): "Given pixel coordinates, return the correpsonding cell indicies" cellsize = self.pixelWidth / self.numcells centerx -= self.margin centery -= self.margin centerx /= cellsize; centery /= cellsize; # Note conversion of X,Y coords to row,col coords! return (int(centery), int(centerx)) def drawMaze(self): "Initialize and draw the maze" def addWall(row, col, dir): cellsize = self.pixelWidth / self.numcells x = self.margin + col * cellsize y = self.margin + row * cellsize linewidth = 1 # Preset walls around the edges if ((row == 0) or (row == self.numcells)) and (dir == 'H'): linewidth = self.thickLine if ((col == 0) or (col == self.numcells)) and (dir == 'V'): linewidth = self.thickLine if (dir == 'H'): self.lastObj = self.canvas.create_line( x, y, x + cellsize, y, width=linewidth) if (dir == 'V'): self.lastObj = self.canvas.create_line( x, y, x, y + cellsize, width=linewidth) self.addtags(dir, row, col) self.erase() # Be reasonable if (self.numcells < 3): self.numcells = 3 if (self.numcells > 30): self.numcells = 30 cellsize = self.pixelWidth / self.numcells for i in range(0, self.numcells): for j in range(0, self.numcells): addWall(i, j, 'H' ) addWall(i, j, 'V' ) if (j == self.numcells-1): addWall(i, j+1, 'V') if (i == self.numcells-1): addWall(i+1, j, 'H') # Grab the focus back so keyboard events work self.canvas.focus_set() def moveBall(self, rowdir, coldir): "Move the ball in response to an arrow key event" def testwall(row, col, dir): objs = self.getcellcontents(row, col) for id in objs: tags = self.canvas.gettags(id) if dir in self.canvas.gettags(id): width = self.canvas.itemcget( id, 'width' ) return self.canvas.itemcget( id, 'width' ) == '1.0' def teststep(row, col): if (rowdir < 0): return testwall(row, col, 'H') if (rowdir > 0): return testwall(row+1, col, 'H') if (coldir < 0): return testwall(row, col, 'V') if (coldir > 0): return testwall(row, col+1,'V') # We do a canvas.update() to animate the ball # but update() allows new events to come in, and # may call this recursively. This ignores any recursive calls if (self.ballMoving): return ball = self.canvas.find_withtag('S') if (len(ball) == 0): return ball = ball[0] ballStart = self.findCellObj( ball ) ballPos = list(ballStart) # List so it's mutable steps = 0 while teststep(ballPos[0], ballPos[1]): ballPos[0] += rowdir ballPos[1] += coldir steps += 1 if (steps > 0): self.ballMoving = True cellsize = self.pixelWidth / self.numcells for i in range(0, cellsize * steps): self.canvas.move( ball, coldir, rowdir ) self.canvas.update() # Update the tag self.canvas.dtag( ball, '(%d,%d)' % (ballStart[0],ballStart[1]) ) self.canvas.addtag_withtag( '(%d,%d)' % (ballPos[0], ballPos[1]), ball ) self.ballMoving = False goal = self.canvas.find_withtag('G') if (len(goal) == 0): return goalPos = self.findCellObj( goal[0] ) if (goalPos == tuple(ballPos)): self.canvas.tag_raise('S', goal[0] ) # Make sure ball's on top self.canvas.itemconfigure( ball, fill="green" ) else: self.canvas.itemconfigure( ball, fill="red" ) def onDumpMaze(self): "Dump the maze in a form usable by my embedded C code" def testcell(row,col,type,codename): objs = self.getcellcontents( row, col ) for id in objs: if (type in self.canvas.gettags( id )) and ((type not in ['H','V']) or (self.canvas.itemcget( id, 'width' ) != '1.0')): if (len(curcell[0]) == 0): curcell[0] += codename else: curcell[0] += '+' + codename output = [] for i in range(0,self.numcells): for j in range(0,self.numcells): # Disgusting hack - make curcell accessible to testcell by # putting it in a container. # See http://www.python.org/peps/pep-0227.html curcell = [''] testcell(i,j, 'H','_N') testcell(i,j, 'V','_W') testcell(i,j+1, 'V','_E') testcell(i+1,j, 'H','_S') testcell(i,j, 'S','STRT') testcell(i,j, 'G','GOAL') output.append(curcell[0]) # In the final output, it's nice to have the columns # line up, so we look through each column to find the maximum # width string and keep track of it. colwidths = [] for j in range(0,self.numcells): maxwidth = 0 for i in range(0,self.numcells): w = len(output[i*self.numcells+j]) if w > maxwidth: maxwidth = w colwidths.append(maxwidth + 1) # Add space for comma print "\t{ %d," % self.numcells for i in range(0,self.numcells): s = '\t\t' for j in range(0,self.numcells): cs = output[i*self.numcells+j] if (cs == ''): cs = '0' # No walls = zero if (j != self.numcells-1) or (i != self.numcells-1): cs += ',' else: cs += ' };' # This makes a "%-Ns" format string, to left justify the text s += ("%%%ds " % -colwidths[j]) % cs print s def onLoadMaze( self ): "If we can write the maze, might as well be able to read it." def readmazeline(): "Read stdin until we find a '}' (or EOF)" t = self.inputFile.readline() csrc.append(t) if (t == '') or (t.find('}') != -1): return False return True def addWall(id, tag, bit): if (tag in self.canvas.gettags(id)) and (mazedata[i*self.numcells+j] & bit): self.canvas.itemconfigure( id, width=self.thickLine ) if (self.inputFile == None): if (len(sys.argv) > 1): self.inputFile = file(sys.argv[1], 'r') else: self.inputFile = sys.stdin csrc = [] while (readmazeline()): pass if (len(csrc) == 0): # Nothing to read... return csrc = string.join(csrc) # Translate C style array syntax to Python xlat_table = '' for i in range(0,256): xlat_table += chr(i) xlat_table = xlat_table.replace('{', '[') xlat_table = xlat_table.replace('}', ']') psrc = csrc.translate( xlat_table, '\t\n;' ) # Define constants a la the maze C code [_N, _S, _E, _W, STRT, GOAL] = (1, 2, 4, 8, 16, 32) mazedata = eval(psrc) self.numcells = mazedata[0] mazedata = mazedata[1:] # delete size byte # Update the "Size:" box self.numEntry.delete( 0, len(self.numEntry.get())+1 ) self.numEntry.insert( 0, "%d" % self.numcells ) self.drawMaze() for i in range(0,self.numcells): for j in range(0, self.numcells): cellobjs = self.getcellcontents( i, j ) for id in cellobjs: addWall(id, 'H', _N) addWall(id, 'V', _W) if (mazedata[i*self.numcells+j] & STRT): self.addshape( (i, j), 'S', self.canvas.create_oval, 'red' ) if (mazedata[i*self.numcells+j] & GOAL): self.addshape( (i, j), 'G', self.canvas.create_rectangle, 'blue' ) if (j == self.numcells-1): cellobjs = self.getcellcontents( i, j+1 ) for id in cellobjs: addWall(id, 'V', _E) if (i == self.numcells-1): cellobjs = self.getcellcontents( i+1, j ) for id in cellobjs: addWall( id, 'H', _S) def onClearMaze( self ): self.numcells = int(self.numEntry.get()) self.drawMaze() def onClick( self, event ): "Handle mouse clicks to set/clear walls" # If we're clicked, also grab the focus self.canvas.focus_set() wallObj = self.canvas.find_closest( event.x, event.y, self.thickLine ) if (len(wallObj) == 1) and (self.canvas.type(wallObj[0]) == "line"): # Don't allow clicks on the outer walls tags = self.canvas.gettags( wallObj[0] ) for t in tags: if t[0] == '(': (row,col) = eval(t) if t in ['H','V']: dir = t if ((row == 0) or (row == self.numcells)) and dir == 'H': return if ((col == 0) or (col == self.numcells)) and dir == 'V': return curWidth = self.canvas.itemcget( wallObj[0], 'width' ) if (curWidth == '1.0'): self.canvas.itemconfigure( wallObj[0], width=self.thickLine ) else: self.canvas.itemconfigure( wallObj[0], width=1 ) def onStart( self, event ): "Create the starting position" self.addshapeEvent( event, 'S', self.canvas.create_oval, 'red' ) def onGoal( self, event) : "Create the goal position" self.addshapeEvent( event, 'G', self.canvas.create_rectangle, 'blue' ) # "event=None" because Button calls this w/o event argument def onReset( self, event=None ): if (self.startPosition): ball = self.canvas.find_withtag('S') self.canvas.delete( ball[0] ) self.addshape( self.startPosition, 'S', self.canvas.create_oval, 'red' ) def onArrow( self, event ): keyDict = { 'Up':(-1,0), 'Down':(1,0), 'Left':(0,-1), 'Right':(0,1) } (r, c) = keyDict[event.keysym] self.moveBall( r, c ) def onDelete( self, event ): "Delete any objects in the cell (start/goal)" cellPos = self.findCellEvent( event ) objs = self.getcellcontents( cellPos[0], cellPos[1] ) for id in objs: if self.canvas.type( id ) != "line": self.canvas.delete( id ) root = Tk(); m = Maze(root) m.onClearMaze(); root.mainloop()