from tkinter import * from random import random from random import randrange import time # Good programming practice is to make these *constants* rather than "magic numbers" thrown into the code. # They could be within one of the classes, although it is valid to put them here, # particularly given that the intention is that they are read only. canvas_height = 600 canvas_width = 800 world_top = 20 # 20 pixels from the top of the canvas world_ground = canvas_height - 20 # 20 pixels from bottom of the canvas world_left = 20 # 20 pixels from the left of the canvas world_right = canvas_width #------------------------------------------------------- # This class represents the bouncing ball class class BouncingBall: # Constructor. It is passed the initial position and colour of the ball # The constructor determines the initial velocity (a random number) def __init__(self, x, y, color): self.x = x self.y = y self.vx = random()+ .1 self.vy = random() self.radius = (random() *10) + 4 self.color = color #Get the x position of the ball def getX(self): return self.x #I think this is the main challenge in terms of program logic. #My way of thinking about the problem suggests that the checking # whether it is over the x axis should be done in the world # Whereas the check for if it is under the ground should be done in this class def move(self): #Set the new x and y self.x += self.vx self.y += self.vy #Is y now below the ground? if self.y + self.radius >= world_ground: self.y = world_ground - self.radius#Sit it on ground level self.vy = self.vy * -1 #Reverse the y velocity #Is y above the top of the world? if self.y - self.radius <= world_top: self.y = world_top + self.radius #Make it so it is touching the top self.vy = self.vy * -1 #Reverse the y velocity #Draw the ball in the given canvas def draw(self, canvas): #The math here is potentially problematic. However, COMP102 students # at Vic do these calculations in the second lab (flag drawing) left = self.x - self.radius right = self.x + self.radius top = self.y - self.radius bot = self.y + self.radius canvas.create_oval(left, top, right, bot, fill=self.color) #------------------------------------------------------- # This class holds the animation and the GUI. # It is possible to seperate them out, however it isn't as # obvious how you would go about this as it is for the flower program. class BouncingBallAnimation: default_speed = 10 possible_colors = ["red", "green", "blue", "yellow"] # This approach gives students an additional opportunity to # demonstrate their understanding of lists, essential for 3.46. # An alternative approach is to generate random colours using RGB, def __init__(self): self.current_speed = 50 window = Tk() Label(window, text="How many balls?").pack() self.number_of_balls_field = Entry(window) self.number_of_balls_field.pack() Button(window, text="Reset", command=self.play_animation).pack() # A button event is one of the at least 2 types of event driven # input that the students need to have in their code Label(window, text="How fast do you want the animation to be?").pack() self.speed_slider = Scale(window, from_=1, to=100, orient=HORIZONTAL, command=self.change_speed, showvalue=self.current_speed) #Horizontal looks nicer #Note: The reason that from_ has the underscore is because the # word "from" is a keyword in the python language # (have a look at the input lines at the very top of the program :-)) self.speed_slider.pack() # The scale is a slider, which will generate events. # Allows a second event driven input, as required by the standard. self.canvas = Canvas(window, height=canvas_height, width=canvas_width, bg="white") self.canvas.pack() self.__draw_world_outline() window.mainloop() #We don't actually need to use the event. Instead, we can just query #But we still use an event handler, because it is the event that lets #us know that the value has actually changed. def change_speed(self, event): self.current_speed = (100 - self.speed_slider.get()) #While this is only a one line method, it is still a good idea # to separate it out, to ensure it is done the same way by each # piece of code that does it. Also, this would allow us to make # a nicer looking outline more easily. e.g. brick pattern. def __draw_world_outline(self): self.canvas.create_line(world_right, world_top, world_left, world_top, world_left, world_ground, world_right, world_ground) #Makes new random bounching balls objects and puts them into the list def __list_of_balls(self, number_of_balls): balls = [] #Reset the bouncing balls list for i in range(number_of_balls): height = world_top+(world_ground-world_top)*random() random_colour_number = randrange(0, len(BouncingBallAnimation.possible_colors)) color = BouncingBallAnimation.possible_colors[random_colour_number] ball = BouncingBall(world_left, height, color) balls.append(ball) return balls # The main animation def play_animation(self): #Read how many balls are needed count = int(self.number_of_balls_field.get()) self.balls = self.__list_of_balls(count) #It is important to make the balls list a field of the class, # not a local variable while self.balls: #While there are still balls in the list (balls are removed #when they go over the edge of the screen) time.sleep(self.current_speed/10000) remaining = [] for ball in self.balls: ball.move() if ball.getX() < world_right: remaining.append(ball) self.balls = remaining self.redraw_world() def redraw_world(self): self.canvas.delete(ALL) self.__draw_world_outline() for ball in self.balls: ball.draw(self.canvas) self.canvas.update() #This is necessary to start the program BouncingBallAnimation()