Python Asteroids+Invaders on GitHub

I will share with you an intuition, a guess, about where this Sprite thing is going. Then I’ll plug away.

Intuition

I was musing, as one does, about putting the Sprite into future code. Suddenly, very gently, a feeling, notion, intuition came to me, in a sort of cloudy pictorial form. The Sprite, it came to me, might allow us to reduce the code of our various Flyers, by subsuming common behavior. My intuition didn’t think “subsuming”, it was more like “oh, wow, stuff could move into Sprite!”

When Ward Cunningham originally coined the phrase “technical debt”, he described it as the difference between the design we actually have, and the design we would have if we had known then what we know now. And he went on to say that when we put today’s ideas into the system well, it can bring about a decrease in the system size. He used a gesture like holding a largish ball and collapsing it down smaller, bringing his hands together, fingers interlaced, holding a much smaller ball.

Today, of course, “technical debt” has come to mean something entirely different, too often something like “we’ll work fast and dirty now and when the technical debt gets too high we’ll clean it up later”. Right, like that trick ever worked.

I could rave on here about how so many of the actually good ideas we offered to the world at the time of the Agile Manifesto have been watered down or downright perverted in the name of making more money for someone else. That ship has sailed and sunk, but there remain folks who understand what we were really saying, and there are more now than there were then. We may have hundreds. Possibly thousands have part of it. Unfortunately we need about two or three more orders of magnitude of understanding to really do the job.

But I will not rave about that. I will not. I will not.

Meanwhile

I plan to continue putting the Sprite into the various objects that can use it. I think there are nigh on to a dozen. We have seen how it goes, I think. Unless something else interesting arises, I’ll show the results, if they are interesting.

No, Wait!

We’re looking at an interesting situation. If I’m right that there is a significant improvement that can come from putting in the Sprite, my thinking up to this moment has been that I have to put the Sprite in everywhere and only then go to a second phase where I reap further benefits.

But that’s ten or a dozen objects away. If this were a real product, it could be months away, or never.

As things stand, the main benefit of the Sprite seems to be that it handles display and animation in a more consistent manner. It does not reduce the code in an individual object much if at all. The code is arguably more maintainable. This is not a suitable incentive for proactively going through all the objects and putting in the Sprite: there is little or no benefit to doing it now over doing it when, if ever, we maintain the individual classes.

And, in this product, we’ll really never do that. In a real product, we might maintain some objects, but it’s quite likely that others would never come up for changes, and we would never improve their code. And that is the right thing to do. The unimproved code is only an issue if it comes into play for some other reason.

With Sprite as it stands, I think the net improvement to each user class is small, mostly coming from more consistency. But if my intuition is correct, that there is further improvement, a code reduction or something similar, then we would like to get that benefit sooner, and the benefit might actually make it worth adjusting other classes as well.

Therefore …

We should move as quickly as we can to identify as much benefit as possible from Sprite, and to getting that benefit early on, not only after making a large number of changes that do not offer much benefit.

Unfortunately …

That’s going to require a bit more speculation, or, as I like to call it “Design Thinking”. We need to get a sense of what value we can get, so that we can start working toward it.

The following is rank speculation design thinking:

  • The objects that do not use Sprite have to implement mask and rect anyway, and there are a few patterns, not very well thought out, that have evolved. Some return None. Some, return self._rect, maintaining an actual rectangle of their own. All this would by Sprite and if anyone had to implement rect at all, which I doubt, it would always be the same way.

  • The ones that return None can only do that because no one ever checks them for collision. This cannot be checked from the object implementing rect as None. It’s just that the code has to be that way. In particular, if some object did try to check one of these for collision, the game would crash. That could be “fixed” my bullet-proofing the Collider, making it check for None in a the right places. Nasty.

  • We might have a NullSprite object for delegation in the case of non graphical objects.

  • We might want some kind of non-colliding subclass of Flyer, but that seems messy.

  • Given that it’s done the same way, Sprite users could inherit the whole megillah, if we allowed inheritance of concrete code, which we do. This would reduce the code face substantially for many objects.

  • Intuitively, it seems to me that an InvadersFlyer might come down to nothing but a few interact_with implementations, with everything else handled by inheritance and the Sprite.

  • The Sprite has the positioned rectangle and the mask, so it has all the information needed for calculating collisions. That could eliminate the explicit use of Collider in most of the classes, though there would still be a method colliding somewhere. This, too, might be inherited.

  • The init for any drawn object (except perhaps InvaderScoreKeeper) includes some rigmarole about getting the bitmap and setting up the masks and such. Much of this is already done in Sprite, and with a bit of cleverness we can probably subsume all of it into a Sprite factory or factory method.

  • This will all be made a bit more tricky because the core framework will not know about Sprites, so we have to handle the generic framework calls properly. My belief is that this will not be much of a problem, perhaps no problem at all, because the core framework does nothing that could involved the sprite other than call draw, and we can field that in the superclass.

  • I think we’d wind up with everyone implementing a property, sprite. Objects using Sprite would of course return their sprite. Objects that draw with bitmaps but are not converted would sprite as return self. Objects with no Sprites could return self or a NullSprite.

Wow, there’s quite a lot of thinking there, and I believe it’s pretty solid considering that the whole thing is just an idea. Details at 11, of course.

In those speculations, er, design thoughts, the notion of changing the hierarchy is lurking at a few points. And that reminds me that I have some special tests that are checking to make sure object implement methods properly. Those will require work. More importantly, we should try to avoid changing the hierarchy too many times, where two many is probably “more than once”. I think it would be OK to add or remove particular abstract methods, but if we are to add another level, we should do that at most once. So that needs to wait.

Plan

It seems to me that we know what needs to be done. We need to get more benefit from the Sprite than we presently are, to make converting objects to Sprite more desirable. And we need to keep the process incremental, to the degree possible, rather than via a big switch, requiring serious updates to all the classes at once.

You Know What?

Too much speculation. Let’s try for some actual savings in our two existing objects that use Sprite, Invader and PlayerShot.

They even have an interesting interaction, so this may be the best case for improvement.

I think our two most likely areas of saving are in creating the Sprite, and in colliding. Since we have to create lots of Sprites if we do this, let’s start there.

class PlayerShot(InvadersFlyer):
    def __init__(self, position=u.CENTER):
        offset = Vector2(2, -8*4)
        self.velocity = Vector2(0, -4*4)
        maker = BitmapMaker.instance()
        bits = maker.player_shot
        self._sprite = Sprite([bits])
        self.position = position + offset
        self.should_die = False
        explosion = BitmapMaker.instance().player_shot_explosion
        self.explosion_mask = pygame.mask.from_surface(explosion)

Here’s what I wish I could say:

class PlayerShot(InvadersFlyer):
    def __init__(self, position=u.CENTER):
        offset = Vector2(2, -8*4)
        self.velocity = Vector2(0, -4*4)
        self._sprite = Sprite.player_shot
        self.position = position + offset
        self.should_die = False
        explosion = BitmapMaker.instance().player_shot_explosion
        self.explosion_mask = pygame.mask.from_surface(explosion)

I guess we could make a class method on Sprite for every one of the properties that BitmakMaker knows. But I don’t think we can make a class property, that seems to be deprecated in Python 11. We’ll use a method.

class PlayerShot(InvadersFlyer):
    def __init__(self, position=u.CENTER):
        offset = Vector2(2, -8*4)
        self.velocity = Vector2(0, -4*4)
        self._sprite = Sprite.player_shot()
        self.position = position + offset
        self.should_die = False
        explosion = BitmapMaker.instance().player_shot_explosion
        self.explosion_mask = pygame.mask.from_surface(explosion)

Let’s just build that.

class Sprite:
    @classmethod
    def player_shot(cls):
        return cls([BitmapMaker.instance().player_shot, ])

That one is a bit tricky, because Sprite wants a list, since it doesn’t animate. Works as advertised. Committed.

Let’s do Invader.

That actually needs to be handled in InvaderGroup and it should pass the Sprite to Invader, and it’s a bit intricate.

class InvaderGroup
    def create_invaders(self, invader_table):
        self.invaders = []
        for x in range(55):
            col = x % 11
            row = x // 11
            sprite = Sprite(invader_table[row])
            self.invaders.append(Invader(col, row, sprite))

class Invader:
    def __init__(self, column, row, sprite):
        self._score = [10, 10, 20, 20, 30][row]
        self._sprite = sprite
        self.column = column
        self.relative_position = Vector2(INVADER_SPACING * column, -INVADER_SPACING * row)
        self.image = 0

I had to change a few tests to accomplish this trick, since they were passing in bitmaps instead of Sprites. Commit: sprite creation moved to invader group. No specialized BitmapMaker method.

I am a bit surprised that I didn’t need a specialized method, but the invaders are special in that all three forms are created in one go in bitmapMaker and then that list is sliced to get the various pairs.

This has taken long enough that it is time for a break. A bit less progress than I’d hoped for, but the creation of the InvaderShot doesn’t use BitmapMaker, and the Invader now receives a Sprite.

Note
I should have stopped while ahead. No harm done but some wasted time. Needed a break, knew it, but then tried One More Thing.

You know what? We could move a bit more logic to Sprite. Here’s the code that makes the Invaders now:

    def create_invader_bitmaps(self):
        maker = BitmapMaker.instance()
        aliens = maker.invaders
        alien_table = (aliens[0:2], aliens[0:2], aliens[2:4], aliens[2:4], aliens[4:])
        return alien_table

    def create_invaders(self, invader_table):
        self.invaders = []
        for x in range(55):
            col = x % 11
            row = x // 11
            sprite = Sprite(invader_table[row])
            self.invaders.append(Invader(col, row, sprite))

How could we change this class so that it didn’t have to know BitmapMaker, just Sprite?

Perhaps we could, but I’ve tried two times and bollixed it both times. This is a major clue: Time for a break. And a bagel.

See you next time!