Python Asteroids on GitHub

What if there was some sort of Position object?

In the previous article I was mentioning the common moving code:

class Ship(Flyer):
    def __init__(self, position):
        super().__init__()
        self.position = position.copy()
        self.velocity = Vector2(0, 0)
        ...

    def move(self, delta_time, _ships):
        position = self.position + self.velocity * delta_time
        position.x = position.x % u.SCREEN_SIZE
        position.y = position.y % u.SCREEN_SIZE
        self.position = position

I wonder whether there could be a handy little object to do this job for us.

Aside
I should mention the Flyer inheritance there. That’s a thing we were trying in the Zoom meeting, to see whether we could induce PyCharm to do a Change Signature refactoring correctly. Giving the classes a common superclass did help. I don’t intend for the classes to inherit from anyone, so I’ll probably remove that soon. Not worth writing home about.

Let’s TDD up a MovablePosition object.

class TestMovablePosition:
    def test_creation(self):
        position = Vector2(0, 0)
        velocity = Vector2(100, 200)
        mp = MovablePosition(position, velocity)


class MovablePosition:
    def __init__(self, position, velocity):
        self.position = position
        self.velocity = velocity

Green. Commit: Initial MovablePosition class.

class TestMovablePosition:
    def test_motion(self):
        position = Vector2(0, 0)
        velocity = Vector2(100, 200)
        mp = MovablePosition(position, velocity)
        mp.move(0.25)
        assert mp.position == Vector2(25, 50)

class MovablePosition:
    def move(self, delta_time):
        self.position += self.velocity*delta_time

Green. Commit: initial move method.

    def test_motion_wraps(self):
        position = Vector2(990, 990)
        velocity = Vector2(100, 200)
        mp = MovablePosition(position, velocity, 1000)
        mp.move(0.25)
        assert mp.position == Vector2(15, 40)

I’m positing that you can provide the world size with a vector parameter. I also plan to default it to u.CENTER.

class MovablePosition:
    def __init__(self, position, velocity, size=u.SCREEN_SIZE):
        self.position = position
        self.velocity = velocity
        self.size = size

    def move(self, delta_time):
        position = self.position + self.velocity*delta_time
        position.x = position.x % self.size
        position.y = position.y % self.size
        self.position = position

I’ll trust the default to work, I guess.

Commit: move implemented with wrap.

I think that’s all we need for an object like Asteroid to use one of these things. Let’s see how it might plug in.

class Asteroid(Flyer):
    def __init__(self, size=2, position=None):
        super().__init__()
        self.size = size
        if self.size not in [0, 1, 2]:
            self.size = 2
        self.radius = [16, 32, 64][self.size]
        self.position = position if position is not None else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        self.velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.offset = Vector2(self.radius, self.radius)
        self.surface = SurfaceMaker.asteroid_surface(self.radius * 2)

Here we’ll need to make position and velocity temps and then set a position object after we compute them both.

There are a few references to self.position that want to be the Vector2. Let’s rename our class to movable location. Then we can have a self.location for the object and a property for self.position.

Let’s just plug it in and see if we can make it work.

class Asteroid(Flyer):
    def __init__(self, size=2, position=None):
        super().__init__()
        self.size = size
        if self.size not in [0, 1, 2]:
            self.size = 2
        self.radius = [16, 32, 64][self.size]
        position = position if position is not None else Vector2(0, random.randrange(0, u.SCREEN_SIZE))
        angle_of_travel = random.randint(0, 360)
        velocity = u.ASTEROID_SPEED.rotate(angle_of_travel)
        self.location = MovableLocation(position, velocity)
        self.offset = Vector2(self.radius, self.radius)
        self.surface = SurfaceMaker.asteroid_surface(self.radius * 2)

This breaks some things. Let’s make properties for position and velocity:

    @property
    def position(self):
        return self.location.position

    @position.setter
    def position(self, position):
        self.location.position = position

    @property
    def velocity(self):
        return self.location.velocity

The tests are all running. Of course, we aren’t using the thing yet. But:

    def move(self, delta_time, _asteroids):
        position = self.position + self.velocity * delta_time
        position.x = position.x % u.SCREEN_SIZE
        position.y = position.y % u.SCREEN_SIZE
        self.position = position

Becomes:

    def move(self, delta_time, _asteroids):
        self.location.move(delta_time)

And we are green and the game is good. Commit: Asteroid moves via MovableLocation object, position and velocity are virtual.

Summary

That went nicely, didn’t it? Using MovableLocation in our object inits should reduce the duplication of those lines in move. We’ll need some more operations on the class, I think, but it seems like a useful improvement so far.

We’ll want to take a look at our uses of position and velocity, to see if we can get by without the properties. At least some of them can probably be handled by adding distance methods and such to MovableLocation. We’ll see. Maybe tomorrow when I’m more fresh.

See you next time!