I am a new coder and I am building a "Breakout" game in python and i got most of it to except getting the bricks to delete. *What is odd, is after the ball touches the bottom wall (out of bounds) and regenerates, the bricks are then able to be deleted by playing the game! Also, I need to stop the game after 3 chances. I'm stuck, with those 2 problems. Help please. Here is my code:
import tkinter
import time
# How big is the playing area?
CANVAS_WIDTH = 600 # Width of drawing canvas in pixels
CANVAS_HEIGHT = 800 # Height of drawing canvas in pixels
# Constants for the bricks
N_ROWS = 8 # How many rows of bricks are there?
N_COLS = 10 # How many columns of bricks are there?
SPACING = 5 # How much space is there between each brick?
BRICK_START_Y = 50 # The y coordinate of the top-most brick
BRICK_HEIGHT = 20 # How many pixels high is each brick
BRICK_WIDTH = (CANVAS_WIDTH - (N_COLS + 1) * SPACING) // N_COLS
# Constants for the ball and paddle
BALL_SIZE = 70
PADDLE_Y = CANVAS_HEIGHT - 40
PADDLE_WIDTH = 200
def main():
canvas = make_canvas(CANVAS_WIDTH, CANVAS_HEIGHT, 'Brick Breaker')
# Makes a ball
ball = canvas.create_oval(300, 300, 350, 350, fill="red", outline="red")
# Makes a paddle
paddle = canvas.create_rectangle(299, PADDLE_Y, PADDLE_WIDTH, CANVAS_HEIGHT - 20, fill="black")
# Change_X
dx = 6
# Change_Y
dy = 6
for row in range(N_ROWS):
# Draws columns of brick
for col in range(N_COLS):
draw_brick(canvas, row, col)
while True:
# Mouse location and respond to movement
mouse_x = canvas.winfo_pointerx()
# Move Paddle to X location
canvas.moveto(paddle, mouse_x, PADDLE_Y)
# Ball movement
canvas.move(ball, dx, dy)
# If ball hits left of right wall, change X location
if hit_left_wall(canvas, ball) or hit_right_wall(canvas, ball):
dx *= -1
# If ball hits top wall, then change Y location
elif hit_top_wall(canvas, ball):
dy *= -1
elif hit_brick(canvas, ball, paddle):
dy *= -1
if hit_bottom(canvas, ball):
canvas.delete(ball)
ball = make_ball(canvas)
# Recreates canvas
canvas.update()
# Pause time
time.sleep(1 / 50.)
canvas.mainloop()
# Finds coordinates of paddle
def hit_paddle(canvas, ball, paddle):
paddle_coords = canvas.coords(paddle)
x1 = paddle_coords[0]
y1 = paddle_coords[1]
x2 = paddle_coords[2]
y2 = paddle_coords[3]
# If any object begins to overlap with paddle, create a Hit
result = canvas.find_overlapping(x1, y1, x2, y2)
return len(result) > 1
def make_ball(canvas):
return canvas.create_oval(300, 300, 350, 350, fill="red", outline="red")
def hit_brick(canvas, ball, paddle):
ball_coord = canvas.coords(ball)
x_1 = ball_coord[0]
y_1 = ball_coord[1]
x_2 = ball_coord[2]
y_2 = ball_coord[3]
results = canvas.find_overlapping(x_1, y_1, x_2, y_2)
for object in results:
if object == paddle or object == ball:
return len(results) > 1
else:
canvas.delete(object)
def moveto(canvas, oval, x, y):
# Get current position
x0, y0, x1, y1 = canvas.coords(oval)
# Sets new position
canvas.move(oval, x - x0, y - y0)
def hit_bottom(canvas, ball):
return get_bottom_y(canvas, ball) >= CANVAS_HEIGHT
def hit_left_wall(canvas, ball):
return get_left_x(canvas, ball) <= 0
def hit_right_wall(canvas, ball):
return get_right_x(canvas, ball) >= CANVAS_WIDTH
def hit_top_wall(canvas, ball):
return get_top_y(canvas, ball) <= 0
def draw_brick(canvas, row, col):
x = col * (BRICK_WIDTH + SPACING)
y = row * (BRICK_HEIGHT + SPACING)
color = "blue"
canvas.create_rectangle(x, y, x + BRICK_WIDTH, y + BRICK_HEIGHT, fill=color, outline=color)
def get_bottom_y(canvas, ball):
return canvas.coords(ball)[3]
def get_top_y(canvas, ball):
"""
This friendly method returns the y coordinate of the top of an object.
Recall that canvas.coords(object) returns a list of the object
bounding box: [x_1, y_1, x_2, y_2]. The element at index 1 is the top-y
"""
return canvas.coords(ball)[1]
def get_left_x(canvas, ball):
"""
This friendly method returns the x coordinate of the left of an object.
Recall that canvas.coords(object) returns a list of the object
bounding box: [x_1, y_1, x_2, y_2]. The element at index 0 is the left-x
"""
return canvas.coords(ball)[0]
def get_right_x(canvas, ball):
"""
This friendly method returns the x coordinate of the right of an object.
Recall that canvas.coords(object) returns a list of the object
bounding box: [x_1, y_1, x_2, y_2]. The element at index 2 is the right-x
"""
return canvas.coords(ball)[2]
def make_canvas(width, height, title):
"""
Creates and returns a drawing canvas
of the given int size with a blue border,
ready for drawing.
"""
top = tkinter.Tk()
top.minsize(width=width, height=height)
top.title(title)
canvas = tkinter.Canvas(top, width=width + 1, height=height + 1)
canvas.pack()
return canvas
if __name__ == '__main__':
main()
The first problem is due to the if statement in the for loop inside hit_brick():
def hit_brick(canvas, ball, paddle):
ball_coord = canvas.coords(ball)
x_1 = ball_coord[0]
y_1 = ball_coord[1]
x_2 = ball_coord[2]
y_2 = ball_coord[3]
results = canvas.find_overlapping(x_1, y_1, x_2, y_2)
for object in results:
if object == paddle or object == ball: # <-- problem here
return len(results) > 1
else:
canvas.delete(object)
As the values of ball and paddle are 1 and 2 (as they are the first two canvas items created) and so results is something like (1, N) when the ball hit one of the bricks.
So the if statement returns true for the first checking and then the function exits by the return statement.
Now let the ball hits the bottom and it will be recreated with id greater than existing canvas items. The results will be something like (N, ball) when the ball hits one of the bricks.
This time the if statement will return false and the brick is deleted.
So hit_brick() should be modified as below:
def hit_brick(canvas, ball, paddle):
ball_coord = canvas.coords(ball)
results = canvas.find_overlapping(*ball_coord)
for object in results:
if object not in (paddle, ball):
canvas.delete(object)
return len(results) > 1
For the second problem, you need to declare a variable, for example lives = 3, before the while loop and decrease it by one if the ball hits the bottom.
The while loop should be terminated if lives == 0:
def main():
...
lives = 3
while lives > 0:
...
if hit_bottom(canvas.ball):
...
lives -= 1
Related
i have a problem with this code, i am a new person with programming and been using the book "how to think like a computer scientist 3rd edition" and he did not solve exercise 2 of chapter 17 this given: "he deliberately left a mistake in the code to animate Duke. If you click on one of the checkerboard squares to the right of Duke, he salutes anyway. Why? Find a one-line solution to the error ", I've tried many forms but I have not succeeded, I leave you all the code and the images that I have used
PS: images must have the name: ball.png and duke_spritesheet.png
import pygame
gravity = 0.025
my_clock = pygame.time.Clock()
class QueenSprite:
def __init__(self, img, target_posn):
self.image = img
self.target_posn = target_posn
(x, y) = target_posn
self.posn = (x, 0) # Start ball at top of its column
self.y_velocity = 0 # with zero initial velocity
def update(self):
self.y_velocity += gravity
(x, y) = self.posn
new_y_pos = y + self.y_velocity
(target_x, target_y) = self.target_posn # Unpack the position
dist_to_go = target_y - new_y_pos # How far to our floor?
if dist_to_go < 0: # Are we under floor?
self.y_velocity = -0.65 * self.y_velocity # Bounce
new_y_pos = target_y + dist_to_go # Move back above floor
self.posn = (x, new_y_pos) # Set our new position.
def draw(self, target_surface): # Same as before.
target_surface.blit(self.image, self.posn)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains point pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
self.y_velocity += -2 # Kick it up
class DukeSprite:
def __init__(self, img, target_posn):
self.image = img
self.posn = target_posn
self.anim_frame_count = 0
self.curr_patch_num = 0
def update(self):
if self.anim_frame_count > 0:
self.anim_frame_count = (self.anim_frame_count + 1 ) % 60
self.curr_patch_num = self.anim_frame_count // 6
def draw(self, target_surface):
patch_rect = (self.curr_patch_num * 50, 0,
50, self.image.get_width())
target_surface.blit(self.image, self.posn, patch_rect)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
if self.anim_frame_count == 0:
self.anim_frame_count = 5
def draw_board(the_board):
""" Draw a chess board with queens, as determined by the the_board. """
pygame.init()
colors = [(255,0,0), (0,0,0)] # Set up colors [red, black]
n = len(the_board) # This is an NxN chess board.
surface_sz = 480 # Proposed physical surface size.
sq_sz = surface_sz // n # sq_sz is length of a square.
surface_sz = n * sq_sz # Adjust to exactly fit n squares.
# Create the surface of (width, height), and its window.
surface = pygame.display.set_mode((surface_sz, surface_sz))
ball = pygame.image.load("ball.png")
# Use an extra offset to centre the ball in its square.
# If the square is too small, offset becomes negative,
# but it will still be centered :-)
ball_offset = (sq_sz-ball.get_width()) // 2
all_sprites = [] # Keep a list of all sprites in the game
# Create a sprite object for each queen, and populate our list.
for (col, row) in enumerate(the_board):
a_queen = QueenSprite(ball,
(col*sq_sz+ball_offset, row*sq_sz+ball_offset))
all_sprites.append(a_queen)
# Load the sprite sheet
duke_sprite_sheet = pygame.image.load("duke_spritesheet.png")
# Instantiate two duke instances, put them on the chessboard
duke1 = DukeSprite(duke_sprite_sheet,(sq_sz*2, 0))
duke2 = DukeSprite(duke_sprite_sheet,(sq_sz*5, sq_sz))
# Add them to the list of sprites which our game loop manages
all_sprites.append(duke1)
all_sprites.append(duke2)
while True:
# Look for an event from keyboard, mouse, etc.
ev = pygame.event.poll()
if ev.type == pygame.QUIT:
break;
if ev.type == pygame.KEYDOWN:
key = ev.dict["key"]
if key == 27: # On Escape key ...
break # leave the game loop.
if key == ord("r"):
colors[0] = (255, 0, 0) # Change to red + black.
elif key == ord("g"):
colors[0] = (0, 255, 0) # Change to green + black.
elif key == ord("b"):
colors[0] = (0, 0, 255) # Change to blue + black.
if ev.type == pygame.MOUSEBUTTONDOWN: # Mouse gone down?
posn_of_click = ev.dict["pos"] # Get the coordinates.
for sprite in all_sprites:
if sprite.contains_point(posn_of_click):
sprite.handle_click()
break
for sprite in all_sprites:
sprite.update()
# Draw a fresh background (a blank chess board)
for row in range(n): # Draw each row of the board.
c_indx = row % 2 # Alternate starting color
for col in range(n): # Run through cols drawing squares
the_square = (col*sq_sz, row*sq_sz, sq_sz, sq_sz)
surface.fill(colors[c_indx], the_square)
# Now flip the color index for the next square
c_indx = (c_indx + 1) % 2
# Ask every sprite to draw itself.
for sprite in all_sprites:
sprite.draw(surface)
my_clock.tick(60) # Waste time so that frame rate becomes 60 fps
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
draw_board([0, 5, 3, 1, 6, 4, 2]) # 7 x 7 to test window size
PS: I think the error is here but it did not succeed
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
The issue is caused by the face, that "duke_spritesheet.png" is a sprite sheet. When you define the rectangular region which is covered by the object, then you have to use the width of a single image, rather than the width of the entire sprite sheet:
my_width = self.image.get_width()
my_width = 50
Change this in the method contains_point of the class DukeSprite:
class DukeSprite:
# [...]
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = 50
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
I am trying to make a program that will increase the speed of the moving blue circle by 50% if the yellow square is clicked, and decrease it by 50% if the purple square is clicked. The purple square works fine but the yellow square will not register clicks at all. Interestingly, if the black square is used in place of the yellow square, it will function as necessary. Additionally, the blue circle will register clicks within it but will not close the window as intended. I am a novice in python and am beyond confused here. Any suggestions would be greatly appreciated.
As stated previously, functionality works as intended when the black square is used to carry out the functionality of the yellow square. I have used print statements to test if clicks are being registered in different shapes. Clicks register in the blue circle, black square and purple square, but not in the yellow square.
from graphics import*
from time import sleep
def main():
win,c=make()
yellow=Rectangle(Point(495,200),Point(395,300))
yellow.setFill("yellow")
yellow.draw(win)
purple=Rectangle(Point(5,200),Point(105,300))
purple.setFill("purple")
purple.draw(win)
black=Rectangle(Point(200,5),Point(300,105))
black.setFill("black")
black.draw(win)
z=0.2
dx,dy=3,-6
while True:
c.move(dx,dy)
sleep(z)
center=c.getCenter()
dx,dy=bounce(dx,dy,center)
point=win.checkMouse()
if point != None:
if isClicked(point,yellow):
z=z-0.05
elif isClicked(point,purple):
z=z+0.05
elif isClicked(point,c):
win.close
def bounce(dx,dy,center):
if center.getX()<25 or center.getX()>475:
dx *= -1
if center.getY()<25 or center.getY()>475:
dy *= -1
if (center.getX() > 370 and center.getX() < 470) and (center.getY() >175 and center.getY() < 325):
dx *= -1
dy *= -1
return dx,dy
def isClicked(click, shape):
# verify that click is a Point object otherwise return False
if not click:
return False
# get the X,Y coordinates of the mouse click
x,y = click.getX(), click.getY()
# check if shape is a Circle
if type(shape).__name__ == 'Circle':
center = shape.getCenter()
cx, cy = center.getX(), center.getY()
# if click is within the Circle return True
if ((x-cx)**2 + (y-cy)**2)**.5 <= 25:
return True
# shape must be a Rectangle
else:
x1, y1 = shape.getP1().getX(), shape.getP1().getY()
x2, y2 = shape.getP2().getX(), shape.getP2().getY()
# if click is within the Rectangle
if (x1 < x < x2) and (y1 < y < y2):
return True
# click was not inside the shape
return False
def make():
win = GraphWin('Tester',500,500)
win.setBackground('grey')
c = Circle(Point(250,250),25)
c.setFill('blue')
c.draw(win)
return win, c
main()
As stated previously, the intended function would be that a click in the purple rectangle would slow the circle by 50% and a click in the yellow rectangle should increase speed by 50%. Currently a click in the circle does nothing, as does a click in the yellow square. Additionally, a click in the blue circle should close the circle. Thank you for any advice!
You forgot () in win.close() and it never run this function. You could also add break to exit loop because win.close() only close window but it doesn't end loop which still try to win.checkMouse() but win doesn't exists any more and it gives error.
Problem with yellow rectangle is because first X is bigger then second X (495 > 395) in
yellow = Rectangle(Point(495, 200), Point(395, 300))
and Rectangle doesn't sort these values so finally this is not true
if (x1 < x < x2) and (y1 < y < y2)
If you change it to
yellow = Rectangle(Point(395,200),Point(495,300))
then it works correctly.
BTW:
Instead of type(shape).__name__ == 'Circle' you can use isinstance(shape, Circle)
You could add some spaces and empty lines to make it more readable.
Using import * is not prefered - but this part I didn't change.
PEP 8 -- Style Guide for Python Code
Instead of if point != None: you should rather use if point is not None: or shorter if point:
from graphics import *
from time import sleep
def main():
win, c = make()
yellow = Rectangle(Point(395, 200), Point(495, 300))
yellow.setFill("yellow")
yellow.draw(win)
purple = Rectangle(Point(5, 200), Point(105, 300))
purple.setFill("purple")
purple.draw(win)
black = Rectangle(Point(200, 5), Point(300, 105))
black.setFill("black")
black.draw(win)
z = 0.2
dx, dy = 3, -6
while True:
c.move(dx, dy)
sleep(z)
center = c.getCenter()
dx, dy = bounce(dx, dy, center)
point = win.checkMouse()
#if point is not None:
if point:
if isClicked(point, yellow):
z = z - 0.05
elif isClicked(point, purple):
z = z + 0.05
elif isClicked(point, c):
win.close()
break
def bounce(dx, dy, center):
if center.getX() < 25 or center.getX() > 475:
dx *= -1
if center.getY() < 25 or center.getY() > 475:
dy *= -1
if (center.getX() > 370 and center.getX() < 470) and (center.getY() > 175 and center.getY() < 325):
dx *= -1
dy *= -1
return dx, dy
def isClicked(click, shape):
# verify that click is a Point object otherwise return False
if not click:
return False
# get the X,Y coordinates of the mouse click
x, y = click.getX(), click.getY()
# check if shape is a Circle
if isinstance(shape, Circle):
center = shape.getCenter()
cx, cy = center.getX(), center.getY()
# if click is within the Circle return True
if ((x-cx)**2 + (y-cy)**2)**.5 <= 25:
return True
# shape must be a Rectangle
else:
x1, y1 = shape.getP1().getX(), shape.getP1().getY()
x2, y2 = shape.getP2().getX(), shape.getP2().getY()
# if click is within the Rectangle
if (x1 < x < x2) and (y1 < y < y2):
return True
# click was not inside the shape
return False
def make():
win = GraphWin('Tester', 500, 500)
win.setBackground('grey')
c = Circle(Point(250, 250), 25)
c.setFill('blue')
c.draw(win)
return win, c
main()
This is my homework: Python, using tkinter/Spyder.
In Python, I want to make a simulation : There is a fixed ball in a canvas, and use a stick to hit the ball(collision) and make it move.
I can make the stick move, but cannot hit the ball and the ball doesn't move at all. The stick just goes through the ball. Or, if I make the ball move by giving the velocity, then the ball moves by itself without hitting.
def get_position(self):
return self.canvas.coords(self.item)
# ...
def check_collisions(self):
Ball_coords = self.Ball.get_position()
items = [Stick, Ball]
items = self.canvas.find_overlapping(Ball_coords)
items = [self.items[x] for x in items if x in self.items]
self.Ball.collide(items)
def move(self, velocity):
coords = self.get_position()
width = self.canvas.winfo_width()
if coords[0] + velocity >= 0 and coords[2] + velocity <= width:
#coords == [x1, y1, x2, y2]
super(Stick, self).move(velocity, 0)
vx = -3
vy = 0
dt = .1
for it in range(100000):
dx = vx * dt
dy = vy * dt
x += dx
if x-r < xmin : vx = -vx
if y-r < ymin : vy = -vy
if x+r > xmax : vx = -vx
if y+r > ymax : vy = -vy
canvas.move(Ball,dx,dy)
window.update()
time.sleep(0.000001)
I expect the collision with a ball and a stick makes move the ball, but, the actual output is the ball doesn't move at all or just move itself without collision.
I have a program in Python for making one ball bounce off of all 4 sides of a window, but now I need to make 10 balls using an array. I'm still very new to this and confused on how to implement this. I'll post what I have so far below:
#create 10 balls bouncing off all 4 sides of the window (400 x 500)
#The ball must start moving after a click, then stop after a given amount of time
#After a second click, the program ends
#----Algorithm----#
#create screen 400 x 500
#create array of 10 circles in different starting points
#wait for click anywhere on screen
#click anywhere on screen
#all 10 balls move in different directions bouncing off all 4 walls
#if ball hits left or right wall
#dy=dy*-1
#if ball hits top or bottom
#dx=dx*-1
#ball moves for no more than 30 seconds
#ball stops
#wait for next click
#program ends
#----ProgramStarts----#
from graphics import *
import time, random
#create screen
winWidth = 400;
winHeight = 500;
win = GraphWin('Ball Bounce', winWidth, winHeight);
win.setBackground(color_rgb(255,255,255));
win.setCoords(0,0,winWidth, winHeight);
numBalls= 10;
#create 10 balls
def makeBall(center, radius, win):
balls=[];
radius=10;
for i in range (0,numBalls):
aBall=Circle(center, radius);
aBall.setFill("red");
aBall.draw(win);
balls.append(aBall);
#animate 10 balls bouncing off edges of window
def bounceInWin(shape, dx, dy, xLow, xHigh, yLow, yHigh):
clickPoint=win.getMouse();
delay = .005
for x in range(900):
shape.move(dx, dy)
center = shape.getCenter()
x = center.getX()
y = center.getY()
if x < xLow:
dx = -dx
if x > xHigh:
dx = -dx
if y < yLow:
dy = -dy
if y > yHigh:
dy = -dy
time.sleep(delay);
#get a random Point
def getRandomPoint(xLow, xHigh, yLow, yHigh):
x = random.randrange(xLow, xHigh+1)
y = random.randrange(yLow, yHigh+1)
return Point(x, y)
#make ball bounce
def bounceBall(dx, dy):
winWidth = 400
winHeight = 500
win = GraphWin('Ball Bounce', winWidth, winHeight);
win.setCoords(0,0,winWidth, winHeight);
radius = 10
xLow = radius
xHigh = winWidth - radius
yLow = radius
yHigh = winHeight - radius
center = getRandomPoint(xLow, xHigh, yLow, yHigh)
ball = makeBall(center, radius, win)
bounceInWin(ball, dx, dy, xLow, xHigh, yLow, yHigh)
bounceBall(3, 5);
#wait for another click to end program
win.getMouse(); #close doesn't work, tried putting in loop
win.close;
Specific problems with your code: makeBall() fails to return balls; makeBall() needs to generate random starting points, not all starting at the same place; bounceInWin() needs to have a separate, and separately updated, dx and dy for every ball, not just the one; the code creates the main window twice -- just once is fine; need to add time calculation to shut down motion instead of fixed number of iterations; win.close() is a method so it needs parentheses; avoid using semi-colon ";" in Python code.
Below is a rework of your code with the above changes and some style tweaks:
# Create 10 balls bouncing off all 4 sides of a window (400 x 500)
# The ball must start moving after a click, then stop after a given
# amount of time. After a second click, the program ends.
#----Algorithm----#
# create screen 400 x 500
# create array of 10 circles in different starting points
# wait for click anywhere on screen
# all 10 balls move in different directions bouncing off all 4 walls
# if ball hits left or right wall
# dx = -dx
# if ball hits top or bottom
# dy = -dy
# balls move for no more than 30 seconds
# balls stops
# wait for next click
# program ends
#----ProgramStarts----#
import time
import random
from graphics import *
winWidth, winHeight = 400, 500
ballRadius = 10
ballColor = 'red'
numBalls = 10
delay = .005
runFor = 30 # in seconds
# create 10 balls, randomly located
def makeBalls(xLow, xHigh, yLow, yHigh):
balls = []
for _ in range(numBalls):
center = getRandomPoint(xLow, xHigh, yLow, yHigh)
aBall = Circle(center, ballRadius)
aBall.setFill(ballColor)
aBall.draw(win)
balls.append(aBall)
return balls
# animate 10 balls bouncing off edges of window
def bounceInWin(shapes, dx, dy, xLow, xHigh, yLow, yHigh):
movedShapes = [(getRandomDirection(dx, dy), shape) for shape in shapes]
start_time = time.time()
while time.time() < start_time + runFor:
shapes = movedShapes
movedShapes = []
for (dx, dy), shape in shapes:
shape.move(dx, dy)
center = shape.getCenter()
x = center.getX()
if x < xLow or x > xHigh:
dx = -dx
y = center.getY()
if y < yLow or y > yHigh:
dy = -dy
# Could be so much simpler if Point had setX() and setY() methods
movedShapes.append(((dx, dy), shape))
time.sleep(delay)
# get a random direction
def getRandomDirection(dx, dy):
x = random.randrange(-dx, dx)
y = random.randrange(-dy, dy)
return x, y
# get a random Point
def getRandomPoint(xLow, xHigh, yLow, yHigh):
x = random.randrange(xLow, xHigh + 1)
y = random.randrange(yLow, yHigh + 1)
return Point(x, y)
# make balls bounce
def bounceBalls(dx, dy):
xLow = ballRadius * 2
xHigh = winWidth - ballRadius * 2
yLow = ballRadius * 2
yHigh = winHeight - ballRadius * 2
balls = makeBalls(xLow, xHigh, yLow, yHigh)
win.getMouse()
bounceInWin(balls, dx, dy, xLow, xHigh, yLow, yHigh)
# create screen
win = GraphWin('Ball Bounce', winWidth, winHeight)
bounceBalls(3, 5)
# wait for another click to end program
win.getMouse()
win.close()
I am in the process of learning tkinter on Python 3.X. I am writing a simple program which will get one or more balls (tkinter ovals) bouncing round a rectangular court (tkinter root window with a canvas and rectangle drawn on it).
I want to be able to terminate the program cleanly by pressing the q key, and have managed to bind the key to the root and fire the callback function when a key is pressed, which then calls root.destroy().
However, I'm still getting errors of the form _tkinter.TclError: invalid command name ".140625086752360" when I do so. This is driving me crazy. What am I doing wrong?
from tkinter import *
import time
import numpy
class Ball:
def bates():
"""
Generator for the sequential index number used in order to
identify the various balls.
"""
k = 0
while True:
yield k
k += 1
index = bates()
def __init__(self, parent, x, y, v=0.0, angle=0.0, accel=0.0, radius=10, border=2):
self.parent = parent # The parent Canvas widget
self.index = next(Ball.index) # Fortunately, I have all my feathers individually numbered, for just such an eventuality
self.x = x # X-coordinate (-1.0 .. 1.0)
self.y = y # Y-coordinate (-1.0 .. 1.0)
self.radius = radius # Radius (0.0 .. 1.0)
self.v = v # Velocity
self.theta = angle # Angle
self.accel = accel # Acceleration per tick
self.border = border # Border thickness (integer)
self.widget = self.parent.canvas.create_oval(
self.px() - self.pr(), self.py() - self.pr(),
self.px() + self.pr(), self.py() + self.pr(),
fill = "red", width=self.border, outline="black")
def __repr__(self):
return "[{}] x={:.4f} y={:.4f} v={:.4f} a={:.4f} r={:.4f} t={}, px={} py={} pr={}".format(
self.index, self.x, self.y, self.v, self.theta,
self.radius, self.border, self.px(), self.py(), self.pr())
def pr(self):
"""
Converts a radius from the range 0.0 .. 1.0 to window coordinates
based on the width and height of the window
"""
assert self.radius > 0.0 and self.radius <= 1.0
return int(min(self.parent.height, self.parent.width)*self.radius/2.0)
def px(self):
"""
Converts an X-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its width
"""
assert self.x >= -1.0 and self.x <= 1.0
return int((1.0 + self.x) * self.parent.width / 2.0 + self.parent.border)
def py(self):
"""
Converts a Y-coordinate in the range -1.0 .. +1.0 to a position
within the window based on its height
"""
assert self.y >= -1.0 and self.y <= 1.0
return int((1.0 - self.y) * self.parent.height / 2.0 + self.parent.border)
def Move(self, x, y):
"""
Moves ball to absolute position (x, y) where x and y are both -1.0 .. 1.0
"""
oldx = self.px()
oldy = self.py()
self.x = x
self.y = y
deltax = self.px() - oldx
deltay = self.py() - oldy
if oldx != 0 or oldy != 0:
self.parent.canvas.move(self.widget, deltax, deltay)
def HandleWallCollision(self):
"""
Detects if a ball collides with the wall of the rectangular
Court.
"""
pass
class Court:
"""
A 2D rectangular enclosure containing a centred, rectagular
grid of balls (instances of the Ball class).
"""
def __init__(self,
width=1000, # Width of the canvas in pixels
height=750, # Height of the canvas in pixels
border=5, # Width of the border around the canvas in pixels
rows=1, # Number of rows of balls
cols=1, # Number of columns of balls
radius=0.05, # Ball radius
ballborder=1, # Width of the border around the balls in pixels
cycles=1000, # Number of animation cycles
tick=0.01): # Animation tick length (sec)
self.root = Tk()
self.height = height
self.width = width
self.border = border
self.cycles = cycles
self.tick = tick
self.canvas = Canvas(self.root, width=width+2*border, height=height+2*border)
self.rectangle = self.canvas.create_rectangle(border, border, width+border, height+border, outline="black", fill="white", width=border)
self.root.bind('<Key>', self.key)
self.CreateGrid(rows, cols, radius, ballborder)
self.canvas.pack()
self.afterid = self.root.after(0, self.Animate)
self.root.mainloop()
def __repr__(self):
s = "width={} height={} border={} balls={}\n".format(self.width,
self.height,
self.border,
len(self.balls))
for b in self.balls:
s += "> {}\n".format(b)
return s
def key(self, event):
print("Got key '{}'".format(event.char))
if event.char == 'q':
print("Bye!")
self.root.after_cancel(self.afterid)
self.root.destroy()
def CreateGrid(self, rows, cols, radius, border):
"""
Creates a rectangular rows x cols grid of balls of
the specified radius and border thickness
"""
self.balls = []
for r in range(1, rows+1):
y = 1.0-2.0*r/(rows+1)
for c in range(1, cols+1):
x = 2.0*c/(cols+1) - 1.0
self.balls.append(Ball(self, x, y, 0.001,
numpy.pi/6.0, 0.0, radius, border))
def Animate(self):
"""
Animates the movement of the various balls
"""
for c in range(self.cycles):
for b in self.balls:
b.v += b.accel
b.Move(b.x + b.v * numpy.cos(b.theta),
b.y + b.v * numpy.sin(b.theta))
self.canvas.update()
time.sleep(self.tick)
self.root.destroy()
I've included the full listing for completeness, but I'm fairly sure that the problem lies in the Court class. I presume it's some sort of callback or similar firing but I seem to be beating my head against a wall trying to fix it.
You have effectively got two mainloops. In your Court.__init__ method you use after to start the Animate method and then start the Tk mainloop which will process events until you destroy the main Tk window.
However the Animate method basically replicates this mainloop by calling update to process events then time.sleep to waste some time and repeating this. When you handle the keypress and terminate your window, the Animate method is still running and attempts to update the canvas which no longer exists.
The correct way to handle this is to rewrite the Animate method to perform a single round of moving the balls and then schedule another call of Animate using after and provide the necessary delay as the after parameter. This way the event system will call your animation function at the correct intervals while still processing all other window system events promptly.