KetZoom
Posts: 6
Joined: Wed Jan 08, 2020 6:14 am

Classes and For Loops

Wed Jan 08, 2020 6:25 am

Hi. I have created this space invader game, but I added a way to delete the invader, and if gives my and error. I am looping through a class variable, and the in a nested for loop, and looping through another class variable. It is saying the first class variable is undifined. The error is occuring on line 197, or the line starting like this: if thing.x < thing2.x < thing.x+80

Code: Select all

import pygame, sys, random, math, time
from pygame.locals import *
pygame.init()
screen = pygame.display.set_mode((950,950))
pygame.display.set_caption("Space Invaders")
red = (255,0,0)
green = (0,255,0)
blue = (0, 0, 255)
nblue = (50, 50, 255)
yellow = (255, 235, 0)
brown = (200,100,60)
black = (0,0,0)
white = (255,255,255)
gray = (211,211,211)
screen.fill(nblue)

findnew = True

image1 = pygame.image.load("/home/pi/Pictures/Space Invaders/space_invader1.png")
image1 = pygame.transform.scale(image1, (80, 60))
image2 = pygame.image.load("/home/pi/Pictures/Space Invaders/space_invader2.png")
image2 = pygame.transform.scale(image2, (80, 60))
image3 = pygame.image.load("/home/pi/Pictures/Space Invaders/space_invader3.png")
image3 = pygame.transform.scale(image3, (80, 60))
image4 = pygame.image.load("/home/pi/Pictures/Space Invaders/Bullet.png")
image4 = pygame.transform.scale(image4, (10, 40))
image5 = pygame.image.load("/home/pi/Pictures/Space Invaders/spaceship.png")
image5 = pygame.transform.scale(image5, (100, 100))
image6 = pygame.transform.flip(image4, False, True)

class Invader:
    
    invader1 = []
    invader2 = []
    invader3 = []
    findnew = True
    
    def __init__(self, x, y, pic, lis):
        self.x = x
        self.y = y
        self.pic = pic
        self.lis = lis
    def display(self):
        screen.blit(self.pic, (self.x, self.y))
    def moveall():
        if Invader.findnew:
            move = 0
            while move==0:
                move = random.randint(-3, 3)*10
        else:
            move = random.randint(-3, -1)*10
        for thing in Invader.invader1:
            if 870 < invader.x:
                if move < 0:
                    thing.x+=move
                else:
                    move = random.randint(-3, -1)*10
                    thing.x+=move
                    Invader.findnew = False
            elif 10 > thing.x:
                if move > 0:
                    thing.x+=move
                else:
                    move = random.randint(1,3)*10
                    thing.x+=move
            else:
                thing.x+=move
        
        for thing in Invader.invader2:
            if 870 < invader.x:
                if move < 0:
                    thing.x+=move
                else:
                    move = random.randint(-3, -1)*10
                    thing.x+=move
                    Invader.findnew = False
            elif 10 > thing.x:
                if move > 0:
                    thing.x+=move
                else:
                    move = random.randint(1,3)*10
                    thing.x+=move
            else:
                thing.x+=move
        for thing in Invader.invader3:
            if 870 < invader.x:
                if move < 0:
                    thing.x+=move
                else:
                    move = random.randint(-3, -1)*10
                    thing.x+=move
                    Invader.findnew = False
            elif 10 > thing.x:
                if move > 0:
                    thing.x+=move
                else:
                    move = random.randint(1,3)*10
                    thing.x+=move
            else:
                thing.x+=move
                   
class Bullet:
    
    bullets = []
    
    def __init__(self, x, y, speed, direc, image=image4):
        self.x = x
        self.y = y
        self.speed = speed
        self.direction = direc
        self.image = image
        Bullet.bullets.append(self)
    def show(self):
        screen.blit(self.image, (self.x, self.y))
    def move(self):
        if self.direction == "up":
            self.y-=self.speed
        else:
            self.y+=self.speed
    def __del__(self):
        Bullet.bullets.remove(self)

class User:
    def __init__(self, speed, pic=image5):
        self.x = 10
        self.y = 850
        speed = speed
        self.pic=pic
    def shoot(self):
        bullet = Bullet(self.x+45, self.y, 15, "up", image6)
    def display(self):
        screen.blit(self.pic, (self.x, self.y))
    def move(self, direction):
        if direction == "right":
            if self.x < 850:
                self.x+=5
        else:
            if self.x > 0:
                self.x-=5

user = User(15)

a = 200
b = 10
image = image1
thelist = Invader.invader1

for i in range(0, 3):
    for i2 in range(0, 5):
        invader = Invader(a, b, image, thelist)
        if thelist == Invader.invader1:
            Invader.invader1.append(invader)
        elif thelist == Invader.invader2:
            Invader.invader2.append(invader)
        elif thelist == Invader.invader3:
            Invader.invader3.append(invader)
        a+=90
    a = 200
    b+=90
    if image == image1:
        image = image2
        thelist = Invader.invader2
    elif image == image2:
        image = image3
        thelist = Invader.invader3
    elif image == image3:
        image = image1

d = time.time()

while True:
    
    e = time.time()
    
    if 0.295 < e - d > 0.305:
        Invader.moveall()
        
    if 0.395 < e - d > 0.405:
        if len(Invader.invader3) > 0:
            Invader.moveall()
            shooting = random.choice(Invader.invader3)
            bullet = Bullet(shooting.x+30, shooting.y+30, 10, "down")   
        elif len(Invader.invader2) > 0:
            Invader.moveall()
            shooting = random.choice(Invader.invader2)
            bullet = Bullet(shooting.x+30, shooting.y+30, 10, "down")
        elif len(Invader.invader1) > 0:
            Invader.moveall()
            shooting = random.choice(Invader.invader1)
            bullet = Bullet(shooting.x+30, shooting.y+30, 10, "down")
        
        d = time.time()
    
    for thing in Invader.invader1:
        thing.display()
        for thing2 in Bullet.bullets:
            if thing.x < thing2.x < thing.x+80:
                if thing.y < thing2.y < thing.y+80:
                    del thing
            
    for thing in Invader.invader2:
        thing.display()
            
    for thing in Invader.invader3:
        thing.display()
    
    for t2 in Bullet.bullets:
        t2.move()
        t2.show()
        if t2.y > 599:
            del t2
            
    user.display()
        
    if len(Invader.invader1)+len(Invader.invader2)+len(Invader.invader3) == 0:
        screen.fill(nblue)
        time.sleep(3)
        pygame.quit()
        sys.exit()
    
    pygame.display.update()
    screen.fill(nblue)
    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()
            sys.exit()
        if event.type == KEYDOWN:
            if event.key == K_SPACE:
                user.shoot()
            if event.key == K_UP:
                user.shoot()
    key = pygame.key.get_pressed()
    if key[K_LEFT]:
        user.move("left")
    if key[K_RIGHT]:
        user.move("right")
Sorry that the code is a bit long. Look at lines 192-200.

User avatar
jojopi
Posts: 3135
Joined: Tue Oct 11, 2011 8:38 pm

Re: Classes and For Loops

Wed Jan 08, 2020 1:56 pm

The error message is because the loops try to check the rest of the bullets against the thing even after we have done "del thing", so thing is now undefined. This is easily fixed by adding a "break" statement after "del thing". That terminates the inner loop early, skipping the rest of the bullets and going on to the next invader.

The next problem is that "del thing" only actually deletes the loop variable; it does not remove the dead invader from the list. What we really want is "Invader.invader1.remove(thing)".

However, I do not think it is allowed in Python to add or remove items from a list inside a for loop that is iterating over that list. The for loop could become confused about which items it has already processed. The most general solution is to a make a temporary list and assign it back afterwards:

Code: Select all

    notdead = Invader.invader1[:]
    for vader in Invader.invader1:
        vader.display()
        for proj in Bullet.bullets:
            if vader.x < proj.x < vader.x+80:
                if vader.y < proj.y < vader.y+80:
                    notdead.remove(vader)
                    break
    Invader.invader1 = notdead

User avatar
Zilla707
Posts: 83
Joined: Fri Aug 23, 2019 11:04 pm

Re: Classes and For Loops

Thu Jan 09, 2020 3:09 am

I think you can remove an item in a list while looping through it.

Code: Select all

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for number in numbers:
	if number == 5:
		numbers.remove(number)
		
print(numbers)
> [0, 1, 2, 3, 4, 6, 7, 8, 9]
So you should be able to remove the invader inside the for loop.
Aim for perfect and you'll hit somewhere near pretty good. (maybe...)
A quick wit is best followed by quick reflexes. (and a Band-Aid...)

User avatar
jojopi
Posts: 3135
Joined: Tue Oct 11, 2011 8:38 pm

Re: Classes and For Loops

Thu Jan 09, 2020 4:20 am

No, you really should not.

Code: Select all

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for number in numbers:
    if 4 <= number <= 6:
        numbers.remove(number)

print(numbers)
[0, 1, 2, 3, 5, 7, 8, 9]
Pesky five!

User avatar
Zilla707
Posts: 83
Joined: Fri Aug 23, 2019 11:04 pm

Re: Classes and For Loops

Thu Jan 09, 2020 10:32 pm

Ok, so perhaps only if you had to remove one invader. Or maybe make you if statement more specific. But I agree that, while I have never had any serious errors or bugs when doing stuff like this, it is probably best to make a copy of the list or something like that.
Aim for perfect and you'll hit somewhere near pretty good. (maybe...)
A quick wit is best followed by quick reflexes. (and a Band-Aid...)

User avatar
paddyg
Posts: 2464
Joined: Sat Jan 28, 2012 11:57 am
Location: UK

Re: Classes and For Loops

Thu Jan 09, 2020 11:56 pm

You can start at the other end of the list

Code: Select all

for number in numbers[::-1]:
and it *seems* to behave. However I'm not sure I would trust it in all circumstance!
also https://groups.google.com/forum/?hl=en-GB&fromgroups=#!forum/pi3d

KetZoom
Posts: 6
Joined: Wed Jan 08, 2020 6:14 am

Re: Classes and For Loops

Fri Jan 10, 2020 1:06 am

Thank you to everyone that replied. I changed my code and the bullets work!

User avatar
Paeryn
Posts: 2807
Joined: Wed Nov 23, 2011 1:10 am
Location: Sheffield, England

Re: Classes and For Loops

Fri Jan 10, 2020 5:45 am

paddyg wrote:
Thu Jan 09, 2020 11:56 pm
You can start at the other end of the list

Code: Select all

for number in numbers[::-1]:
and it *seems* to behave. However I'm not sure I would trust it in all circumstance!
Doing that is creating a copy of the list (but in reverse order) which the iterator traverses so isn't affected by any deletions to the original list, it would work equally well if you just made a copy

Code: Select all

for number in numbers[:]:
You are right in that deleting from a list whilst you are iterating over said list then you need to start at the end and work towards the start (so that removing an element won't affect the iteration). Also list.remove() is slow in that it searches the list for the first occurrence of the item to remove and then has to move all the items after it down (so effectively it has to traverse the entire list to remove one item).

Better would be to enumerate a reversed iterator (no intermediate list created) and del the item (deleting an element of a list will just incur the cost of moving all the items above it down). There's a quicker way of deleting that doesn't incur that cost but it doesn't preserve the order of the list.

Using the more or less the original code,

Code: Select all

original_size = len(Invader.invader1) - 1
for index, vader in enumerate(reversed(Invader.invader1):
    vader.display()
    for proj in Bullet.bullets:
        if vader.x < proj.x < vader.x+80 and vader.y < proj.y < vader.y+80:
            del Invader.invader1[original_size - index]
            break
She who travels light — forgot something.

User avatar
Zilla707
Posts: 83
Joined: Fri Aug 23, 2019 11:04 pm

Re: Classes and For Loops

Fri Jan 10, 2020 8:42 pm

Glad you got it working @KetZoom!
Aim for perfect and you'll hit somewhere near pretty good. (maybe...)
A quick wit is best followed by quick reflexes. (and a Band-Aid...)

User avatar
paddyg
Posts: 2464
Joined: Sat Jan 28, 2012 11:57 am
Location: UK

Re: Classes and For Loops

Sat Jan 11, 2020 11:06 am

Just for completeness, and to follow up on @Paeyrn's very good point about copying lists with [:] (which I admit I hadn't really thought about). It occurred to me that the tidiest way to do what the OP intended might be to mark instances of Invader and Bullet with a dead flag, and not attempt to delete them in the main loop - but remake the list with a comprehension every now and then (or have a class level to_tidy flag). Generally list comprehensions are blindingly fast:

Code: Select all

Invader.invader1 = [x for x in Invader.invader1 if not x.dead]
but before posting I thought I would test what the difference was. (see code at the bottom). With very large invading armies (millions of beasts) with a high dead percentage, the list comprehension does work faster. However at any normal size with only one or two to cull, I was very surprised to find that the remove() method is best! Not looked to see what tricks python internals are performing.

Code: Select all

#pop, remove, del, comprehension
#100 3.7us, 5.0us, 5.1us (10% dead)
#1,000 35us, 61us, 49us (1% dead)
#10,000 0.39ms, 0.57ms, 0.47ms (0.1% dead) 
#100,000 5.2ms, 6.5ms, 5.4ms
#1,000,000 700ms, 99ms, 55ms

Code: Select all

import timeit

setup = '''
import random

class Invader:
    population = []
    def __init__(self):
        self.dead = True if random.random() < 0.01 else False

Invader.population = [Invader() for _i in range(1000)]
'''

funcs = ['''#1
for invader in Invader.population[::]:
    if invader.dead:
        Invader.population.remove(invader)
''', '''#2
len_pop = len(Invader.population) - 1
for i, invader in enumerate(reversed(Invader.population)):
    if invader.dead:
        del Invader.population[len_pop - i]
''', '''#3
Invader.population = [x for x in Invader.population if not x.dead]
''']

for f in funcs:
    print(timeit.timeit(f, setup, number=1000))
also https://groups.google.com/forum/?hl=en-GB&fromgroups=#!forum/pi3d

Return to “Python”