Why does this list index go out of range? - python

I'm taking some baby steps with pygame, and I have gotten this program to work. However it throws a "List index out of range" message after looping through main() several hundred times. The list doesn't change size once it grows to a certain limit, so I am not sure where the error is happening.
Once you run the program, just move the mouse across the display, and a bunch of circles are drawn and grow as the clock ticks. Eventually, it will crash.
I have omitted all comments except the lines where the error is happening, hopefully that will make it easier to find out the cause.
Stacktrace:
Traceback (most recent call last):
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 89, in <module>
main()
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 49, in main
drawCircles(snakeLength)
File "C:\Users\Devo\AppData\Local\Programs\Python\Python37-32\test files\Psychedelic Circles.py", line 75, in drawCircles
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
IndexError: list index out of range
import pygame, sys, random
from pygame.locals import*
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
FUSCHIA = (255, 0, 240)
GRAY = (80, 80, 80)
YELLOW = (255, 255, 0)
ORANGE = (255, 127, 0)
BLUE = (0, 0, 255)
INDIGO = (75, 0, 130)
VIOLET = (148, 0, 211)
FPS = 20
WWIDTH = 1000
WHEIGHT = 700
BIGBUTTON = pygame.Rect(0, 0, 1000, 700)
rainbowTuple = (RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET)
def main():
global SCREEN, FPS, colorList, coords, snakeLength
pygame.init()
clock = pygame.time.Clock()
size = (WWIDTH, WHEIGHT)
SCREEN = pygame.display.set_mode(size)
pygame.display.set_caption('Psychedelic Circles')
colorList = []
coords = []
snakeLength = 50 ### Change this value to make the circles disappear more quickly or slowly
while True:
clickedButton = None
SCREEN.fill(GRAY)
drawButtons()
checkForQuit()
for event in pygame.event.get():
if event.type == MOUSEMOTION:
mousex, mousey = event.pos
clickedButton = getButtonClicked(mousex, mousey)
if clickedButton == FUSCHIA:
sendCoords(mousex, mousey)
drawCircles(snakeLength)
pygame.display.update()
clock.tick(FPS)
def terminate():
pygame.quit()
sys.exit()
def sendCoords(x, y):
coords.append((x, y))
colorList.append(random.choice(rainbowTuple))
def checkForQuit():
for event in pygame.event.get(QUIT):
terminate()
for event in pygame.event.get(KEYUP):
if event.key == K_ESCAPE:
terminate()
pygame.event.post(event)
def drawButtons():
pygame.draw.rect(SCREEN, FUSCHIA, BIGBUTTON)
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
if i > snakeLength :
popList()
def popList():
coords.pop(0)
colorList.pop(0)
def getButtonClicked(x, y):
if BIGBUTTON.collidepoint((x, y)):
return FUSCHIA
return None
if __name__ == '__main__':
main()

I suspect this error only occurs when there are multiple mouse move events in the event queue. Normally, pygame is fast enough to render the screen while accruing no more than one new user input event, so sendCoords will only be called once in between drawCircles calls. In that case, coords never exceeds a size of 52. But if several mouse move events accrue (perhaps due to system lag, or because the user is jiggling his mouse really fast), then sendCoords may be called many more times in a row. So by the time drawCircles executes, coords can have 53 elements, or even more.
This becomes a problem when you reach drawCircles:
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
if i > snakeLength :
popList()
Let's say this function executes when coords contains 53 elements, and snakeLength is 50. The loop will iterate normally until i equals 51. Then the i > snakeLength will evaluate to True, and popList will be called. Now coords is one element smaller, and has a length of 52. That iteration of the loop ends, and the next iteration starts. i will equal 52. The pygame.draw.circle line will attempt to access coords[i], but because coords no longer has 53 elements, coords[i] will raise an IndexError trying to access the 53rd element.
Python is not smart enough to understand that the for i in range(len(coords)) loop should end one iteration earlier than usual if coords reduces in size by one. It happily iterates all the way up to the original length of the list, heedless of whether this might cause a crash.
One possible solution is to move popList outside of the list, so the size of coords doesn't change while you're iterating over it.
def drawCircles(snakeLength):
for i in range(len(coords)):
pygame.draw.circle(SCREEN, colorList[i], coords[i], abs(i-(len(coords))) * 5, 0)#### Why does the list index go out of range?
while len(coords) > snakeLength :
popList()
You might be thinking "But why is it OK to modify coords' length in this while loop, when it wasn't OK to do it in the for loop?" The critical distinction is the evaluation time of the two statements. range(len(coords)) exectues exactly once before the loop starts, so modifications to coords won't be noticed. but len(coords) > snakeLength executes at the beginning of very iteration of the while loop, so changes to coords gets noticed right away.

Related

Item in list multiplied by delta_time not showing change on screen

I created a class that handles particles in pygame. In the emit function which moves and displays the particles, I also have it set to gradually darken each particle. It darkens each particle but very minimally. This is because somehow the color of each particle, stored in a list with the particle's other data, is only changed by a small amount. For example, its default value is (255, 255, 255), after a single darken, it becomes around (245, 245, 245). It should continue to darken, but it just stops instead. When I change the other values of the particle, like its position, they don't reset, and each particle has its own list of values, so I have no idea why it is resetting. Here is part of the code(inside a class):
def emit(self, screen, delta_time):
if self.particles: # If there is anything in the particles list:
self.null_particles() # Delete all faded particles from list
for particle in self.particles: # Loop through all particles in list
# Shift particle spawn location
particle[0][0] += random.randint(-20, 20)
particle[0][1] += random.randint(-10, 10)
# If the direction isn't set, make it random
if particle[2] == [0, 0]:
particle[2] = [random.randint(-3, 3), random.randint(-3, 3)]
elif particle[2][0] == 0:
particle[2][0] = random.randint(-3, 3)
elif particle[2][1] == 0:
particle[2][1] = random.randint(-3, 3)
# Move particle based off of direction parameters
particle[0][0] += particle[2][0]
particle[0][1] += particle[2][1]
# Make particle smaller (fade out effect)
particle[1] -= 50*delta_time
# Make color gradually get darker
proxy_color = (particle[3][0]-10*delta_time, particle[3][1]-10*delta_time, particle[3][2]-10*delta_time)
if proxy_color[0] < 0 or proxy_color[1] < 0 or proxy_color[2] < 0:
pass
else:
particle[3] = proxy_color
# Display particle
pygame.draw.circle(screen, particle[3], particle[0], int(particle[1]))
def add_particles(self, pos_x, pos_y, direction_x=0, direction_y=0, color=(255, 255, 255)):
pos_x = pos_x
pos_y = pos_y
radius = 7
particle_circle = [[pos_x, pos_y], radius, [direction_x, direction_y], color]
self.particles.append(particle_circle)
def null_particles(self):
particle_copy = [particle for particle in self.particles if particle[1] > 0]
self.particles = particle_copy
def delete_particles(self):
self.particles = []
Next part, inside a player class, where the particle object is being used:
def particle_display(self, screen, delta_time):
if pygame.time.get_ticks() - self.time_jumping <= 200: # While player jumped within less than .2 seconds ago:
dust_particle.add_particles(self.rect.midbottom[0], self.ground_level, 0, -10) # Add more particles
dust_particle.add_particles(self.rect.midbottom[0], self.ground_level, 0, -10) # Add more particles
dust_particle.add_particles(self.rect.midbottom[0], self.ground_level, 0, -10) # Add more particles
dust_particle.emit(screen, delta_time) # Display said particles
else:
# Delay added to give particles time to disperse, so particles dont get deleted when buttons get deleted
if pygame.time.get_ticks() - self.jump_ended >= 200:
# Delete all particles after .2 sec of jump, or before jumping happened
dust_particle.delete_particles()
The above code, inside a player class, is called in an update method inside the class. This update method is called in a while 1 loop(infinite game loop).
Particle[3] (particles color data), is never manually (or purposefully), reset by me.
Sample output when particle[3] (the part of the list that stores the color) is printed:
Blockquote
(253.68000000000006, 253.68000000000006, 253.68000000000006)
(253.68000000000006, 253.68000000000006, 253.68000000000006)
(253.68000000000006, 253.68000000000006, 253.68000000000006)
(253.85000000000005, 253.85000000000005, 253.85000000000005)
(253.85000000000005, 253.85000000000005, 253.85000000000005)
(253.85000000000005, 253.85000000000005, 253.85000000000005)
(254.02000000000004, 254.02000000000004, 254.02000000000004)
(254.02000000000004, 254.02000000000004, 254.02000000000004)
(254.02000000000004, 254.02000000000004, 254.02000000000004)
(254.19000000000003, 254.19000000000003, 254.19000000000003)
(254.19000000000003, 254.19000000000003, 254.19000000000003)
(254.19000000000003, 254.19000000000003, 254.19000000000003)
(254.36, 254.36, 254.36)
(254.36, 254.36, 254.36)
(254.36, 254.36, 254.36)
(254.52, 254.52, 254.52)
(254.52, 254.52, 254.52)
(254.52, 254.52, 254.52)
(254.68, 254.68, 254.68)
(254.68, 254.68, 254.68)
(254.68, 254.68, 254.68)
(254.84, 254.84, 254.84)
(254.84, 254.84, 254.84)
(254.84, 254.84, 254.84)
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
All help is appreciated.
For this issue, as with many other things related to delta_time. If something doesn't work as expected, simply raise the value you are multiplying by delta_time, as when you multiply by delta_time, it makes the change over time consistent at different frame rates, but it is a consistently smaller change than before on the same frame rate with delta_time.
In this case, all I had to do was lower the value from -10 to -800.
So : proxy_color = (particle[3][0]-10*delta_time, particle[3][1]-10*delta_time, particle[3][2]-10*delta_time) -> proxy_color = (particle[3][0]-800*delta_time, particle[3][1]-800*delta_time, particle[3][2]-800*delta_time)

pygame.display.update updates the entire screen

I am creating a multiplayer game with splitted screen.
I start by drawing the first player on the left-hand side (spaceship, fire bombs, stars in the background (scrolling at half speed) and finally the background), then I update the first part of the screen, for the first player. Then I do the same things for the second player, on the other part of the screen.
But most of the images overlap thoughout the two half-screens. (see image below)
So, basically I need to update one part of the screen using pygame.display.update(), then the other part.
But the command doesn’t work, and updates the entire screen. And everything overlaps.
I've tried the following:
pygame.display.update(Rect((pos, 0), size))
pygame.display.update(Rect((pos, 0, size[0], size[1])))
pygame.display.update((pos, 0, size[0], size[1]))
pygame.display.update(pos, 0, size[0], size[1])
pygame.display.update((pos, 0), size)
But all of these are doing exactly the same thing, and they don't work as expected.
When you are using pygame.display.update() there are two kinds of an optional argument, single rect (which defaults to None) and a list of rects. If no argument is passed, it updates the entire surface area - like display.flip() does.
update(rectangle=None) -> None
update(rectangle_list) -> None
To update only specific elements, either create a list of these elements if you want to update the same group at the same time
background_rects = [star_rect, star_rect, star_rect, some_other_rect]
foreground_rects = [player_rect, enemy1_rect, enemy2_rect, bullet1_rect, bullet2_rect]
pygame.display.update(background_rects)
pygame.display.update(foreground_rects)
or call update(rect) multiple times with the individual elements:
pygame.display.update(star1_rect)
pygame.display.update(star2_rect)
pygame.display.update(star3_rect)
pygame.display.update(character_rect)
pygame.display.update(enemy_rect)
Link to the documentation: https://www.pygame.org/docs/ref/display.html#pygame.display.update
There seems to be some (probably unintended as there is nothing about it in the docs) difference between the handling of pygame 1.9.6 and the 2.0.0.dev branches - below is a MRE which works with 1.9.6, but not with the 2.0.0.dev10 version. In 1.9.6 the difference in updating the display is easily visible. I suggest you install the stable 1.9.6 version if you need this exact functionality!
In case others want to try their luck, here is the MRE with which I tested:
import pygame
import time
screen = pygame.display.set_mode((720, 480))
rect = pygame.Rect((10, 50), (32, 32))
image = pygame.Surface((32, 32))
image.fill((0,100,0))
rect2 = pygame.Rect((10, 10), (32, 32))
image2 = pygame.Surface((32, 32))
image2.fill((100,100,0))
i = 0
while True:
i += 1
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
screen.blit(image, rect)
screen.blit(image2, rect2)
rect.x += 1 # we update the position every cycle
rect2.x += 1 # we update the position every cycle
# but update the rect on screen at different times:
if i < 10:
pygame.display.update() # both
elif i > 50 and i < 75:
pygame.display.update(rect2) # only rect2
elif i >= 100:
pygame.display.update(rect) # only rect
time.sleep(0.1)

create scroll-able rectangles from items in a list

I Have a program where it has a list of names. I want to be able to list each of these names onto a rectangle. I want each name to create its own rectangle. This example create a rectangle for each title in the list i put '' at the start of the list as I got an error saying I couldn't divide by zero in python by adding '' fixed that.
import pygame
width,height = 800,600
screen = pygame.display.set_mode((width,height))
mover = 0
games = ['','Space Invaders','Snake']
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
quit()
if event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 4:
if mover > 0:
mover -= 15
if event.button == 5:
if mover < (height-120)-60:
mover += 15
screen.fill((47,79,79))
for game in games:
for num in range(len(games)):
if num != 0:
pygame.draw.rect(screen, (0,51,51), (100,((height-120)/num)-mover,width-200,20))
pygame.draw.rect(screen, (0,51,51), (width-30,100,25,height-120)) #SCROLL BAR
pygame.draw.rect(screen, (0,0,0), (width-29,mover+100,23,60)) #SCROLLING BAR
pygame.draw.line(screen, (0,0,0), (0,100),(width,100), 4)
pygame.display.flip()
I am not sure in how I can get the rectangles closer together so that there is only 5 to 10 pixels between each rectangle. I also feel like there is a better way in doing this.
Define a distance to top of the screen. e.g. 100. Define a distance between the lines. The height of a line is 20. If the gap between the lines should be 10, the the distance is 30. So the start of a line is 110 + num*30 - mover:
for num, game in enumerate(games):
pygame.draw.rect(screen, (0,51,51), (100, 110 + num*30 + mover, width-200, 20))
Note, you can use enumerate, to get tuples of list index and list element.

pygame rectangles drawn in loop scroll across screen

Here is my full code:
import pygame
pygame.init()
i = 0
x = 0
# Define the colors we will use in RGB format
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
BLUE = ( 0, 0, 255)
GREEN = ( 0, 255, 0)
RED = (255, 0, 0)
# Set the height and width of the screen
size = [600, 300]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Test")
#Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
while not done:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
screen.fill(WHITE)
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)
pygame.display.flip()
pygame.quit()
10 squares are drawn but they scroll across the window to the right and i dont know why. Is there any way I could stop this?
Thanks.
I realise now that it is not about the rectangle loop, but i have changed that to what has been suggested anyway.
Now that you have added more code I see where the problem is coming from and it's just as I suspected - you are doing a static translation the incorrect way namely you use the x (which you have defined outside your main loop) in your for x in range(x, x+100, 10):.
If you add a print(x) statement inside your for-loop you will be able to see that the x gets bigger and bigger, and bigger...This is perfect for adding dynamics to your scene but my guess is (based on your question) that you want to add 10 static rectangles to your scene.
In order to do that you need to reset your x every single time a new iteration of the while loop begins:
while not done:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
screen.fill(WHITE)
# Every single iteration of the while loop will first reset the x to its initial value
# You can make x be any value you want your set of rectangles to start from
x = 0
# Now start adding up to x's initial value
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)
pygame.display.flip()
You can also omit the definition of x as a variable outside the for loop if you won't be using it to change the x coordinate where your first rectangle will start from and replace the range(x, x+100, 10) with range(CONST, CONST+100, 10) where CONST is a given value such as 0, 100, 1000 etc.
Every time your for loop runs the range is incremented, because the x is persistent. If you remove the x=0, reset it inside the while, or use a different variable in the for loop I think it will work.
x=0
while not done:
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)

Bug when randomizing angle between 0 and 2pi (also program stops responding with long drawing loop)

I'm trying to write a small program to illustrate the idea of the mean free path of a photon in a two dimensional star using pygame. I'm new to python in general (This is my 8th week of playing with python) and have only just started to experiment in pygame.
Essentially what my program is supposed to do is have a line start at the center of a circle and move some length 'l' before it turns a random amount of radians and goes another length 'l' and will repeat this until it hits the edge of the circle.
I'm having problems with the angle randomizing. Currently I've employed random.uniform() between 0 and 2pi, but I've noticed the line likes to progress almost straight downwards every time. Any help or suggestions for this problem would be appreciated.
Line 15 is where I define the angle to be used for the next line, also I've noticed if my path length 'l' is low enough the program will stop responding before finishing. I'm sure there's a simple explanation for this that I'm just unaware of, but if someone knows why that might be I would love to know.
Edit: The first problem was pointed out by schwobaseggl, I had wrote the wrong variable in for the change in my y coordinate in line 16. I still haven't figured out why the program stops responding after it draws for a little while though.
My code so far:
import pygame
import random
import math
def mfp():
Rho = float(input('Enter average density: '))
K = float(input('Enter opacity: '))
l = 1/(K * Rho)
return l
def line(l, screen):
a = [250, 250]
n = 0
while ((a[0]-250)**2)+((a[1]-250)**2) < 220**2:
ang = random.uniform(0, 2*math.pi)
print(ang)
#changed sin(l) to sin(ang)
b = [a[0]+l*math.cos(ang), a[1]+l*math.sin(ang)]
pygame.draw.line(screen, [255, 255, 255], a, b, 1)
a = b
pygame.display.flip()
print('2') #debug
def vis():
size = [500, 500]
screen = pygame.display.set_mode(size)
pygame.draw.circle(screen, [255, 255, 255], [250, 250], 220)
pygame.draw.circle(screen, [0, 0, 0], [250, 250], 219)
pygame.display.flip()
print('1') #debug
return screen
def stop():
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
pygame.quit()
def main():
line(mfp(), vis())
stop()
main()
Problem is this line:
b = [a[0]+l*math.cos(ang), a[1]+l*math.sin(l)]
Surely, you want math.sin(ang) here.

Categories