Python Asteroids on GitHub

I learned something new, and apply it. Also I have an idea of my own. Will wonders never cease!?!?

Yesterday I found mathspp.com, the site of Rodrigo Girão Serrão from Portugal. Rodrigo’s blog is excellent and his series of articles entitled “Pydon’ts” is particularly good. I’ve already learned some useful things and have bought him a pizza. Thanks, Rodrigo! So …

Yesterday I learned …

One of the articles I read was Truthy, Falsy, and bool. The first thing I learned was that when Python goes to decide whether an object is Truthy or Falsy, it does so first by trying to call bool to see whether the object implements __bool__, and then calling len to see if it implements __len__. If either of those returns a True/False, it is used. Otherwise the object is Truthy.

I was looking for information on __bool__ because in earlier versions of Asteroids, before I had my own Fleet collections, I had used if saucers on a raw list of saucers to determine whether there were any saucers, because Python treats empty lists and sequence as Falsy. When I first did Fleet, I didn’t know about bool, but I did know about len, so I implemented __len__:

class Fleet:
    def __len__(self):
        return len(self.flyers)

That allowed me to write, e.g.:

class ShipFleet(Fleet):
    def tick(self, delta_time, fleets):
        ships = fleets.ships
        if len(ships) == 0:
            self.ship_timer.tick(delta_time, fleets)
        super().tick(delta_time, fleets)
        return True

What I did not know was that implementing len automatically adds in the truthy check, allowing this:

    def tick(self, delta_time, fleets):
        ships = fleets.ships
        if not ships:
            self.ship_timer.tick(delta_time, fleets)
        super().tick(delta_time, fleets)
        return True

Or even this:

    def tick(self, delta_time, fleets):
        if not fleets.ships:
            self.ship_timer.tick(delta_time, fleets)
        super().tick(delta_time, fleets)
        return True

Just a bit nicer. Of course it will be even nicer, I think, if we implement bool by implementing __bool__:

    def __bool__(self):
        return bool(self.flyers)

There are many ways I could implement this, checking len > 0 etc etc, but I think I like this one best.

Now that I’ve done that, I’ll change some other calls to len and then commit.

    def safe_to_emerge(self):
        if self.missiles:
            return False
        if self.saucer_missiles:
            return False
        return self.all_asteroids_are_away_from_center()

Commit: implement bool on Fleet and use it throughout.

Reflection

Is this better? In think that in Python terms, it definitely is. In other languages, empty collections generally do not turn out to be Falsy, but Python is consistent about “useless” values being Falsy, and so the usage is pythonic, as they say.

Is it “efficient”? Well, it’s as efficient as anything is likely to be. We could implement is_empty on our Fleet, maybe like this:

    def is_empty(self):
    	return len(self.flyers) > 0

But does that save even one lookup? I’m not sure. Better to program in the Python style, I think.

So, thanks again to Rodrigo. I expect to learn more good things as I cruise through his blog and Pydon’ts.

An Idea

I was thinking about the specialized Fleet classes that we already have, AsteroidFleet, SaucerFleet, and ShipFleet. It seemed to me to be “interesting” that we do not also have MissileFleet, since we have all the others. And that got me thinking about what we do with missiles and how a MissileFleet class might be helpful.

There are two special rules for missiles. A ship can have no more than four missiles flying at once, and a saucer can have only two. Ship does this:

class Ship:
    def fire_if_possible(self, missiles):
        if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
            missiles.append(Missile.from_ship(self.missile_start(), self.missile_velocity()))
            self.can_fire = False

Saucer does it a bit differently:

class Saucer:
    def fire_if_missile_available(self, saucer_missiles, ships):
        if self.a_missile_is_available(saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)
            return True
        else:
            return False

    @staticmethod
    def a_missile_is_available(saucer_missiles):
        return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT

    def fire_a_missile(self, saucer_missiles, ships):
        saucer_missiles.append(self.create_missile(ships))

    def create_missile(self, ships):
        should_target = random.random()
        random_angle = random.random()
        return self.suitable_missile(should_target, random_angle, ships)
    ...

Saucer goes on, since it can target missiles and whatnot, but that’s enough for now.

Both of these implementations check to see whether their missile collection has room for another missile, and if so, they create a missile and append it to the missile collection.

It seems that the collection could be more helpful. Suppose that …

  1. MissileFleet (a new class) knew a maximum number of missiles it could contain, and
  2. it had a method fire_if_possible that took a callback, and
  3. the callback created and returned a missile, and
  4. the Fleet appended the missile.

It seems to me that this scheme would have some advantages. Only the MissileFleet would need to know about the limit. Limit checking would be removed from Ship and Saucer, as would appending the missile. Ship and Sauce could just concern themselves with creating a suitable missile, and that method would only be called when the missile was about to be put into use.

I think it’ll be better. I’m gonna try it. And I’m gonna TDD the object.

    def test_missile_fleet(self):
        fleet = MissileFleet([])

That’s enough to drive out the class:

class MissileFleet(Fleet):
    def __init__(self, flyers):
        super().__init__(flyers)

However, I want to have a maximum number of missiles. I make the test more aggressive:

    def test_missile_fleet(self):
        missiles = []
        fleet = MissileFleet(missiles, 3)
        fired = fleet.fire(lambda:  666 )
        assert fired
        assert len(missiles) == 1
        assert missiles[0] == 666

And I implement a bit more than I need:

class MissileFleet(Fleet):
    def __init__(self, flyers, maximum_number_of_missiles):
        self.maximum_number_of_missiles = maximum_number_of_missiles
        super().__init__(flyers)

    def fire(self, callback):
        if len(self) < self.maximum_number_of_missiles:
            self.append(callback())
            return True
        else:
            return False

And I complete the test:

    def test_missile_fleet(self):
        missiles = []
        fleet = MissileFleet(missiles, 3)
        fired = fleet.fire(lambda:  666 )
        assert fired
        assert len(missiles) == 1
        assert missiles[-1] == 666

        fired = fleet.fire(lambda:  777 )
        assert fired
        assert len(missiles) == 2
        assert missiles[-1] == 777

        fired = fleet.fire(lambda:  888 )
        assert fired
        assert len(missiles) == 3
        assert missiles[-1] == 888

        fired = fleet.fire(lambda:  999 )
        assert not fired
        assert len(missiles) == 3

It fires three, appends the returned values, and returns True when it fires and False when it doesn’t. Perfect. Commit: TDD MissileFleet with maximum number of missiles, and fire method with callback.

Now let’s use the class in the game.

As things stand, we create Fleets like this:

class Game:
    # noinspection PyAttributeOutsideInit
    def init_fleets(self):
        asteroids = []
        missiles = []
        saucers = []
        saucer_missiles = []
        self.ship = Ship(pygame.Vector2(u.SCREEN_SIZE / 2, u.SCREEN_SIZE / 2))
        ships = []
        self.fleets = Fleets(asteroids, missiles, saucers, saucer_missiles, ships)

We’re passing in empty collections, and then Fleets tucks them away:

class Fleets:
    def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
        self.fleets = (
        	AsteroidFleet(asteroids), 
        	Fleet(missiles), 
        	SaucerFleet(saucers), 
        	Fleet(saucer_missiles), 
        	ShipFleet(ships))

So we can just change that:

class Fleets:
    def __init__(self, asteroids, missiles, saucers, saucer_missiles, ships):
        self.fleets = (
        	AsteroidFleet(asteroids), 
        	MissileFleet(missiles, u.MISSILE_LIMIT), 
        	SaucerFleet(saucers), 
        	MissileFleet(saucer_missiles, u.SAUCER_MISSILE_LIMIT), 
        	ShipFleet(ships))

Commit: Missile Fleets now use MissileFleet class with proper limit set.

Now let’s see about using this new facility.

I think Ship will be easier.

    def fire_if_possible(self, missiles):
        if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
            missiles.append(Missile.from_ship(self.missile_start(), self.missile_velocity()))
            self.can_fire = False

Let’s extract a method first:

    def fire_if_possible(self, missiles):
        if self.can_fire and len(missiles) < u.MISSILE_LIMIT:
            missiles.append(self.create_missile())
            self.can_fire = False

    def create_missile(self):
        return Missile.from_ship(self.missile_start(), self.missile_velocity())

Now can’t we just say this:

    def fire_if_possible(self, missiles):
        if self.can_fire and missiles.fire(self.create_missile):
            self.can_fire = False

Two tests fail, but the game works. The first test says:

    def test_missile_timeout(self):
        ship = Ship(Vector2(100,100))
        missiles = []
        ship.fire_if_possible(missiles)
        assert len(missiles) == 1
        missile = missiles[0]
        missile.tick_timer(0.5, missiles)
        assert len(missiles) == 1
        missile.tick_timer(3.0, missiles)
        assert len(missiles) == 0

I think this will help:

    def test_missile_timeout(self):
        ship = Ship(Vector2(100,100))
        missiles = MissileFleet([], 4)
        ship.fire_if_possible(missiles)
        assert len(missiles) == 1
        missile = missiles[0]
        missile.tick_timer(0.5, missiles)
        assert len(missiles) == 1
        missile.tick_timer(3.0, missiles)
        assert len(missiles) == 0

That passes, and it’s a better test since it was passing in a raw collection, no longer suitable.

    def test_firing_limit(self):
        ship = Ship(u.CENTER)
        count = 0
        missiles = []
        while len(missiles) < u.MISSILE_LIMIT:
            ship.can_fire = True
            ship.fire_if_possible(missiles)
            count += 1
            assert len(missiles) == count
        assert len(missiles) == u.MISSILE_LIMIT
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == u.MISSILE_LIMIT

I think the same fix should work. Yes:

    def test_firing_limit(self):
        ship = Ship(u.CENTER)
        count = 0
        missiles = MissileFleet([], u.MISSILE_LIMIT)
        while len(missiles) < u.MISSILE_LIMIT:
            ship.can_fire = True
            ship.fire_if_possible(missiles)
            count += 1
            assert len(missiles) == count
        assert len(missiles) == u.MISSILE_LIMIT
        ship.can_fire = True
        ship.fire_if_possible(missiles)
        assert len(missiles) == u.MISSILE_LIMIT

Much improved. Commit: Ship now uses MissileFleet.fire to fire missiles.

Let’s see whether there’s anything unused in Ship now. I think not. Right. The can_fire flag handling is a bit odd, though:

    def control_motion(self, delta_time, missiles):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_f]:
            self.turn_left(delta_time)
        if keys[pygame.K_d]:
            self.turn_right(delta_time)
        if keys[pygame.K_j]:
            self.power_on(delta_time)
        else:
            self.power_off()
        if keys[pygame.K_k]:
            self.fire_if_possible(missiles)
        else:
            self.not_firing()

    def not_firing(self):
        self.can_fire = True

    def fire_if_possible(self, missiles):
        if self.can_fire and missiles.fire(self.create_missile):
            self.can_fire = False

I think we should either all set the can_fire flag directly, or all set it indirectly. I choose directly, and remove the not_firing method.

    def control_motion(self, delta_time, missiles):
        if not pygame.get_init():
            return
        keys = pygame.key.get_pressed()
        if keys[pygame.K_f]:
            self.turn_left(delta_time)
        if keys[pygame.K_d]:
            self.turn_right(delta_time)
        if keys[pygame.K_j]:
            self.power_on(delta_time)
        else:
            self.power_off()
        if keys[pygame.K_k]:
            self.fire_if_possible(missiles)
        else:
            self.can_fire = True

Commit: remove not_firing, set can_fire flag directly. Questionable?

That went smoothly. Let’s look at Saucer. When the saucer timer ticks down, it does this:

    def set_firing_timer(self):
        self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available)

    def fire_if_missile_available(self, saucer_missiles, ships):
        if self.a_missile_is_available(saucer_missiles):
            self.fire_a_missile(saucer_missiles, ships)
            return True
        else:
            return False

    @staticmethod
    def a_missile_is_available(saucer_missiles):
        return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT

    def fire_a_missile(self, saucer_missiles, ships):
        saucer_missiles.append(self.create_missile(ships))

It seems that we can almost do this:

    def fire_if_missile_available(self, saucer_missiles, ships):
        return saucer_missiles.fire(self.create_missile)

The problem is that we need ships in create_missile. So we need to pass an additional parameter to fire. Or any number? I’ll change the code to what needs to work:

    def fire_if_missile_available(self, saucer_missiles, ships):
        return saucer_missiles.fire(self.create_missile, ships)

Now I do have a test failing so let’s fix that, until we get the error I expect:

    def test_can_only_fire_two(self):
        saucer = Saucer()
        saucer_missiles = MissileFleet([], u.SAUCER_MISSILE_LIMIT)
        saucer.fire_if_possible(delta_time=0.1, saucer_missiles=saucer_missiles, ships=[])
        assert not saucer_missiles
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 1
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2
        saucer.fire_if_possible(u.SAUCER_MISSILE_DELAY, saucer_missiles=saucer_missiles, ships=[])
        assert len(saucer_missiles) == 2

I put in the reference to MissileFleet, and now the failure is:

    def fire_if_missile_available(self, saucer_missiles, ships):
>       return saucer_missiles.fire(self.create_missile, ships)
E       TypeError: MissileFleet.fire() takes 2 positional arguments but 3 were given

That’s the error I want. Let’s add a direct test for the parameter.

    def test_missile_fleet_parameter(self):
        missiles = []
        fleet = MissileFleet(missiles, 2)
        fired = fleet.fire(lambda m: m*2, 333)
        assert fired
        assert len(missiles) == 1
        assert missiles[-1] == 666

This version of fire is taking a parameter and multiplying it by 2 in the function. Test passes, because fire is now:

    def fire(self, callback, *args):
        if len(self) < self.maximum_number_of_missiles:
            self.append(callback(*args))
            return True
        else:
            return False

And the other test runs as well. Game also looks good.

Let’s see if we can clean up anything extra from Saucer. Yes. These are unused:

    @staticmethod
    def a_missile_is_available(saucer_missiles):
        return len(saucer_missiles) < u.SAUCER_MISSILE_LIMIT

    def fire_a_missile(self, saucer_missiles, ships):
        saucer_missiles.append(self.create_missile(ships))

The job of deciding to fire is now down to ticking the timer and asking the saucer_missiles to call us if we can fire:

    def set_firing_timer(self):
        self.missile_timer = Timer(u.SAUCER_MISSILE_DELAY, self.fire_if_missile_available) 

    def fire_if_possible(self, delta_time, saucer_missiles, ships):
        self.missile_timer.tick(delta_time, saucer_missiles, ships)

    def fire_if_missile_available(self, saucer_missiles, ships):
        return saucer_missiles.fire(self.create_missile, ships)

    def create_missile(self, ships):
        should_target = random.random()
        random_angle = random.random()
        return self.suitable_missile(should_target, random_angle, ships)

The create_missile and suitable_missile methods are still carrying weight, since they decide whether to aim or not, and so on.

Reflection

Saucer concerns itself only with wanting to fire and trying to fire until firing returns True, resetting the timer, and with creating a suitable missile to fire when the time comes.

What is a bit obscure, I think, is that fire_if_possible must return True if it fires and False if it does not, because the timer will only reset if we report success back.

Maybe we should annotate some of these methods:

    def fire_if_missile_available(self, saucer_missiles, ships) -> bool:
        return saucer_missiles.fire(self.create_missile, ships)

    def fire(self, callback, *args) -> bool:
        if len(self) < self.maximum_number_of_missiles:
            self.append(callback(*args))
            return True
        else:
            return False

I’d like to specify the callable in Timer but I can’t work out how to say that action is a Callable with a return of None or bool. I’ve got this much:

class Timer:
    def __init__(self, delay, action: Callable, *args):
        self.delay = delay
        self.action = action
        self.args = args
        self.elapsed = 0

A comment might be useful. I’ll do this:

class Timer:
    def __init__(self, delay, action: Callable, *args):
        """action is callable returning None or bool"""
        self.delay = delay
        self.action = action
        self.args = args
        self.elapsed = 0

Better. I don’t generally comment much, but the docstring feature is pretty nice and perhaps useful on somewhat tricky classes like Timer.

After more study, I get this:

class Timer:
    def __init__(self, delay, action: Callable[[...], Union[None, bool]], *args):
        """action is callable returning None or bool"""
        self.delay = delay
        self.action = action
        self.args = args
        self.elapsed = 0

That mess:

Callable[[...], Union[None, bool]]

I think it means that action is callable with a list of arguments, returning either None or bool. I’ll leave it that way until it causes me trouble, which it probably never will because Python doesn’t check these and PyCharm will be nice about it if it checks. I don’t know if it does or not.

I’d call that semi-useful. The comment is more useful.

Commit: annotate a few returns around timers and firing.

Let’s sum up.

Summary

Learning a bit more about Truthy, Falsy, len and bool let me simplify some code.

More important, once again, creating a container class, MissileFleet, specific to fleets of missiles, paid off. It let us add a limit to the size of the collection to the MissileFleet class, and that let us move determination of whether to fire or not inside the MissileFleet, and by giving the fire method a callback, we simplified both Ship and Saucer quite nicely.

We observe that callbacks, such as the one in MissileFleet.fire and the one in Timer, are a bit less obvious than most code, and we tried annotating the methods with returns, to help with that. We might also do this:

    def create_missile(self, ships):
        """callback method, called from saucer_missiles.fire"""
        should_target = random.random()
        random_angle = random.random()
        return self.suitable_missile(should_target, random_angle, ships)

Can’t hurt, might help. Commit: add docstring comment.

All this polishing is paying off in two regards. It’s making the Asteroids program more like an object-oriented program should be, at least by my standards, which prefer more little objects and carefully-divided responsibilities. That’s not to say that I always accomplish that, but it is what I prefer.

But it’s also paying off by helping me learn Python in more depth. Python is sufficiently like Ruby or C or Lua or a host of other languages to allow me to program well enough while knowing only the rudiments of the language. But the more I learn, the more my code will look “pythonic”, and, since the language is designed to support that kind of code, things will go more smoothly.

It’s fun. I like learning, on those rare occasions where it happens. Yesterday and today were two of those occasions.

Do you have Python learning ideas for me? Toot me up. Or tweet, I’m still hanging on in Twitter by one fingernail.

See you next time!