I'm trying to make a game like Line, but with a horizontal and not vertical wave. The problem is making that the wave continues even after changing its amplitude (I will change the frequency later). So far I have reached this part of wave:
import pygame
import pygame.gfxdraw
import math
import time
DISPLAY_W, DISPLAY_H = 400, 800
clock = pygame.time.Clock()
pygame.init()
SCREEN = pygame.Surface((DISPLAY_W, DISPLAY_H))
GAME_DISPLAY = pygame.display.set_mode((DISPLAY_W, DISPLAY_H))
class Line():
def __init__(self):
self.pointsList = [0]*800
self.listIndex = 0
def game(self):
while True:
clock.tick(60)
SCREEN.fill((0, 0, 0))
self.listIndex += +1
self.generateWave()
self.drawWave()
for event in pygame.event.get():
if (event.type == pygame.QUIT):
quit()
pygame.display.update()
GAME_DISPLAY.blit(SCREEN, (0, 0))
def drawWave(self):
for Y_CORD in range(len(self.pointsList)):
pygame.gfxdraw.pixel(
GAME_DISPLAY, self.pointsList[Y_CORD]-55, DISPLAY_H-Y_CORD, (255, 255, 255))
pygame.gfxdraw.pixel(
GAME_DISPLAY, self.pointsList[Y_CORD]-350, DISPLAY_H-Y_CORD, (255, 255, 255))
def generateWave(self):
waveAmplitude = 50
waveFrequency = 1
XCord = int((DISPLAY_H/2) + waveAmplitude*math.sin(
waveFrequency * ((float(0)/-DISPLAY_W)*(2*math.pi) + (time.time()))))
if self.pointsList[-1] != 0:
self.pointsList.pop(0)
self.pointsList.append(XCord)
else:
self.pointsList[self.listIndex] = XCord
if __name__ == "__main__":
game = Line()
game.game()
I thought about having another function to change the amplitude, but then there would be a gap:
Obligatory reference: https://en.wikipedia.org/wiki/Spline_(mathematics)
Splines have many nice properties, on purpose,
such as matching the slope of inbound and outbound
curve segments. But you're not using splines
in the current code, so let's switch gears.
For current y
you can phrase this in terms of hidden_x
and display_x coordinates.
The trouble with the hidden X is, as you mentioned,
it sadly is discontinuous.
Which would make for an ugly display.
So we won't do that.
But it's a perfectly good ideal to shoot for.
So let's run with that notion.
Initialize display_x to equal hidden_x.
Now as Y advances, the hidden value can jump
around disconcertingly, even discontinuously.
Define some parameter alpha on the unit interval 0 .. 1.
Compute
delta = hidden_x - display_x
For each new Y, advance display_x
in the right direction
by alpha * delta.
Thus, no matter how the hidden X jumps around,
the display X will always smoothly be chasing it.
display_x += alpha * delta
Imagine that alpha was set to 1.0.
Then we're essentially assigning the hidden value
to the display value, right?
For positive values less than 1,
the display value will head in the right direction,
and soon will almost catch up to where
the hidden variable is.
Choosing an appropriate alpha value is up to you.
Largely it's an aesthetic decision,
given that OP did not disclose how errors affect
a cost function.
Play with it, have fun!
For more details on this technique,
look into PID controllers.
Feel free to ignore the integration term.
What we've been looking at are
the Position and Derivative terms.
The PID literature might give you
ideas on how to formalize "goodness of fit"
in your use case.
Non-linear transforms might be applicable,
as well.
assert delta >= 0
display_x += max(max_delta, alpha * delta)
Sometimes we describe this as clipping
the derivative to a maximum slew rate.
(If you choose to adopt this approach,
be sure to use something like sgn()
so that both positive and negative deltas
are handled appropriately.)
Some folks might even prefer to use a quadratic
relation with delta,
in order to catch up to "big" swings more quickly.
Related
Background:
A little new to python and psychopy. I’m trying to build a function that would sort of functionally replicate a roulette wheel and I’ve essentially done so. This function will revolve a small circle around a large circle and pressing 'space' while it is revolving will initiate an exponential deceleration in the revolution velocity of the smaller circle. In order to model the roulette wheel (the larger circle), I used the visual.RadialStim component from PsychoPy. It really was designed, as I understand it, to conveniently build the spinning checkerboard visuals often used as controls in fMRI experiments. However, that means that it evenly divides the wheel into alternating sections of color which is really helpful for my purposes. Here's a visualization of the task in action:
Problem:
I'd like to be able to place more than two colors on the wheel, but I'm not sure that it's possible with visual.RadialStim. Looking through the documentation, I can't see anything that helps, though, I did come across this old thread where Jon seems to suggest that it is possible, but I frankly can't make heads or tails of it and I think the OP left it unresolved as well. Does anyone know if my suspicions about RadialStim are correct (i.e., can't use more than two colors)? Alternatively, does anyone have another recommended solution to replacing it so that I could maybe get 3 or 4 colors modeled on this larger circle?
Code:
PLEASE NOTE - for those unfamiliar with PsychoPy, it is a collection functions specially built for creating reseach studies and requires any code that uses it to be run in a PsychoPy terminal (rather than just any old Python terminal). PsychoPy can run any Python package, but a Python terminal cannot run PsychoPy code, so if you tried to run this on your own without PsychoPy, it likely will not work.
# Psychopy modules
from psychopy import core, event, visual, gui
# Needed to calculate the trajectory of the revolving ball
import math
# Needed to calculcate the deceleration of the revolving ball
import random
# Specifying which monitor to use
monitor=0
# The speed with which the ball revolves around the wheel
speed = 0.125
# The radius of the wheel around which the ball is revolving
wheel_radius=0.45
# How many frames per second the animation should use
fps = 30
# Specifying Window & Screen Information -----
win = visual.Window(size=(1024, 768),
fullscr=True,
screen= monitor,
winType='pyglet',
allowGUI=True,
allowStencil=False,
monitor='testMonitor',
color=[0,0,0],
colorSpace='rgb',
blendMode='avg',
useFBO=True,
units='height')
# Noting the starting position of the revolving ball
position = 0.0
# Noting whether the ball is decelerating
decelerate = False
# Creating the ball
ball = visual.Circle(win, edges=100,radius=0.02, fillColor='white', lineColor=None, pos=[position,position])
# Creating the wheel
wheel = visual.RadialStim(win, pos=(0,0), size=((wheel_radius * 2), (wheel_radius * 2)),
color =('red', 'blue', 'white'), angularRes=300,
angularCycles=6, radialCycles = 0, opacity= 0.8, autoLog=False)
# While speed is greater than 0:
while speed > 0:
# Change the position of the ball according to the current value of position
ball.pos = [((math.sin(position)/10) * (wheel_radius * 10)),
((math.cos(position)/10) * (wheel_radius * 10))]
# Produce the visualization of the wheel
wheel.draw()
# Produce the visualization of the ball
ball.draw()
# If the participant hasn't asked to stop the spinner yet
if decelerate == False:
# Continue spinning the ball around the wheel according to the specified speed
position += speed
# If the participant has asked to stop the spinner
if decelerate == True:
# Randomly select a value between 0.005 and 0.035
rand_dec = random.uniform(0.005,0.035)
# Reduce speed to be a percentage (99.5% - 96.5%) of its last value
# Randomizing the the value of the deceleration will hopefully prevent
# participants from being able to predict where the ball will stop. Also
# making speed a fraction or what it once was, rather than using a linear value
# will better model friction and exponential decay in the real world
speed *= 1 - rand_dec
# Continue spinning the ball around the wheel according to the new speed
position += speed
# If speed drops below 0.001
if speed < 0.001:
# Round speed down to 0
speed = 0
# If escape is pressed, end the task
if event.getKeys('escape'):
break
# If space is pressed, begin slowing the ball
if event.getKeys('space'):
decelerate = True
# Refresh the screen according to the core.wait rate allowing for objects and visualizations
# to change position
win.flip()
# How long psychopy should wait before updating the screen
core.wait(1/fps)
# close the window
win.close()
Try as I could, I could not get the texture approach to work, but I settled on a far less eloquent solution. By reducing the opacity of the RadialStim and overlaying another RadialStim of a complementary color at half opacity, and situated at a 30-degree angle, I was able to more or less create the appearance of four colors. Not thrilled, but it'll do for now. Looking forward to someone else showing me up.
# Psychopy modules
from psychopy import core, event, visual, gui
# Needed to calculate the trajectory of the revolving ball
import math
# Needed to calculcate the deceleration of the revolving ball
import random
# Needed to create colors for the roulette wheel
import numpy as np
# Specifying which monitor to use
monitor=0
# The speed with which the ball revolves around the wheel
speed = 0.125
# The radius of the wheel around which the ball is revolving
wheel_radius=0.45
# How many frames per second the animation should use
fps = 30
# Specifying Window & Screen Information -----
win = visual.Window(size=(1024, 768),
fullscr=True,
screen= monitor,
winType='pyglet',
allowGUI=True,
allowStencil=False,
monitor='testMonitor',
color=[0,0,0],
colorSpace='rgb',
blendMode='avg',
useFBO=True,
units='height')
# Noting the starting position of the revolving ball
position = 0.0
# Noting whether the ball is decelerating
decelerate = False
# Creating the ball
ball = visual.Circle(win, edges=100,radius=0.02, fillColor='white', lineColor=None, pos=[position,position])
# Creating the wheel
wheel_base = visual.RadialStim(win, pos=(0,0), size=((wheel_radius * 2), (wheel_radius * 2)),
color ='yellow', angularRes=300,
angularCycles=3, radialCycles = 0, opacity= 0.9, autoLog=False)
wheel_layer = visual.RadialStim(win, pos=(0,0), size=((wheel_radius * 2), (wheel_radius * 2)),
color ='red', angularRes=300, ori=30,
angularCycles=3, radialCycles = 0, opacity= 0.5, autoLog=False)
# While speed is greater than 0:
while speed > 0:
# Change the position of the ball according to the current value of position
ball.pos = [((math.sin(position)/10) * (wheel_radius * 10)),
((math.cos(position)/10) * (wheel_radius * 10))]
# Produce the visualization of the wheel
wheel_base.draw()
wheel_layer.draw()
# Produce the visualization of the ball
ball.draw()
# If the participant hasn't asked to stop the spinner yet
if decelerate == False:
# Continue spinning the ball around the wheel according to the specified speed
position += speed
# If the participant has asked to stop the spinner
if decelerate == True:
# Reduce speed to be a percentage (99.5% - 96.5%) of its last value
# Randomizing the the value of the deceleration will hopefully prevent
# participants from being able to predict where the ball will stop. Also
# making speed a fraction or what it once was, rather than using a linear value
# will better model friction and exponential decay in the real world
speed *= 1 - rand_dec
# Continue spinning the ball around the wheel according to the new speed
position += speed
# If speed drops below 0.001
if speed < 0.001:
# Round speed down to 0
speed = 0
# If escape is pressed, end the task
if event.getKeys('escape'):
break
# If space is pressed, begin slowing the ball
if event.getKeys('space'):
decelerate = True
# Randomly select a value between 0.005 and 0.035
rand_dec = random.uniform(0.005,0.035)
# Refresh the screen according to the core.wait rate allowing for objects and visualizations
# to change position
win.flip()
# How long psychopy should wait before updating the screen
core.wait(1/fps)
# close the window
win.close()
I've been trying to make a code with pygame to simulate simple gravity. At the moment, there is only one object (HOM) which is orbiting the sun. However, for reasons unknown to me, whenever I run the code, HOM travels round the sun in an orbit at the start, but then accelerates away from the sun when it reaches ~135 degrees from vertical.
Does anyone know why this is happening and how I can fix it? I have been printing some variables to try and source the problem, but have had no luck so far.
Code:
import pygame,sys,time
from math import *
screen=pygame.display.set_mode((800,600))
G = 5
class Object: #Just an object, like a moon or planet
def __init__(self,mass,init_cds,init_vel,orbit_obj='Sun'):
self.mass = mass
self.cds = init_cds
self.velocity = init_vel
self.accel = [0,0]
self.angle = 0
self.orb_obj = orbit_obj
def display(self):
int_cds = (round(self.cds[0]),round(self.cds[1]))#Stores its co-ordinates as floats, has to convert to integers for draw function
pygame.draw.circle(screen,(255,0,0),int_cds,10)
def calc_gravity(self):
if self.orb_obj == 'Sun':
c_x,c_y = 400,300
c_mass = 10000
else:
c_x,c_y = self.orb_obj.cds
c_mass = self.orb_obj.mass
d_x = self.cds[0]-c_x
d_y = self.cds[1]-c_y
dist = sqrt(d_x**2+d_y**2) #Find direct distance
angle = atan(d_x/d_y) #Find angle
print(d_x,d_y)
print(dist,degrees(angle))
if dist == 0:
acc = 0
else:
acc = G*c_mass/(dist**2) #F=G(Mm)/r^2, a=F/m -> a=GM/r^2
print(acc)
acc_x = acc*sin(angle) #Convert acceleration from magnitude+angle -> x and y components
acc_y = acc*cos(angle)
self.accel = [acc_x,acc_y]
print(self.accel)
self.velocity = [self.velocity[0]+self.accel[0],self.velocity[1]+self.accel[1]] #Add acceleration to velocity
print(self.velocity)
self.cds = (self.cds[0]+self.velocity[0],self.cds[1]+self.velocity[1]) #Change co-ordinates by velocity
print(self.cds)
print('-------------------') #For seperating each run of the function when printing variables
HOM = Object(1000000,(400,100),[10,0]) #The problem planet
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill((0,0,0))
pygame.draw.circle(screen,(255,255,0),(400,300),25)
HOM.display()
HOM.calc_gravity()
clock.tick(30)
pygame.display.flip()
Your main issue has to do with this line:
angle = atan(d_x/d_y) #Find angle
The atan function is very limited in its ability to compute angles because it can't tell the signs of the coordinates you combined in your division. For instance, it will give the same result for atan(1/1) and atan(-1/-1), since both divisions compute the same slope (1).
Instead you should use atan2, and pass the coordinates separately. Since this will let the code see both coordinates, it can pick an angle in the right quadrant of the circle every time.
But there's an even better fix. Instead of computing an angle and then immediately converting it back to a unit vector (by calling sin and cos on it), why not compute the unit vector directly? You already have the original vector's length! Instead of:
acc_x = acc*sin(angle) #Convert acceleration from magnitude+angle -> x and y components
acc_y = acc*cos(angle)
Use:
acc_x = acc * d_x / distance
acc_y = acc * d_y / distance
The d_x / distance and d_y / distance values are the same as the sin and cos values you were getting before (for the angles when they were working correctly), but there's no need for the trigonometry. You can get rid of the line I quoted up top completely!
Note that you might need to reverse the way you're computing d_x and d_y, so that you get a vector that points from the orbiting object towards the object it's orbiting around (instead of pointing the other way, from the center of the orbit towards the orbiting object). I'm not sure if I'm reading your code correctly, but it looks to me like you have it the other way around right now. That means that you were actually getting the wrong results from atan in the cases where your current code was working the way you expected, and the bad behavior (flying off into nowhere) is the code working "correctly" (from a mathematical point of view). Alternatively, you could compute acc to be negative, rather than positive.
As several commenters mentioned, you may have other issues related to your choice of integration algorithm, but those errors are not going to be as large as the main issue with the acceleration angle. They'll crop up as you run your simulation over longer time periods (and try to use larger time steps to make the simulation go faster). Your current algorithm is good enough for an orbit or two, but if you're simulating dozens or hundreds of orbits, you'll start seeing errors accumulate and so you should pick a better integrator.
I've been wanting to create a physics engine for my game, and decided I should first cover forces. I'm using pygame on python to accomplish this.
For example, I want to be able to program a force of a certain magnitude, that acts on an object. I haven't been able to think of any way to do this. After I get the basic strategy down, it should be easy to subtract magnitudes of forces and add all kinds of different properties. Here is my source code now:
import pygame
pygame.init()
display_width = 800
display_height = 600
gameDisplay = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption('Physics Engine')
clock = pygame.time.Clock()
x = display_width / 2
y = display_height / 2
gameExit = False
def force():
# force function
# Code here
y_change = 0
x_change = 0
while not gameExit:
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
elif event.type == pygame.KEYDOWN: # check for key presses
if event.key == pygame.K_LEFT: # left arrow turns left
x_change = -10
y_change = 0
elif event.key == pygame.K_RIGHT: # right arrow turns right
x_change = 10
y_change = 0
y += y_change
x += x_change
print(x, y)
gameDisplay.fill((255, 255, 255))
pygame.draw.rect(gameDisplay, (255, 0, 0), [x, y, 30, 30])
pygame.display.update()
clock.tick(25)
pygame.quit()
quit()
As of now, it simply is a red square moving left and right. I want it to be that if I hold down the arrow key it moves, and when I release it stops. I've tried using a boolean in the event, but it doesn't seem to want to work.
I would like to implement a simple gravity factor.
Thanks so much for reading!
There are a few points I can see. Firstly, if you want an effect to occur while you hold a key down then you should query if that key is down, rather than just pressed. Ignore KEYDOWN events and use http://www.pygame.org/docs/ref/key.html#pygame.key.get_pressed
instead. Secondly, you should define or use library classes for positions, velocities, etc... which will give you a concrete set of operations.
From there the basic idea is to query the keys, set this update's acceleration from that and gravity, set velocity by updating old velocity with acceleration, set position by updating old position with velocity, and repeat forever.
When a force is applied, it adds an acceleration to the object in terms of the mass in the direction of the force.
First of all, I would suggest creating variables for your object's mass, x and y velocity and its x and y resultant force components. When a force is added to the object, it would be easier to add it in terms of its x and y components ( if you wanted the force to be defined with an angle and magnitude you could use opposite = sin(angle) * hypotenuse and adjacent = cos(angle) * hypotenuse to split the force into components. Careful though -- the math module uses radians, and pygame uses degrees ) .
To apply the force on the screen, you can use force = mass * acceleration to find the x and y accelerations on the object, and apply those changes to the x and y velocities of the object (that's your x_change and y_change). Then apply the x and y velocities to the position of the object.
If you want it to work in terms of meters and seconds, instead of pixels and frames, you'll need to work out conversions for that as well: just use your clock.tick value to work out frames per second, and you can make up a value for pixels per meter.
As for gravity, the force applied = mass * gravitational field strength (the gravitational field strength on earth is 9.81 Newtons / kilogram ). Using this formula, you can add gravity to your object like any other force. The acceleration due to gravity is always the same as the gravitational field strength though, which might be useful if no other vertical forces are being applied.
I am trying to move a rectangle to a specific point with a specific speed.
however it only works properly if x,y (the point i am trying to move it to) are the same. otherwise, if x was bigger than y it would move at a 45 degree angle until self.location[1]==y(it reached where it needed to be for y), then it would move in a straight for x and vise versa.
i know that i would have to change speed_y so that it was slower. how do i work out what speed i need y to be at in order to get the rectangle to move to location in a straight line no matter what location is?
full function:
def move(self,x,y,speed):
if speed==0:
self.location=[x,y]
else:
speed_x = speed
speed_y = speed
if x > 0 and not self.location[0]==x: # x is positive and not already where it needs to be
if not x == y:
speed_x = something # need to slow down the speed so that it gets to y as the same time as it gets to x
self.speed_x=speed_x
else: self.speed_x=0
if y > 0 and not self.location[1]==y: # same for y
if not x == y:
speed_y = something # need to slow down the speed so that it gets to y as the same time as it gets to x
self.speed_y=speed_y
else: self.speed_y=0
You should set your speeds to a ratio of required distance for each axis.
e.g. if distance to x is half distance to y then speed_x should be half speed_y.
Further example as requested:
distance_x = x-self.location[0]
distance_y = y-self.location[1]
if abs(distance_x) < abs(distance_y):
ratio = distance_x/abs(distance_y)
speed_x = ratio * speed
edit: Reworked the directions of example.
I don't quite get what you are asking for, but see if this helps you:
speed_x = speed*(cos(atan2((y-self.location[1]), (x-self.location[0]))))
speed_y = speed*(sin(atan2((y-self.location[1]), (x-self.location[0]))))
That would "slow" the speed you are given by splitting it in the values needed to get to where you want your box to be at the same time.
Pardon any errors in my english/python, im not native on either one :)
You need mathematical calculations to get new position in every move.
I have coded an animation (in python) for three beach balls to bounce around a screen. I now wish to have them all collide and be able to bounce off each other. I would really appreciate any help that can be offered.
import pygame
import random
import sys
class Ball:
def __init__(self,X,Y):
self.velocity = [1,1]
self.ball_image = pygame.image.load ('Beachball.jpg'). convert()
self.ball_boundary = self.ball_image.get_rect (center=(X,Y))
self.sound = pygame.mixer.Sound ('Thump.wav')
self.rect = self.ball_image.get_rect (center=(X,Y))
if __name__ =='__main__':
width = 800
height = 600
background_colour = 0,0,0
pygame.init()
window = pygame.display.set_mode((width, height))
pygame.display.set_caption("Bouncing Ball animation")
num_balls = 3
ball_list = []
for number in range(num_balls):
ball_list.append( Ball(random.randint(10, (width - 10)),random.randint(10, (height - 10))) )
while True:
for event in pygame.event.get():
print event
if event.type == pygame.QUIT:
sys.exit(0)
window.fill (background_colour)
for ball in ball_list:
if ball.ball_boundary.left < 0 or ball.ball_boundary.right > width:
ball.sound.play()
ball.velocity[0] = -1 * ball.velocity[0]
if ball.ball_boundary.top < 0 or ball.ball_boundary.bottom > height:
ball.sound.play()
ball.velocity[1] = -1 * ball.velocity[1]
ball.ball_boundary = ball.ball_boundary.move (ball.velocity)
window.blit (ball.ball_image, ball.ball_boundary)
pygame.display.flip()
Collision detection for arbitrary shapes is usually quite tricky since you have to figure out if any pixel collides.
This is actually easier with circles. If you have two circles of radius r1 and r2, a collision has occurred if the distance between the centers is less than r1+r2.
The distance between the two centers (x1,y1) and (x2,y2) can be calculated and compared as:
d = sqrt((y2-y1) * (y2-y1) + (x2-x1) * (x2-x1));
if (d < r1 + r2) { ... bang ... }
Or, as jfclavette points out, square roots are expensive so it may be better to calculate using just simple operations:
dsqrd = (y2-y1) * (y2-y1) + (x2-x1) * (x2-x1);
if (dsqrd < (r1+r2)*(r1+r2)) { ... bang ... }
The tricky bit comes in calculating the new movement vectors (the rate at which (x,y) changes over time for a given object) since you need to take into account the current movement vectors and the point of contact.
I think as a first cut, you should just reverse the movement vectors to test if the collision detection works first.
Then ask another question - it's better to keep individual questions specific so answers can be targeted.
Detecting a collision is only the first step. Let's break that down.
The fastest thing to do is calculate their square bounding boxes and see if those collide. Two of the sides need to cross (top of 1 and bottom or 2, and left of 1 and right of 2, or vice versa) in order for the bounding boxes to overlap. No overlap, no collision.
Now, when they do overlap, you need to calculate the distance between them. If this distance is more than the sums of the radii of the balls, then no collision.
Okay! We have two balls colliding. Now what? Well, they have to bounce off each other. Which way they bounce depends on a few factors.
The first is their elasticity. Two rubber balls bouncing off each other rebound differently than two glass balls.
The second is their initial velocity. Inertia states that they'll want to keep going in mostly the same direction they started in.
The third is the mass of the balls. A ball with smaller mass will rebound off a much larger mass with a higher velocity.
Let's deal with the second and third factors first, since they are intertwined.
Two balls will rarely hit exactly dead on. Glancing blows are far more likely. In any case, the impact will happen along the normal of the tangent where the balls collide. You need to calculate the vector component of both along this normal given their initial velocities. This will result in a pair of normal velocities that both balls will bring to the collision. Add up the sum and store it somewhere handy.
Now we have to figure out what each ball will take away from it. The resulting normal velocity of each ball is inversely proportional to the given ball's mass. That is to say, take the reciprocal of each ball's mass, add both masses together, and then parcel out the resultant normal velocity away from the collision based on the ratio of the ball's mass to the sum of the reciprocal of both ball's masses. Then add the tangential velocity to this, and you get the resultant velocity of the ball.
Elasticity is mostly the same, except it requires some basic calculus due to the fact that the balls are still moving even as they compress. I'll leave it to you to find the relevant math.
Detecting collisions was covered well by Pax's answer. With respect to having objects bounce off one another, I suggest checking out the following links concerning elastic collisions, inelastic collisions, and coefficients of restitution.
EDIT: I just noticed that this was covered in another SO question, albeit not specifically for Python. You should also check there for some good links.
I think there is somehthing simpler that you guys are missing espeically considering he's using pygame.
Calling the get_rect function can set probably boundraies for the images and Rect that is created, is used for calculating the position of the image and if there are more than one object in the animation, it can be used for detecting collisions.
colliderect & rect can be used, problem is i have no idea how you would implement it especially for an unkown number of balls.
Keeping in mind it's python.
Back in the good old times when CPU cycles were a premium coders used a simple trick to detect collision: they used such colours that they could tell from the pixel colour if it was background or an object. This was done on at least some C64 games.
Don't know if you are willing to go this route, though..
First you need to check collision with rect.colliderect(other_rect)
after that if they are colliding, you can check pixel perfect collision. So you don't mess with object's radius or shape.
For pixel perfect collision checking, I use Masks:
Make both mask objects with mask.from_surface, then put them to Mask.overlap function.
I made a python collision detection if statement, here it is:
if beach ball 1 x < beach ball 2 x + beach ball 1 width and beach ball 1 x + beach ball 2 width > beach ball 2 x and beach ball 1 y < beach ball 2 y + beach ball 1 height and beach ball 2 height + beach ball 1 y > beach ball 2 y:
#put needed code here
In your case, with 3 bouncing balls, you will have to make 2 if statements of this format for each ball to make sure that the collision detection is flawless. I hope this helps.