Python Asteroids+Invaders on GitHub

I gave abstract method a chance, for almost 24 hours. I hated changing all my classes to implement all those pass methods. Let’s try a different dispatch. Waffles?

Waffling

I’m not sure why they call opinions going back and forth “waffling”. Most of the waffles I’ve encountered were actually pretty rigid. Be that as it may, I do not like the implications of requiring all the subclasses of Flyer to implement all possible interact_with_foo methods, as there are over a dozen on the Invaders side so far, and even more on the Asteroids side.

The abstract methods have one main advantage: they ensure that every object’s programmer had at least a moment to consider whether the object should respond to that message. In practice, the programmer just lets PyCharm implement all the missing methods as pass and moves on. Tomas Aschan has suggested that there should be a solid spec for each object and that the spec should clearly state what it interacts with. And possibly, in some shop, they’re implementing games that way. But here chez Ron we just have poor old me, and I’m not going to do that, so my programming needs to accommodate my quirks and failures.

The abstract methods glom up my classes with line after line of empty methods. We were able to avoid that by allowing implementation inheritance of a simple pass method for each case. Yesterday, I reversed that decision and in twenty minutes, sprayed glom all over my classes, implementing probably over 100 empty methods spread over a dozen classes. Yesterday, I was happy with that decision for a couple of minutes and then started thinking about options.

First Principles

The cause of the problem, doctor, is that there are over a dozen different classes in SpaceInvaders, and each one interacts, in principle, with all the others, and even with other instances of that class itself. For reasons, the top level of the game considers all the objects to be simple “flyers”, and does not know or want to know their individual classes. Because we do need to know their classes when they interact, we use a “double dispatch” to convert the message x.interact_with(y) to y.interact_with_klass(x), where klass is the class of x. The downside is that every object y has to be prepared to receive every possible interact_with_klass. This is tedious, boring, and clogs up the code with null methods.

I’ve tried resolving that by making those methods non-abstract and implementing them in the superclass as pass. That works just fine and I wonder why I don’t just stick with it. The sole somewhat legitimate objections are, first, that implementation inheritance is thought by some people to be bad, and second, that it makes it possible for a needed interaction to be forgotten, resulting in a game defect. That forgetting has happened at least once.

In theory, making the methods abstract addressed the forgetting problem, but in practice, it does not. The darn programmer just implements everything as pass unless he happens to remember that a given class needs to be changed to deal with the new object.

The cost of the abstract method approach is high. Adding a new object requires editing every other Flyer in the game, whether it actually needs to interact with that object or not.

We would like to eliminate that cost.

Solutions

One possibility is just to waffle one more time and allow implementation inheritance in this one situation. It works just fine.

We explored another possibility, the notion of each object stating what it ignores. That led to some useful learning about metaclasses and decorators, but doesn’t address the need to edit all the files when a new class comes along.

Yesterday, we considered the possibility of an interface among objects that was a bit more like publish-subscribe, possibly using the Python match / case capability. Instead of sending upwards of a dozen interact_with_klass1 messages, we would send one parameterized message, perhaps something like x.engage_with("Missile", y, fleets), where the first parameter would signify what kind of object we’re dealing with.

What difference would it make? Would it be better than retaining the individual interact_with_klass methods? I have my doubts. But I am interested to find out. So we’re going to try it, using some object that has just a few interactions.

I think PlayerShot may be a good example to try. Here is its interaction code.

class PlayerShot(InvadersFlyer):
    def interact_with_bumper(self, bumper, fleets):
        pass

    def interact_with_invaderexplosion(self, explosion, fleets):
        pass

    def interact_with_invaderfleet(self, bumper, fleets):
        pass

    def interact_with_invaderplayer(self, bumper, fleets):
        pass

    def interact_with_invadershot(self, shot, fleets):
        if self.colliding(shot):
            fleets.append(ShotExplosion(self.position))
            fleets.remove(self)

    def interact_with_playerexplosion(self, _explosion, _fleets):
        pass

    def interact_with_playershot(self, bumper, fleets):
        pass

    def interact_with_shield(self, shield, fleets):
        if self.colliding(shield):
            fleets.remove(self)

    def interact_with_shotcontroller(self, controller, fleets):
        pass

    def interact_with_shotexplosion(self, bumper, fleets):
        pass

    def interact_with_topbumper(self, top_bumper, fleets):
        if top_bumper.intersecting(self.position):
            fleets.append(ShotExplosion(self.position))
            fleets.remove(self)

It’s easy to see why we’d like to get rid of those pass methods. If we keep the methods in alpha order, which lets us find them if we need them, the action is spread out. If we put the action items together, the method order is odd, although we could surely develop some standard like putting the unused ones at the bottom of the class.

I’d like to try the match / case scheme. What I’ll do, just to get a feeling for what it would look like, is posit a new method engage_with, that includes a string first parameter, which we’ll call message, and the other two parameters of an interaction, a flyer and the fleets object. Let’s just change that last interaction to call our newly posited method:

    def interact_with_topbumper(self, top_bumper, fleets):
        self.engage_with("topbumper", top_bumper, fleets)

    def engage_with(self, message, top_bumper, fleets):
        match message:
            case "topbumper":
                if top_bumper.intersecting(self.position):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case _:
                pass

The tests run. We could commit this but let’s consider it an experiment for now. Do the other two live ones:

    def interact_with_invadershot(self, shot, fleets):
        self.engage_with("invadershot", shot, fleets)

    def interact_with_shield(self, shield, fleets):
        self.engage_with("shield", shield, fleets)

    def engage_with(self, message, flyer, fleets):
        match message:
            case "topbumper":
                if flyer.intersecting(self.position):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case "shield":
                if self.colliding(flyer):
                    fleets.remove(self)
            case "invadershot":
                if self.colliding(flyer):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case _:
                pass

Now if we were really going to use this scheme, we would refactor to the point where no one sends the interact_with_klass methods, instead only sending engage_with. That could take quite a while and we would probably find some incremental way to do it. The question before us is whether we like this idea well enough to go with it. What do we see?

Well, we see that we should probably alphabetize the cases:

    def engage_with(self, message, flyer, fleets):
        match message:
            case "invadershot":
                if self.colliding(flyer):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case "shield":
                if self.colliding(flyer):
                    fleets.remove(self)
            case "topbumper":
                if flyer.intersecting(self.position):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case _:
                pass

Yes, much nicer. What do we like about this? What concerns us?

Like
It’s more compact than the other form, keeping all the interactions together. If the segments get too long, we could of course always extract small methods.
Concern
The strings are subject to error. A typo in a string will cause the case to be skipped and if the typo is subtle enough, it will be hard to catch.

It’s slower than a method dispatch. The match will be searched sequentially, where the method dispatch went directly to the relevant code.

There may be more things to think about. Let’s fix the concern about typos by passing the actual class as the message.

    def interact_with_invadershot(self, shot, fleets):
        self.engage_with(InvaderShot, shot, fleets)

    def interact_with_shield(self, shield, fleets):
        self.engage_with(Shield, shield, fleets)

    def interact_with_topbumper(self, top_bumper, fleets):
        self.engage_with(TopBumper, top_bumper, fleets)

    def engage_with(self, klass, flyer, fleets):
        match klass:
            case invader_shot.InvaderShot:
                if self.colliding(flyer):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case shield.Shield:
                if self.colliding(flyer):
                    fleets.remove(self)
            case top_bumper.TopBumper:
                if flyer.intersecting(self.position):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case _:
                pass

We had to qualify the class names, because Python’s match / case treats anything that looks like a variable name as a capture variable. The qualification required us to import those modules, which rather gloms up the name space a bit. But it’s a possibility if we’re concerned about the possibility of a typo in the match.

Of course, there’s no real reason that we have to use match / case. We could do this:

    def engage_with(self, klass, flyer, fleets):
        if InvaderShot == klass:
            if self.colliding(flyer):
                fleets.append(ShotExplosion(self.position))
                fleets.remove(self)
        elif Shield == klass:
            if self.colliding(flyer):
                fleets.remove(self)
        elif TopBumper == klass:
            if flyer.intersecting(self.position):
                fleets.append(ShotExplosion(self.position))
                fleets.remove(self)
        else:
            pass

Let’s compare that with what we could do using the old style:

    def interact_with_invadershot(self, shot, fleets):
        if self.colliding(shot):
            fleets.append(ShotExplosion(self.position))
            fleets.remove(self)

    def interact_with_shield(self, shield, fleets):
        if self.colliding(shield):
            fleets.remove(self)

    def interact_with_topbumper(self, top_bumper, fleets):
        if top_bumper.intersecting(self.position):
            fleets.append(ShotExplosion(self.position))
            fleets.remove(self)

FMG. I like the active interact methods, grouped together, better than the if/elif/else for sure. What about compared to this form of the match, which I think is the most clean:

    def engage_with(self, message, flyer, fleets):
        match message:
            case "invadershot":
                if self.colliding(flyer):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case "shield":
                if self.colliding(flyer):
                    fleets.remove(self)
            case "topbumper":
                if flyer.intersecting(self.position):
                    fleets.append(ShotExplosion(self.position))
                    fleets.remove(self)
            case _:
                pass

The interact methods have less nesting. They are perhaps more readable because of the spacing. They are faster.

Back Where I Started

I seem to be in a loop. The First Rule of Holes is “When you find yourself in a hole, stop digging”. I need a First Rule for this loop where I try to find a way to avoid editing all my classes while having a reasonable and safe description of the object’s interactions.

The state of play right now is that all of the interact_with_klass methods in Invaders are abstract and implemented in all the necessary classes, leaving them all a bit more messy than we would like. We were unable to invest the time to clean them up, so the abstract methods are not well arranged. We promise ourselves that we’ll clean them up when next we edit those classes. Who knows, it could happen.

Or, with the best of will, we may just have made our program a little bit harder to maintain, slowing down the project just a bit.

Summary

I had high hopes for the match / case approach, but once I could see it in code, I did not find it preferable to the simple double-dispatch we’re using now. But it is quite distressing that we have to modify around 15 files every time we want to add another Flyer, no matter how simple it is. That’s just not practical.

The tension is between practicality and purity, with a tiny bit of safety thrown in. I think that the facts are that the safety we gain from abstract methods is quite small, because it is too easy to just pass and be done. If we really want to be sure that the objects implement all the right interactions, we’d probably be better off with a separate declaration, some kind of matrix where we would thoughtfully mark all the necessary interactions. And even there … we still have every chance to get it wrong.

I am running out of ideas that are even tempting. Right now, we’re at a stable point. I might decide to go back to using non-abstract methods, but that troubles me because it becomes a mixed bag. We should either use them, or not use them, rather than use them sometimes and not others.

Arguably, this issue is an inevitable result of the decentralized design with all those different flyers in the mix. the interactions multiply rapidly as the number of objects increases. N-squared. Gets large quickly.

I’m glad to have gotten a bit of experience with match / case, but otherwise, this morning has been a big nothing in the code.

Don’t read this article. See you next time!