Python Asteroids+Invaders on GitHub

I’ve been pecking away at making the bitmaps for Space Invaders. Let’s take a look at where we are. Gross ad-hocery and a lot of pasted data and bitmaps. Not very interesting, even for me.

Note
I want this series to be fairly complete, so that if you’re willing to read hundreds of articles, you can at least find out how everything here was done. Some things, like creating the bitmaps today, are a bit tedious. Scan, read, or move on, as seems right to you.

Lesson learned? Probably OK not to have done TDD, but could have benefited from a bit more work on paper.

In the “invaders” repo, which you can find on my GitHubif you wish, I’ve been working with creating the bitmaps that make up the invaders and all the graphics for the Space Invaders game. The Asteroids game draws lines, so pygame bitmaps are a bit new to me. Here’s the picture that my current invaders project draws:

bit-mapped pictures of aliens, shots, etc

Every time I do this, I think of it as a one-off, and so I do not automate quite as much as I might. My process this time has gone like this:

Copy and paste the Space Invaders source lines for the bitmaps I want. They look like this:

aliens A, B, C position 1 (of 0, 1)
1C30: 00 00 38 7A 7F 6D EC FA FA EC 6D 7F 7A 38 00 00
1C40: 00 00 00 0E 18 BE 6D 3D 3C 3D 6D BE 18 0E 00 00
1C50: 00 00 00 00 1A 3D 68 FC FC 68 3D 1A 00 00 00 00

player
1C60: 00 00 0F 1F 1F 1F 1F 7F FF 7F 1F 1F 1F 1F 0F 00
player explode 0
1C70: 00 04 01 13 03 07 B3 0F 2F 03 2F 49 04 03 00 01
player explode 1
1C80: 40 08 05 A3 0A 03 5B 0F 27 27 0B 4B 40 84 11 48
player shot
1C90: 0F
player shot explode
1C91: 99 3C 7E 3D BC 3E 7C 99

There’s a lot of it. Just assembler lines, values assumed hex. As often happens, not all the lines are formatted quite the same way, but they are basically like that.

Then I take that data into Sublime Text and with a few regex replace operations and a bit of ad-hoc repeated editing, I get it into a form that I can almost use, like this:

(aliens, A,, B,, C, position, 1, (of, 0,, 1))
(0x00, 0x00, 0x38, 0x7A, 0x7F, 0x6D, 0xEC, 0xFA, 0xFA, 0xEC, 0x6D, 0x7F, 0x7A, 0x38, 0x00, 0x00)
(0x00, 0x00, 0x00, 0x0E, 0x18, 0xBE, 0x6D, 0x3D, 0x3C, 0x3D, 0x6D, 0xBE, 0x18, 0x0E, 0x00, 0x00)
(0x00, 0x00, 0x00, 0x00, 0x1A, 0x3D, 0x68, 0xFC, 0xFC, 0x68, 0x3D, 0x1A, 0x00, 0x00, 0x00, 0x00)

(player)
(0x00, 0x00, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x7F, 0xFF, 0x7F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0F, 0x00)
(player, explode, 0)
(0x00, 0x04, 0x01, 0x13, 0x03, 0x07, 0xB3, 0x0F, 0x2F, 0x03, 0x2F, 0x49, 0x04, 0x03, 0x00, 0x01)
(player, explode, 1)
(0x40, 0x08, 0x05, 0xA3, 0x0A, 0x03, 0x5B, 0x0F, 0x27, 0x27, 0x0B, 0x4B, 0x40, 0x84, 0x11, 0x48)
(player, shot)
(0x0F)
(player, shot, explode)
(0x99, 0x3C, 0x7E, 0x3D, 0xBC, 0x3E, 0x7C, 0x99)

That’s just about what I need for Python, and I bring it into my code for creating the bit-mapped surfaces that I need. Here’s the basic idea, trimmed down to keep things reasonably short:

class Game:
    def __init__(self, testing=False):
        self._testing = testing
        alien10 = (0x00, 0x00, 0x39, 0x79, 0x7A, 0x6E, 0xEC, 0xFA, 0xFA, 0xEC, 0x6E, 0x7A, 0x79, 0x39, 0x00, 0x00)
        alien20 = (0x00, 0x00, 0x00, 0x78, 0x1D, 0xBE, 0x6C, 0x3C, 0x3C, 0x3C, 0x6C, 0xBE, 0x1D, 0x78, 0x00, 0x00)
        ...
        player = (0x00, 0x00, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x7F, 0xFF, 0x7F, 0x1F, 0x1F, 0x1F, 0x1F, 0x0F, 0x00)
        player_e0 = (0x00, 0x04, 0x01, 0x13, 0x03, 0x07, 0xB3, 0x0F, 0x2F, 0x03, 0x2F, 0x49, 0x04, 0x03, 0x00, 0x01)
        player_e1 = (0x40, 0x08, 0x05, 0xA3, 0x0A, 0x03, 0x5B, 0x0F, 0x27, 0x27, 0x0B, 0x4B, 0x40, 0x84, 0x11, 0x48)
        ...
        if not testing:
            pygame.init()
            pygame.display.set_caption("Space Invaders")
            self.delta_time = 0
            self.clock = pygame.time.Clock()
            self.screen = pygame.display.set_mode((512, 768))

            aliens = (alien10, alien11, alien20, alien21, alien30, alien31)
            self.aliens = [self.make_and_scale_surface(bytes, 8) for bytes in aliens]

            players = (player, player_e0, player_e1)
            self.players = [self.make_and_scale_surface(bytes, 8) for bytes in players]
            # ... more of the same ...
        self.player_location = Vector2(128, 128)

    def make_and_scale_surface(self, bytes, scale, size=(16, 8)):
        return pygame.transform.scale_by(self.make_surface(bytes, size), scale)

    def make_surface(self, alien, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        count = 0
        for byte in alien:
            for z in range(8):
                bit = byte & 1
                x, y = divmod(count, 8)
                if bit:
                    s.set_at((x, 7-y), "white")
                byte = byte >> 1
                count += 1
        return s

    def main_loop(self):
        running = not self._testing
        while running:
            for event in pygame.event.get():
                if event.type == KEYDOWN:
                    if event.key == K_ESCAPE:
                        running = False
                elif event.type == QUIT:
                    running = False
            self.screen.fill("midnightblue")
            rect = Rect(0, 0, 32, 32)
            rect.center = self.player_location
            for i, alien in enumerate(self.aliens):
                x_base = 32
                x_off = 256*(i%2)
                dest = (x_base + x_off, 16+80*(i//2))
                self.screen.blit(alien, dest)
            for p, player in enumerate(self.players):
                dest = (32, 256 + 80*p)
                self.screen.blit(player, dest)
            # ... more of the same ...
            pygame.display.flip()
            self.delta_time = self.clock.tick(60) / 1000
        return "done"

The class here is named Game because this was my starting shell for Invaders and because it had to be named something. Recall that I started this before realizing that a more interesting way to do Invaders was to build it in to the Asteroids program by adding a few objects and a new kind of coin.

There is little to see in the above code. It’s pretty ad doc, and some parts, like the make surface method, have had no effort expended on making them clear. Let’s do look at that for a moment:

    def make_surface(self, alien, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        count = 0
        for byte in alien:
            for z in range(8):
                bit = byte & 1
                x, y = divmod(count, 8)
                if bit:
                    s.set_at((x, 7-y), "white")
                byte = byte >> 1
                count += 1
        return s

This function knows that all the bitmaps have a vertical size of 8 pixels. And it knows that the bitmaps provided are in y, x order, not x,y. This is the case because the original game had its display rotated 90 degrees, I think to give more pixels vertically than horizontally, because the original raster display was 224 bits by 256 or something like that.

Each byte in the input array provides 8 bits of the image, from top to bottom. The image is as wide, in pixels, as the size of the array. The code as it stands is assuming that every bitmap is 8 pixels high. That turns out not to be the case, but we have not hit that one yet. We’ll deal with it soon enough.

So the make_surface method shifts through the bytes from low bit to high, keeping track of how many bits it has done. (I think this could have been done differently, but we are where we are.) It calculates the x position in the bitmap as count//8 and the y position as count%8, so that it’ll do x = 0 and y = 0-7, then x = 1, and so on.

Except that the low bit is the bottom of the bitmap not the high, so when we set the bit into the bitmap, we set 7-y, not y. If we don’t do that, the bitmap is upside down.

Ad hoc, as I say, but it does the job.

What remains to be done?

Our next steps include bringing in additional bitmaps. There is one more alien shot in addition to the two shown in the picture now, there is a player shot, which is trivial, and there are the shields, which are 16 bits high rather than just 8. I’m not sure how wide they are: we’ll find out.

What I plan to do with this code is to move it into the real program and use it to initialize some kind of structure for the Space Invaders game to use to draw things. I have a few staring thoughts on that, very tentative.

The Invaders have two main forms, as you can see in the picture above. They alternate between forms each time they step. They also have one exploding image. I am not sure at this point whether that’s part of the invader, or a separate object, but I’m leaning toward a separate ephemeral object. I could be wrong about that.

The invader’s shots, two of which are shown in the picture above, have four forms, each 3 pixels by 8, which alternate on each step downward. They look pretty cool. So, I’m guessing that a shot will have its four bitmaps and step between them.

I think there needs to be some kind of bitmap repository in the game, a bit like a dictionary by name, which either returns a specific bitmap, or a small collection. I am leaning toward the small collection, with the notion being that each object just fetches its animation sequence and runs it. There should probably be one or more small record objects involved rather than simple sequences. This may be an opportunity to use the Python namedtuple idea, to see if we like it. Otherwise, we can readily build some tiny classes.

There may be more that we need: we’ll find out as we go.

What’s next?

I think we’ll first bring in the shields, which will break our existing code. We’ll see where that leads us. Still in the Invaders repo, we’ll drive out the repository idea, probably just a simple dictionary pointing to what we need to know. I think that will amount to a rectangle giving the size of the bitmap, and a simple list of the bitmaps for that dictionary entry.

If we are wise, which assumes facts not in evidence, we’ll provide our own cover object or objects for these things, rather than use vanilla collections.

The shield bytes look like this:

(shield)
(0xFF, 0x0F, 0xFF, 0x1F, 0xFF, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFC, 0xFF, 0xF8, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF)
(0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xFF, 0x0F)

I think there are 22 bytes in each row here. The original source has this comment:

ShieldImage:
; Shield image pattern. 2 x 22 = 44 bytes.

This thing is 22 pixels wide by 16 pixels high. Our mission is to import it, and to display it in our sample image shown above.

With a bit of editing, I get this definition up where the other bytes are defined:

        # shield is 22 pixels by 16 pixels, 44 bytes
        shield = (
            0xFF, 0x0F, 0xFF, 0x1F, 0xFF, 0x3F, 0xFF, 0x7F, 0xFF, 0xFF, 0xFC, 
            0xFF, 0xF8, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 
            0xF0, 0xFF, 0xF0, 0xFF, 0xF0, 0xFF, 0xF8, 0xFF, 0xFC, 0xFF, 0xFF, 
            0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0x3F, 0xFF, 0x1F, 0xFF, 0x0F)

What I would like to wind up with is with a new make_surface method that is sufficiently parameterized to draw all our exiting maps plus this one (and, presumably, any other odd ones that might come along). I can see at least two approaches:

  1. Eyeball the existing method and suss out how to fix it up.
  2. Write a new method for the shield using the existing one as a pattern and then observe the duplication.

I’ll start with a third way, by refactoring the existing one a bit. I think we can pull out something about producing a single byte’s worth. Let’s see. It looks like this:

    def make_surface(self, alien, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        count = 0
        for byte in alien:
            for z in range(8):
                bit = byte & 1
                x, y = divmod(count, 8)
                if bit:
                    s.set_at((x, 7-y), "white")
                byte = byte >> 1
                count += 1
        return s

Inside the for byte loop, we have a thing that writes one byte into the surface. Right now it writes into bits y = 7-0 of a structure that is assumed to be 8 high. Let’s extract that using Extract Method … wait, what are we going to do about count?

Over the course of the z loop, x is constant. Let’s rearrange the code. We’ll use the picture to see if we have it right: we have no tests for this, and I’m not terribly psyched for writing them.

    def make_surface(self, alien, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        count = 0
        for byte in alien:
            x = count // 8
            for z in range(8):
                bit = byte & 1
                y = 7 - z
                if bit:
                    s.set_at((x, y), "white")
                byte = byte >> 1
            count += 8
        return s

This is still working. Note that I am doing this with thinking and looking at the screen. I don’t really have an image of what we’re doing here. Let’s think a bit more abstractly.

No, let’s extract the byte loop now and then think. Things will be a bit more clear, I think.

    def make_surface(self, alien, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        count = 0
        for byte in alien:
            x = count // 8
            self.store_byte(byte, x, s)
            count += 8
        return s

    def store_byte(self, byte, x, surface):
        for z in range(8):
            bit = byte & 1
            y = 7 - z
            if bit:
                surface.set_at((x, y), "white")
            byte = byte >> 1

Now, looking at the make_surface, we see that x is increasing with each byte, and is just going 0, 1, 2, …

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        for x, byte in enumerate(bytes):
            self.store_byte(byte, x, s)
        return s

That’s nearly good, though it clearly assumes only one layer of bytes, and for the shield we need two. I should mention that I am trying hard not to think too much about x and y, because the things are stored sideways and if I get that in my head I’ll surely explode.

What will it take for the store_byte method to do stacked layers? It will need a constant added to its y value:

    def store_byte(self, byte, x, surface):
        for z in range(8):
            bit = byte & 1
            y = 7 - z
            if bit:
                surface.set_at((x, y), "white")
            byte = byte >> 1

Let’s change its signature to have a y offset. Change Signature in PyCharm:

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        for x, byte in enumerate(bytes):
            self.store_byte(byte, x, 0, s)
        return s

    def store_byte(self, byte, x, y_offset, surface):
        for z in range(8):
            bit = byte & 1
            y = y_offset + 7 - z
            if bit:
                surface.set_at((x, y), "white")
            byte = byte >> 1

All is the same so far. Now let’s see what we want to have happen. Our shield is 22 bits wide by 16 high. So we wish that make_surface would call store_byte with an x value of zero through twenty-one, twice, with a y value of 0 in the first case and 8 in the second. Probably.

Could we use the x value of the input size for this purpose? Let’s try passing in something like this:

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        for x, byte in enumerate(bytes):
            y_offset = x // width
            self.store_byte(byte, x, y_offset, s)
        return s

I’m going to try the shield now and see what happens. I do these things:

            self.shield = self.make_and_scale_surface(shield, 8, (22, 16))

            dest = (32, 512 + 80)
            self.screen.blit(self.shield, dest)

garbled image

That’s not it. I can see that I never got to the second row. And why would I: I need a value of 8, not 1. I try again with no luck. Let’s go another way. We’ll switch back to assuming only one layer and draw the thing into 44x16. We get this picture:

wide still weird

I freely grant that I am just bashing on this until it works. However, I am bashing in a slightly orderly fashion, reasoning about the divides and mods needed to move bytes where they should go.

I think I’ve figured it out. It’s not stored first layer all the way across, it’s stored column wise. So every other time through we need to swap down. Let me see if I can do this. We want y_offset to go 0, 8, 0, 8, and x to go 0, 0, 1, 1, 2, 2 …

I think I’ll write a new function for the shield and then see what I can do.

I still have the picture wrong in my head. Here’s a comment from the original code that shows what we want:

;************....
;*************...
;**************..
;***************.
;****************
;..**************
;...*************
;....************
;....************
;....************
;....************
;....************
;....************
;....************
;...*************
;..**************
;****************
;****************
;***************.
;**************..
;*************...
;************....

The input starts like this:

FF 0F FF 1F

We process the bits right to left. We see from the input that the first byte should go to y = 8, the second y = 0 and so on. I’ve been doing it 0 then 8, not 8 then 0.

Now I have this method:

    def make_shield_surface(self, bytes, size=(22, 16)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        for x, byte in enumerate(bytes):
            x_in = x // 2
            y_offset = 8 - (x%2)*8
            self.store_byte(byte, x_in, y_offset, s)
        return s

And I get this picture … finally!

shield is right

I think that’s actually right. This has taken more time than I’d like and I’d like to think about what I might have done better.

Reflection

I believe that I’d have done well to start with a separate function rather than try to modify the existing one. In particular, this is a one-off, so a little duplication isn’t going to hurt much, and it would have simplified my thinking.

Second, I think I’d have done well to compare the picture in the comments with the bytes, which would have told me right away that I had to start in the bottom of the picture, that is with y offset equal to 8, because Pygame increases y downward.

I might have done well to draw something on paper and associate numbers and mods and divides with it.

What about tests? It would be quite difficult, I think, to write a test against a bitmap, but perhaps what we could have done … this just popped into my mind … what we might have done is provide two bit-setting routines, one that does the actual surface.set_at, which we could test fairly simply, and then another fake bit-setter that set bits into a string, which we could test with visible strings. Those strings might well look a lot like the comments in the original game.

That might have gone better. But we have what we need now. Let’s see what we want to do to import it into the actual game.

First, let’s see if we want to combine our two byte-mapping functions:

    def make_shield_surface(self, bytes, size=(22, 16)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        for x, byte in enumerate(bytes):
            x_in = x // 2
            y_offset = 8 - (x%2)*8
            self.store_byte(byte, x_in, y_offset, s)
        return s

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        for x, byte in enumerate(bytes):
            x_in = x
            y_offset = 0
            if x < 22:
                self.store_byte(byte, x_in, y_offset, s)
        return s

It seems that the conceptual difference between the two is layers. The shield has two, everything else has one.

How do we know layers? I think we can know that by dividing the width of the surface by the length of the input array.

Let’s do that in the shield one:

    def make_shield_surface(self, bytes, size=(22, 16)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        layers = len(bytes)//width
        for x, byte in enumerate(bytes):
            x_in = x // layers
            y_offset = 8 - (x % layers)*8
            self.store_byte(byte, x_in, y_offset, s)
        return s

That works just fine. Can we make the same change to the other method? Not like that. If layers is one we want y_offset to be zero, otherwise that weird thing.

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        layers = len(bytes)//width
        assert layers == 1
        for x, byte in enumerate(bytes):
            x_in = x // layers
            assert x_in == x
            y_offset = 0 if layers == 1 else 8 - (x % layers)*8
            assert y_offset == 0
            self.store_byte(byte, x_in, y_offset, s)
        return s

This works. Do I like it well enough to pay the additional complexity? I think I do. I remove the other and use this one.

    self.shield = self.make_and_scale_surface(shield, 8, (22, 16))

    def make_and_scale_surface(self, bytes, scale, size=(16, 8)):
        return pygame.transform.scale_by(self.make_surface(bytes, size), scale)

    def make_surface(self, bytes, size=(16, 8)):
        s = Surface(size)
        s.set_colorkey((0, 0, 0))
        width = size[0]
        layers = len(bytes)//width
        for x, byte in enumerate(bytes):
            x_in = x // layers
            y_offset = 0 if layers == 1 else 8 - (x % layers)*8
            self.store_byte(byte, x_in, y_offset, s)
        return s

That works, i.e. gives me the right picture. Commit: improve drawing, include shield. Remember, that’s a commit to the invaders repo, not asteroids.

I don’t think that make_surface code meets a high standard of clarity, but it’s working and I still feel that it’s a one-off. What we might do, at some future time, is try to do a more professional job of crafting a surface maker, but this is the sort of thing you’d do at the command line if you could. Well, I would. I don’t know what you’d do.

We’ll wrap this here, put it out and next time bring this code into the main asteroids plus invaders repo

Summary

More ball-peen action than I’d have preferred, and I think some scribbling on paper might have helped. I really only tried a few odd mod things until I looked at the picture and then I got it right.

But could I have done better? I sure feel that I “should” have. But I’m not here to blame past me, I’m here to see what current me can learn that will help future me. Here, I think I might have had interesting results from test-driving the thing, but I think it would not have let me go faster, which is why I didn’t do it. Well, part of why. Mostly I just knew I didn’t want to. But some quick arithmetic on paper? I’m pretty sure that would have helped a bit.

But we have the pictures we need, and it’s still not even 0900.

See you next time!