[Tutor] OIAW: feedback on program requested

Gregor Lingl glingl at aon.at
Mon Feb 16 13:43:53 EST 2004


Hi pythonistas!

Once in a while I like to get some feedback on a
program I wrote. This time it's an animated tower of
hanoi game with a Tkinter - GUI.
It uses some of the newer features of Python and will run
only under Python 2.3 except you import generators
from __future__.

Although the program has about 270 lines, I hope the
code is well structured so you can get a good overview
quickly.

Generally *feedback of any sort* is welcome, from comments
on program design to corrections of malformed docstrings
and comments (my English is a bit error prone). And also
it you like it or not ;-)

Specifically there are a few points I'd like to read your
opinion about ( - is this well done, useful, overkill,
dengerous etc.):

(1) Making  Tower a subclass  of  the  builtin type list

(2) Using a generator for the wellknown tower-of-hanoi-algorithm
    in order to achieve execution of single steps, as well as
    interrupting and resuming the game, easily. (So I use a
    generator only for its side-effect.)

(3) I've tried to separate the Hanoi-Engine, which operates on a
    Tkinter-Canvas, completely from the (rest of the) user interface
    to make it reusable (if there is anybody who wants to reuse
    a Hanoi-game-canvas ;-) )

(4) A problem, for which I really don't know what's the canonical
    way to handle it, is peaceful termination the program while
    the animation is going on.  That means how to  terminate the
    program by CLOSING the WINDOW without issueing error-messages?
    I did it  by catching  TclErrors  at two  specific  points:
    (a) in the run() method of HanoiEngine, which finally moves
        the discs
    (b) in the setState() method of Hanoi (the GUI-building class),
        which configers the widgets of the GUI.
    Without these two try: - except TclError: statements, closing
    the window while the game is running results in a TclError.

    I did not succeed in using the protocol-method as it inhibits
    terminating the priogram while the animatino is going on.

Many thanks for your  feedback in advance

Gregor

-------------- next part --------------
## Animated Tower-Of-Hanoi game with Tkinter GUI
## author: Gregor Lingl, Vienna, Austria
## email: glingl at aon.at
## date: 16. 2. 2004

from Tkinter import * 

class Disc:
    """Movable Rectangle on a Tkinter Canvas"""
    def __init__(self,cv,pos,length,height,colour):
        """creates disc on given Canvas cv at given pos-ition"""
        x0, y0 = pos
        x1, x2 = x0-length/2.0, x0+length/2.0
        y1, y2 = y0-height, y0
        self.cv = cv
        self.item = cv.create_rectangle(x1,y1,x2,y2,
                                        fill = "#%02x%02x%02x" % colour)       
    def move_to(self, x, y, speed):
        """moves bottom center of disc to position (x,y).
           speed is intended to assume values from 1 to 10"""
        x1,y1,x2,y2 = self.cv.coords(self.item)
        x0, y0 = (x1 + x2)/2, y2
        dx, dy = x-x0, y-y0
        d = (dx**2+dy**2)**0.5
        steps = int(d/(10*speed-5)) + 1
        dx, dy = dx/steps, dy/steps
        for i in range(steps):
            self.cv.move(self.item,dx,dy)
            self.cv.update()
            self.cv.after(20)

class Tower(list):
    """Hanoi tower designed as a subclass of built-in type list"""
    def __init__(self, x, y, h):
        """ creates an empty tower.
            (x,y) is floor-level position of tower,
            h is height of the discs. """
        self.x = x
        self.y = y
        self.h = h
    def top(self):
        return self.x, self.y - len(self)*self.h
    

class HanoiEngine:
    """Plays the Hanoi-game on a given canvas."""
    def __init__(self, canvas, nrOfDiscs, speed, moveCntDisplay=None):
        """Sets Canvas to play on as well as default values for
        number of discs and animation-speed.
        moveCntDisplay is a function with 1 parameter, which communicates
        the count of the actual move to the GUI containing the
        Hanoi-engine-canvas."""
        self.cv = canvas
        self.nrOfDiscs = nrOfDiscs
        self.speed = speed
        self.moveDisplay = moveCntDisplay
        self.running = False
        self.moveCnt = 0
        self.discs = []
        self.towerA = Tower( 80, 190, 15)
        self.towerB = Tower(220, 190, 15)
        self.towerC = Tower(360, 190, 15)
        self.reset()
        
    def hanoi(self, n, src, dest, temp):
        """The classical recursive Towers-Of-Hanoi algorithm."""
        if n > 0:
            for x in self.hanoi(n-1, src, temp, dest): yield None
            yield self.move(src, dest)
            for x in self.hanoi(n-1, temp, dest, src): yield None

    def move(self, src_tower, dest_tower):
        """moves uppermost disc of source tower to top of destination
        tower."""
        self.moveCnt += 1
        self.moveDisplay(self.moveCnt)
        disc = src_tower.pop()
        x1, y1 = src_tower.top()
        x2, y2 = dest_tower.top()
        disc.move_to(x1,20, self.speed)
        disc.move_to(x2,20, self.speed)
        disc.move_to(x2,y2, self.speed)
        dest_tower.append(disc)

    def reset(self):
        """Setup of (a new) game."""
        self.moveCnt = 0
        self.moveDisplay(self.moveCnt)
        while self.towerA: self.towerA.pop()
        while self.towerB: self.towerB.pop()
        while self.towerC: self.towerC.pop()
        for s in self.discs:
            self.cv.delete(s.item)
        ## Fancy colouring of discs: red ===> blue
        if self.nrOfDiscs > 1:
            colour_diff = 255 // (self.nrOfDiscs-1)
        else:
            colour_diff = 0
        for i in range(self.nrOfDiscs):  # setup towerA        
            length_diff = 100 // self.nrOfDiscs
            length = 120 - i * length_diff        
            s = Disc( self.cv, self.towerA.top(), length, 13,
                         (255-i*colour_diff, 0, i*colour_diff))
            self.discs.append(s)
            self.towerA.append(s)
        self.HG = self.hanoi(self.nrOfDiscs,
                             self.towerA, self.towerC, self.towerB)
        
    def run(self):
        """runs game ;-)
        returns True if game is over, else False"""
        self.running = True
        try:
            while self.running:
                result = self.step()
            return result  # True iff done
        except StopIteration:  # game over
            return True
        except TclError:   # for silently terminating in case of window closing 
            return False

    def step(self):
        """performs one single step of the game,
        returns True if finished, else False"""
        self.HG.next()
        return 2**self.nrOfDiscs-1 == self.moveCnt
        
    def stop(self):
        """ ;-) """
        self.running = False


class Hanoi:
    """GUI for animated towers-of-Hanoi-game with upto 10 discs:"""

    def displayMove(self, move):
        """method to be passed to the Hanoi-engine as a callback
        to report move-count"""
        self.moveCntLbl.configure(text = "move:\n%d" % move)
    
    def adjust_nr_of_discs(self, e):
        """callback function for nr-of-discs-scale-widget"""
        self.hEngine.nrOfDiscs = self.discs.get()
        self.reset()

    def adjust_speed(self, e):
        """callback function for speeds-scale-widget"""
        self.hEngine.speed = self.tempo.get()

    def setState(self, STATE):
        """most simple representation of a finite state machine"""
        self.state = STATE
        try:
            if STATE == "START":
                self.discs.configure(state=NORMAL)
                self.discs.configure(fg="black")
                self.discsLbl.configure(fg="black")
                self.resetBtn.configure(state=DISABLED)
                self.startBtn.configure(text="start", state=NORMAL)
                self.stepBtn.configure(state=NORMAL)
            elif STATE == "RUNNING":
                self.discs.configure(state=DISABLED)
                self.discs.configure(fg="gray70")
                self.discsLbl.configure(fg="gray70")
                self.resetBtn.configure(state=DISABLED)
                self.startBtn.configure(text="pause", state=NORMAL)
                self.stepBtn.configure(state=DISABLED)
            elif STATE == "PAUSE":
                self.discs.configure(state=NORMAL)
                self.discs.configure(fg="black")
                self.discsLbl.configure(fg="black")
                self.resetBtn.configure(state=NORMAL)
                self.startBtn.configure(text="resume", state=NORMAL)
                self.stepBtn.configure(state=NORMAL)
            elif STATE == "DONE":
                self.discs.configure(state=NORMAL)
                self.discs.configure(fg="black")
                self.discsLbl.configure(fg="black")
                self.resetBtn.configure(state=NORMAL)
                self.startBtn.configure(text="start", state=DISABLED)
                self.stepBtn.configure(state=DISABLED)
            elif STATE == "TIMEOUT":
                self.discs.configure(state=DISABLED)
                self.discs.configure(fg="gray70")
                self.discsLbl.configure(fg="gray70")
                self.resetBtn.configure(state=DISABLED)
                self.startBtn.configure(state=DISABLED)
                self.stepBtn.configure(state=DISABLED)
        except TclError:
            pass
           
    def reset(self):
        """restores START state for a new game"""
        self.hEngine.reset()
        self.setState("START")
        
    def start(self):
        """callback function for start button, which also serves as
        pause button. Makes hEngine running until done or interrupted"""
        if self.state in ["START", "PAUSE"]:
            self.setState("RUNNING")            
            if self.hEngine.run():
                self.setState("DONE")
            else:
                self.setState("PAUSE")
        elif self.state == "RUNNING":
            self.setState("TIMEOUT")
            self.hEngine.stop()

    def step(self):
        """callback function for step button.
        makes hEngine perform a single step"""
        self.setState("TIMEOUT")
        if self.hEngine.step():
            self.setState("DONE")
        else:
            self.setState("PAUSE")
                
    def __init__(self, nrOfDiscs, speed):
        """builds GUI, constructs Hanoi-engine and puts it into START state
        then launches mainloop()"""
        root = Tk()                            
        root.title("TOWERS OF HANOI")
        #root.protocol("WM_DELETE_WINDOW",root.quit) #counter productive here!?
        cv = Canvas(root,width=440,height=210, bg="gray90") 
        cv.pack()
        
        fnt = ("Arial", 12, "bold")

        attrFrame = Frame(root) #contains scales to adjust game's attributes
        self.discsLbl = Label(attrFrame, width=7, height=2, font=fnt,
                              text="discs:\n")
        self.discs = Scale(attrFrame, from_=1, to_=10, orient=HORIZONTAL,
                           font=fnt, length=75, showvalue=1, repeatinterval=10,
                           command=self.adjust_nr_of_discs)
        self.discs.set(nrOfDiscs)
        self.tempoLbl = Label(attrFrame, width=8,  height=2, font=fnt,
                              text = "   speed:\n")
        self.tempo = Scale(attrFrame, from_=1, to_=10, orient=HORIZONTAL,
                           font=fnt, length=100, showvalue=1,repeatinterval=10,
                           command = self.adjust_speed)
        self.tempo.set(speed)
        self.moveCntLbl= Label(attrFrame, width=5, height=2, font=fnt,
                               padx=20, text=" move:\n0", anchor=CENTER)                        
        for widget in ( self.discsLbl, self.discs, self.tempoLbl, self.tempo,
                                                             self.moveCntLbl ):
            widget.pack(side=LEFT)
        attrFrame.pack(side=TOP)

            
        ctrlFrame = Frame(root) # contains Buttons to control the game 
        self.resetBtn = Button(ctrlFrame, width=11, text="reset", font=fnt,
                               state = DISABLED, padx=15, command = self.reset)
        self.stepBtn  = Button(ctrlFrame, width=11, text="step", font=fnt,
                               state = NORMAL,  padx=15, command = self.step)
        self.startBtn = Button(ctrlFrame, width=11, text="start", font=fnt,
                               state = NORMAL,  padx=15, command = self.start)
        for widget in self.resetBtn, self.stepBtn, self.startBtn:
            widget.pack(side=LEFT)
        ctrlFrame.pack(side=TOP)

        # setup of the scene
        peg1 = cv.create_rectangle(  75,  40,  85, 190, fill='darkgreen')
        peg2 = cv.create_rectangle( 215,  40, 225, 190, fill='darkgreen')
        peg3 = cv.create_rectangle( 355,  40, 365, 190, fill='darkgreen')
        floor = cv.create_rectangle( 10, 191, 430, 200, fill='black')

        self.hEngine = HanoiEngine(cv, nrOfDiscs, speed, self.displayMove)
        self.state = "START"

        root.mainloop()

if __name__  == "__main__":
    Hanoi(4,5)


More information about the Tutor mailing list