Python Asteroids on GitHub

My browser was open to the random page, and for a moment, my mind was open too.

I found this:

random.choice(seq)
Return a random element from the non-empty sequence seq. If seq is empty, raises IndexError.

So that should save us some code and a test. We have this:

    def __init__(self, position=None, size=2):
        self.position = position if position is not None else u.CENTER
        self.size = size
        self.velocity = u.SAUCER_VELOCITY
        self.directions = [self.velocity.rotate(45), self.velocity, self.velocity, self.velocity.rotate(-45)]
        ...

    def check_zigzag(self, delta_time):
        self.zig_timer -= delta_time
        if self.zig_timer <= 0:
            self.zig_timer = u.SAUCER_ZIG_TIME
            rand = random.randint(0, 3)
            self.velocity = self.new_direction(rand)*self.direction

    def new_direction(self, index):
        return self.directions[index % 4]

Seems like we could do this:

    ...
        self.directions = (self.velocity.rotate(45), self.velocity, self.velocity, self.velocity.rotate(-45))

    def check_zigzag(self, delta_time):
        self.zig_timer -= delta_time
        if self.zig_timer <= 0:
            self.zig_timer = u.SAUCER_ZIG_TIME
            self.velocity = self.new_direction()*self.direction


    def new_direction(self):
        return random.choice(self.directions)

That works fine but breaks two tests. I expected one of them, this:

    def test_zig_zag_directions(self):
        saucer = Saucer()
        straight = u.SAUCER_VELOCITY
        up = straight.rotate(45)
        down = straight.rotate(-45)
        assert saucer.new_direction(0) == up
        assert saucer.new_direction(1) == straight
        assert saucer.new_direction(2) == straight
        assert saucer.new_direction(3) == down
        assert saucer.new_direction(4) == up
        assert saucer.new_direction(5) == straight
        assert saucer.new_direction(-1) == down

I did not expect this one to fail:

    def test_vanish_at_edge(self):
        saucer = Saucer()
        saucers = [saucer]
        saucer.ready()
        saucer.move(1, saucers)
        assert saucers
        saucer.move(u.SCREEN_SIZE/u.SAUCER_VELOCITY.x, saucers)
        assert saucer.position.x > u.SCREEN_SIZE
        assert not saucers

The error is:

>       assert saucer.position.x > u.SCREEN_SIZE
E       assert 649.1240251292506 > 768
E        +  where 649.1240251292506 = <Vector2(649.124, 786.124)>.x
E        +    where <Vector2(649.124, 786.124)> = <saucer.Saucer object at 0x1023e1e90>.position
E        +  and   768 = u.SCREEN_SIZE

What happened, of course, is that the first move changed the velocity, so the saucer was moving at an angle, and didn’t reach the edge because, after all, it’s going slower in the x direction now.

How can we fix this test? Maybe we should loop, in smaller steps, until we find its x position greater than screen size and then expect saucers to be empty.

Let’s check the move code to see if that works:

    def move(self, delta_time, saucers):
        self.check_zigzag(delta_time)
        self.position += delta_time*self.velocity
        x = self.position.x
        if x < 0 or x > u.SCREEN_SIZE:
            if self in saucers:
                saucers.remove(self)

Why did I check the code, rather than just write the test? Because I wanted to be sure that we didn’t bring the coordinate back into range. Probably I should have just written the test, like this:

I settle for this test:

    def test_vanish_at_edge(self):
        saucer = Saucer()
        saucers = [saucer]
        saucer.ready()
        saucer.move(1, saucers)
        assert saucers
        saucer.move(u.SCREEN_SIZE/u.SAUCER_VELOCITY.x, saucers)
        saucer.move(u.SCREEN_SIZE/u.SAUCER_VELOCITY.x, saucers)
        assert saucer.position.x > u.SCREEN_SIZE
        assert not saucers

Duplicating the line certainly ensures that we’ll be outside the boundary. However, the reason for doing it isn’t clear at all. Instead of that, I go hog-wild:

    def test_vanish_at_edge(self):
        saucer = Saucer()
        saucers = [saucer]
        saucer.ready()
        saucer.move(1, saucers)
        assert saucers
        distance_to_edge = u.SCREEN_SIZE - saucer.position.x
        slowest_possible_speed = u.SAUCER_VELOCITY.x * sin(pi/4)
        a_little_bit_more = 50
        time = (distance_to_edge + a_little_bit_more)/slowest_possible_speed
        saucer.move(time, saucers)
        assert saucer.position.x > u.SCREEN_SIZE
        assert not saucers

If that doesn’t explain what I’m doing, it should certainly scare away anyone who wants to simplify the test.

The test is a solid green now. Commit: modify saucer to use random.choice().

Still, maybe that test is too hard to figure out “why is he doing all that???”

Let’s try the loop, and if necessary, make the time large enough that it won’t slow the tests much.

    def test_vanish_at_edge(self):
        saucer = Saucer()
        saucers = [saucer]
        saucer.ready()
        saucer.move(1, saucers)
        assert saucers
        while saucer.position.x < u.SCREEN_SIZE:
            assert saucers
            saucer.move(0.1, saucers)
        assert not saucers

How’s that? I think I like it. Commit: improve test.

Summary

Better idea, random.choice. Easily done, improves code and tests.

So that’s nice. We’ll file that under old dog / new tricks.

See you next time!