Python Asteroids on GitHub

We make more small changes in the direction of independent objects interacting to make the game emerge. Let’s experiment with full interaction.

Morning Planning

I like to start my thinking by refreshing my thoughts on the longer term, the larger story, where we think we’re trying to go. I say “think we’re trying to go” because as we get closer to a goal, we often see a different goal that would be better to move toward.

Overall, our current goal is to split the Asteroids game responsibility into two main parts, a general “upper level” that knows how to run a game cycle, and a collection of “lower level” objects that collaborate to create a particular game. The particular game in our case right now, is Asteroids, but, at least in principle, we should be able to create another, very different game using the same upper level and different lower level objects. Maybe we’ll even try that. Then again, maybe we won’t. I’m not the boss of me.

Right now, we have the individual objects prepared to interact in all combinations (at least I believe we have that: we’ll test it shortly). We have scoring done at the time an object notices that it has been hit, and it’s done by passing a Score instance to the Fleets object. In the fullness of time, the Score object will move down into the mix.

Aside
This part of the design is questionable. Remind me to question it. In fact, let’s question it a bit now.

In the Kotlin version of this decentralized approach, the mix contained a ScoreKeeper that interacted with Score objects and displayed the Score when it was time to do so. In principle, the Fleets object that contains the mix could absorb the score object and display the score. In fact, right now, it does absorb the score object and accumulate the value, though the value is displayed elsewhere.

You could argue quite effectively that this is better than creating a separate ScoreKeeper object and processing it inside the mix. It’s surely more efficient if nothing else. Yes, and for just the game of Asteroids, I would have to agree. But suppose we had a game where you shot down fruit, and your score consisted of a different number of oranges, apples, and pears. In the decentralized form, we’d just have a different Score and ScoreKeeper and the central part of the game wouldn’t have to change.

Which is better? Define better. Our job as developers is to consider the issues and decide. For my purposes, decentralized is better, at least enough better that I want to build it. For the world’s tightest Asteroids program, it probably isn’t better.

Weren’t we planning?

Yes, sorry, we were. I think what I’d like to try first is to run the game with all the objects interacting with all the others, to see what happens. That will help me choose a direction for next steps.

And it might just work.

A Spike

This is a spike and I expect to throw it away. That said, I will still try to do it as neatly as I can, in case it starts looking really good.

We want to change Interactor, née Collider, so that it interacts every object with every other. Presently it interacts whole collections with other collections, all the asteroids vs all the missiles, all the ships vs all the asteroids, and so on. The code is this:

class Interactor:
    def perform_interactions(self):
        for pair in itertools.combinations(self.fleets.colliding_fleets, 2):
            self.perform_individual_interactions(pair[0], pair[1])
        return self.score

    def perform_individual_interactions(self, attackers, targets):
        for target in targets:
            for attacker in attackers:
                if self.interact_one_pair(target, targets, attacker, attackers):
                    break

    def interact_one_pair(self, target, targets, attacker, attackers):
        attacker.interact_with(target, attackers, self.fleets)
        target.interact_with(attacker, targets, self.fleets)
        return self.within_range(target, attacker)

In our spike, we want to get all combinations from a collection of all the objects that Fleets owns. Let’s assume that Fleets will know all_objects, so that we can iterate. I think we can just say this:

    def perform_interactions(self):
        for attacker, target in itertools.combinations(self.fleets.all_objects, 2):
            self.interact_one_pair(target, [], attacker, [])

The interact_one_pair expects to have the source collection for each object provided, but I think that is unused. If so, we’re still good, except that Fleets needs to provide some help.

    @property
    def all_objects(self):
        result = []
        for fleet in self.fleets:
            result.extend(fleet)
        return result

Some tests fail. The game works until we get a Fragment in there. I thought we had dealt with this problem:

  File "/Users/ron/PycharmProjects/firstGo/interactor.py", line 15, in perform_interactions
    self.interact_one_pair(target, [], attacker, [])
  File "/Users/ron/PycharmProjects/firstGo/interactor.py", line 26, in interact_one_pair
    return self.within_range(target, attacker)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/ron/PycharmProjects/firstGo/interactor.py", line 30, in within_range
    in_range = target.radius + attacker.radius
               ^^^^^^^^^^^^^
AttributeError: 'Fragment' object has no attribute 'radius'

Ah, it’s the code that is supposed to help us break out of the loop. We’re not going to support that for now. Change this:

    def interact_one_pair(self, target, targets, attacker, attackers):
        attacker.interact_with(target, attackers, self.fleets)
        target.interact_with(attacker, targets, self.fleets)
        return self.within_range(target, attacker)

Hm. Doesn’t need to return anything any more. So be it, delete that last line. We’re spiking.

Tests fail. The game works except that score is displaying ONone all the time, a hint that perhaps we aren’t receiving any score packets.

Let’s see what tests are failing. This one is:

    def test_collider(self):
        interactor = Interactor(Fleets([], [], [], [], []))
        score = interactor.perform_interactions()
        assert score == 0

Ah, I forgot to return score. My tests all pass. Test game. Everything appears to work perfectly.

I had high hopes for this working but now that it does, I’m faced with the question of whether I should delete the spike and do it over.

There is at least one issue that should be dealt with, so let’s do the right thing for once.

Roll back.

Once More, For Real This Time

OK, first thing is that I’d like to change this:

    def interact_one_pair(self, target, targets, attacker, attackers):
        attacker.interact_with(target, attackers, self.fleets)
        target.interact_with(attacker, targets, self.fleets)
        return self.within_range(target, attacker)

I’d like to remove the second parameter, which is the specific collection that the receiver is in. I think that is no longer used. And I don’t think PyCharm will be up to this refactoring because all the flyer object implement the method and it may not know what to do.

First I’ll check all the implementors to be sure they are safe.

Wait. I have the Flyer object that amounts to an interface. PyCharm seems to be able to do the refactoring for Flyer’s subclasses. Let’s be sure that all the flyers inherit Flyer.

I make sure that all of Asteroid, Missile, Saucer, Ship, and Fragment, wait I’d better do Score, inherit Flyer. PyCharm makes me call super().__init__(). Fine.

Now in Flyer:

class Flyer():
    def __init__(self):
        pass

    def interact_with(self, attacker, asteroids, fleets):
        pass

Change signature here.

    def interact_with(self, attacker, fleets):
        pass

No luck, some tests break. We’ll hunt them all down. Damn, it’s the recursive import thing. No harm done, I’ll just remove all the inheritance now. I don’t want it anyway.

My tests are green. Remove Flyer class entirely. We tried using it to get PyCharm to understand how to refactor all my flyers, and it does work for that but I’m not up for sorting out the imports to avoid recursion.

Green. Commit: remove unused collection parameter from interact_with.

Long session without a commit, but we were spiking and testing. I’m an hour in.

Now let’s test-drive the all_objects method. There must be a good way to do it in Python but I haven’t found it yet.

    def test_all_objects(self):
        fleets = Fleets([1], [2], [3], [4], [5])
        fleets.explosions.append(6)
        all_objects = fleets.all_objects
        assert len(all_objects) == 6
        assert sum(all_objects) == 21

I’m not going to fly these, so numbers should do just fine. I had to append the explosion because it’s internal to Fleets’ init.

Now:

class Fleets:
    @property
    def all_objects(self):
        result = []
        for fleet in self.fleets:
            result.extend(fleet)
        return result

Test passes. A little research suggests this:

    @property
    def all_objects(self):
        return list(chain(*self.fleets))

This also passes. Commit: test-drive Fleets.all_objects.

Now I think we’re ready to change Interactor.

class Interactor:
    def perform_interactions(self):
        for target, attacker in itertools.combinations(self.fleets.all_objects, 2):
            self.interact_one_pair(target, attacker)

    def interact_one_pair(self, target, attacker):
        attacker.interact_with(target, self.fleets)
        target.interact_with(attacker, self.fleets)

Two tests fail. I’m hoping they have old calling sequences on interact_one_pair but we’ll see.

Oh, it’s this again:

    def test_collider(self):
        interactor = Interactor(Fleets([], [], [], [], []))
        score = interactor.perform_interactions()
        assert score == 0

I forgot to return the score again. Sheesh.

    def perform_interactions(self):
        for target, attacker in itertools.combinations(self.fleets.all_objects, 2):
            self.interact_one_pair(target, attacker)
        return self.score

Green. Test the game. Perfect. Commit: Interactor now interacts all objects in pairs.

Let’s reflect on where we are and what happened.

Reflecting

A spike showed that interacting all objects would work, and highlighted some changes that were needed, including removing excess parameters from the interact_with method. We ran into issues with Score, partly because the Score object is new (and not really used as we intend), and because the process_interactions method has to return the score, which is not obvious and I forgot it twice.

We’ll let that ride, because when we do ScoreKeeper the problem will go away.

We rolled back like good little programmers and even test-drove the creation of the all_objects list. The existence of the test encouraged me to look for a better way to do the job, and I found chain.

I wonder whether we actually need to create the list in all_objects, or whether we could pass the result of chain directly into combinations, but before we’re done there will only be a single collection in Fleets, so the current inefficiency will be gone.

With this change, we have eliminated a design constraint from the system. The prior interaction code was carefully crafted to only consider pairs of objects that could be mutually destructive, so that we matched asteroids vs missiles and ships, but not asteroids vs asteroids or missiles vs missiles. The latter could be considered a problem because it would be nice if one could shoot down an incoming missile as a last resort. Now, in principle, we could implement that.

More to the point, though, the interaction logic “knew” how objects might interact. Now it does not know that and the Interactor is therefore simpler and the game is more loosely coupled.

What remains?

Probably lots remains. One thread of work will be to check to see what use is being made of the various accessors on Fleets, each of which returns a specific collection such as asteroids or explosions. We can be sure that the interaction methods are adding and removing items, and we’ll want to change how that works, as part of getting down to just one collection inside Fleets.

Idea
I wonder if we could convert Fleets to have just one Fleet, all_objects, and to return that collection through all the properties. Objects would be added or removed from that single collection even if the existing code thought it was accessing a specialized collection.

I had been thinking that I’d have to have add_asteroid, remove_asteroid and so on in Fleets. Maybe there’s a more direct way to do it.

We’ll explore that in a future session.

The other remaining work will be to tick through and replace high-level decisions like “is it time for an asteroid wave” with low-level flyer objects that deal with those issues. When we’re done with that, the Game and Fleets object should have no idea what the game actually is … and that will be a good thing.

Right now, there is specialized logic in the Fleet subclasses. That will have to be pushed down as well.

I expect this to go in quite a few more steps and for most of the steps to be pretty straightforward. Will I be surprised? Almost certainly.

For today, an excellent step in the right direction. I am pleased.

See you next time!