[SPOILER] Solution to Perl 'Expert' Quiz of the Week #26

Jimmy Selgen <spamtrap1-x/claU4MwXpaa/[email protected]> Mon, 25 Oct 2004 11:02:20 +0200
Newsgroups gmane.comp.lang.perl.qotw.discuss
Message-ID <[email protected]>
Hi

I usually never go for the expert quiz because it's too time consuming, but this one sounded like fun (and didn't take that long to make :D )

I'm no physics major, so i'm still a bit confused as to the whole kinetic -> velocity calculation, but at least the cart gains speed when falling, and looses speed when climbing :)

I used python for the implementation. 

/Jimmy
--------------------------------------
#!/usr/bin/env python
#############################################
# File name  	: rollercoaster.py
# Date        	: Fri Oct 22 04:41:40 2004
# Author      	: Jimmy Selgen Nielsen
# Email        	: spamtrap1-x/claU4MwXpaa/[email protected]
# Description	:
# Changelog 	:
#	Fri Oct 22 04:41:40 2004 	: Initial version
#############################################

from Tkinter import *
import tkMessageBox,tkFileDialog,sys,os,math,time,tkSimpleDialog

class rollercoaster:
    '''
    Perl QOTW expert quiz #26 solution
    Opens file specified in argv[1], or file opened from menu.
    '''
    def __init__(self,args):
        self.rc_tag = None
        self.height = 400
        self.width = 600
        self.gravity = 1
        self.mass = 1
        self.time_mult = 100
        self.debug = 0
        self.wnd = None

        self.init_display()
        if(len(args)):
            self.read_rc(args[0])
        self.root.mainloop()


    def init_display(self):
        '''
        Initialize main window
        '''
        self.root = Tk()
        self.speedvar = StringVar()
        self.main_frame = Frame(self.root)
        self.main_frame.pack(fill=BOTH,expand=1)
        self.run_button = Button(self.main_frame,text="Run",command=self.run)
        self.canvas = Canvas(self.main_frame,width=self.width,height=self.height)
        speed_label = Label(self.main_frame,text="Velocity")
        self.speedometer = Entry(self.main_frame,textvariable=self.speedvar)
        self.run_button.pack()
        self.canvas.pack()
        speed_label.pack()
        self.speedometer.pack()
        self.mainmenu = Menu(self.root)
        self.root.config(menu=self.mainmenu)
        self.filemenu = Menu(self.mainmenu)
        self.filemenu.add_command(label="Open",command=self.open_rc_handler)
        self.filemenu.add_command(label="Run",command=self.run)
        self.filemenu.add_command(label="Setup",command=self.config_dialog)
        self.filemenu.add_command(label="Exit",command=self.root.destroy)
        self.mainmenu.add_cascade(label="File",menu=self.filemenu)

    def ok_pressed(self):
        '''
        Config dialog callback for OK
        '''
        self.gravity = float(self.grav_str.get())
        self.mass = float(self.mass_str.get())
        self.time_mult = float(self.time_mult_str.get())
        self.debug = self.dbg_var.get()
        print "new settings, gravity = %02f, mass = %02f, time_mult = %02f" % (self.gravity,self.mass,self.time_mult)
        self.wnd.destroy()
        self.wnd = None

    def cancel_pressed(self):
        '''
        Config dialog callback for cancel
        '''
        self.wnd.destroy()
        self.wnd = None

    def config_dialog(self):
        '''
        Configuration dialog for setting mass, gravity, time multiplier and turning debugging on/off
        '''
        if(self.wnd==None):
            self.wnd = Toplevel()
            self.frm = Frame(self.wnd)
            self.grav_str = StringVar()
            self.mass_str = StringVar()
            self.time_mult_str = StringVar()
            self.dbg_var = IntVar()
            self.grav_str.set(self.gravity)
            self.mass_str.set(self.mass)
            self.time_mult_str.set(self.time_mult)
            self.dbg_var.set(self.debug)
            grav_label = Label(self.frm,text="Gravity")
            self.grav_entry = Entry(self.frm,textvariable=self.grav_str)
            mass_label = Label(self.frm,text="Mass")
            self.mass_entry = Entry(self.frm,textvariable=self.mass_str)
            delay_label = Label(self.frm,text="Delay multiplier (dist/v * mult)")
            self.time_mult_entry = Entry(self.frm,textvariable=self.time_mult_str)
            debug_btn = Checkbutton(self.frm,text="Debug",variable=self.dbg_var)
            ok_btn = Button(self.frm,text="OK",command=self.ok_pressed)
            cancel_btn = Button(self.frm,text="Cancel",command=self.cancel_pressed)
            grav_label.pack()
            self.grav_entry.pack()
            mass_label.pack()
            self.mass_entry.pack()
            delay_label.pack()
            self.time_mult_entry.pack()
            debug_btn.pack()
            ok_btn.pack()
            cancel_btn.pack()
            self.frm.pack()

    def draw_cart(self,coord):
        '''
        Draws the "cart" from the given coordinate set
        '''
        #print "draw_cart, coord = ",coord
        self.bbox = [coord[0]-2,self.height - (coord[1]-2),coord[0]+2,self.height - (coord[1]+2)]
        if(self.cart != None):
           self.canvas.coords(self.cart,*self.bbox)
        else:
            self.cart = self.canvas.create_oval(self.bbox,fill="red")

    def draw_rc(self):
        '''
        Draws the rollercoaster from the current coordinate set
        '''
        self.coords = []
        self.cart = None
        self.canvas.delete('all')
        for p in range(len(self.rc)) :
            if(p < len(self.rc)-1):
                point = self.rc[p].rstrip('\n').split(' ')
                point[0] = float(point[0])
                point[1] = float(point[1])
                self.coords.append(point)

        if(self.rc_tag != None):
            self.canvas.delete(self.rc_tag)
        self.rc_tag = []
        for p in range(len(self.coords)):
            if p < len(self.coords)-1:
                co = [self.coords[p][0],self.height - self.coords[p][1],self.coords[p+1][0],self.height - self.coords[p+1][1]]
                self.rc_tag.append(self.canvas.create_line(co))
        self.canvas.pack(fill=BOTH)



    def calc_vel(self, curr_state, next_point):
        '''
        Calculates new speed and direction for cart for a given point

        curr_state : current state of cart
        next_point : the current point of the cart
        returns    : new state with updated cart speed,direction and time to travel distance
        '''
        new_state = dict()
        dist = 0
        nv = 0
        co = self.coords[curr_state['coord']]
        nco = self.coords[next_point]
        new_state = curr_state

        #calculate vertical and horizontal "grid" distance travelled
        dist = co[0] - nco[0]
        nh = co[1] - nco[1]
        dist_travelled = math.sqrt(dist**2 + nh**2)
        if(self.debug):
            print "dist :",dist," height :",nh," dist_travelled :",dist_travelled
        pa = (curr_state['m'] * curr_state['g'] * ((nh/2)/10))

        if(pa > 0):
            nv = math.sqrt(pa)
        else:
            nv = -(math.sqrt(-pa))
        if(self.debug):
            print "nv = ",nv

        new_state['v'] += nv

        #Change cart direction if velocity is less than zero
        if(new_state['v']<0):
            new_state['direction'] = 0 - new_state['direction']
            #change negative velocity to positive velocity going the other way :)
            new_state['v'] = -new_state['v']

        if(self.debug):
            print "new v :",new_state['v']
        new_state['coord'] = next_point

        #Set time to travel distance at new velocity
        new_state['t'] = dist_travelled / (new_state['v']+1)
        if(self.debug):
            print "new t = ",new_state['t']

        return(new_state)


    def read_rc(self,fname):
        '''
        Read a .rc file from disk
        '''
        try:
            f = open(fname)
            self.rc = f.readlines()
            if(self.debug):
                print "read %d lines from %s" % (len(self.rc),fname)
        except:
            tkMessageBox.showerror('Error opening file','Error opening file "'+fname+'"')
            self.rc = None
        self.draw_rc()

    def open_rc_handler(self):
        '''
        Menu callback for "File->Open"
        '''
        fname = tkFileDialog.askopenfilename(filetypes=[(".rc", ".rc")], title="Select the file")
        if(fname != None):
            self.read_rc(fname)

    def tick(self):
        '''
        "Mainloop" of the simulation. Callback to the windowmanager for every iteration of the simulation
        '''
        self.curr_state = self.calc_vel(self.curr_state,self.curr_state['coord']+self.curr_state['direction'])
        self.draw_cart(self.coords[self.curr_state['coord']])
        self.speedvar.set(round(self.curr_state['v'],2))
        nco = self.curr_state['coord'] + self.curr_state['direction']
        if(self.curr_state['v'] != 0 and nco >= 0 and nco < len(self.coords)):
            self.root.after(int(self.curr_state['t']*self.time_mult),self.tick)
        else:
            return(0)

    def run(self):
        '''
        Initialize and start mainloop of simulation
        '''
        self.curr_state = {
                'coord' : 0,      #Index into rollercoaster coordinates
                'v':0,              #Cart Velocity in the current direction
                'm':self.mass,   #Mass
                'g':self.gravity, #Gravity
                't':0,               #Time to travel distance
                'direction':1      #Direction of cart, 1 = left to right, -1 = right to left
        }
        self.calc_vel(self.curr_state,1)
        self.root.after(int(self.curr_state['t']*self.time_mult),self.tick)


if __name__ == '__main__' :
    rc = rollercoaster(sys.argv[1:])