Python Space Invaders on GitHub

We do a quick bitmap experiment, and then create some alien surfaces from the original Space Invaders arcade source code.

I have found the asteroids arcade source again. It includes the hexadecimal data for the graphic images in the game. I do have the images saved as actual images, from my Codea version of Asteroids, but for authenticity and learning, I propose to recreate them from the original hex.

After just a bit of messing about, I have this in Game.__init__:

...
    s = Surface((16, 8))
    for x in range(8):
        s.set_at((x, x), "white")
        s.set_at((7 + x, 7), "red")
    self.alien_surface = s
    self.alien_surface = pygame.transform.scale_by(self.alien_surface, 8)

And I’ve added this to the draw:

    pygame.draw.rect(self.screen, "white", rect)
    pygame.draw.line(self.screen, "red", (128, 0), (128, 256))
    pygame.draw.line(self.screen, "red", (0, 128), (256, 128))
    dest = (32, 32)
    self.screen.blit(self.alien_surface, dest)

And the result is this:

display showing diagonal white and horizontal red lines in large square pixels

That’s just about what I expected. You’ll notice if you look carefully that the picture is drawn on a black rectangle, wiping out part of the dark blue background. That will be worth remembering: we should be sure to fill our bitmaps with the background color.

Let’s work out how to draw the aliens. Here is a picture of the code page so you can be as confused as I am:

source code for aliens

I don’t quite see the pictures in there, do you? Here’s the text:

; Alien sprite type A,B, and C at positions 0
;  ........ ........ ........
;  ........ ........ ........
;  *..***.. ........ ........
;  *..****. ...****. ........
;  .*.****. *.***... *..**...
;  .***.**. .*****.* .*.***..
;  ..**.*** ..**.**. *.**.**.
;  .*.***** ..****.. .*.*****
;  .*.***** ..****.. .*.*****
;  ..**.*** ..****.. *.**.**.
;  .***.**. ..**.**. .*.***..
;  .*.****. .*****.* *..**...
;  *..****. *.***... ........
;  *..***.. ...****. ........
;  ........ ........ ........
;  ........ ........ ........
1C00: 00 00 39 79 7A 6E EC FA FA EC 6E 7A 79 39 00 00 
1C10: 00 00 00 78 1D BE 6C 3C 3C 3C 6C BE 1D 78 00 00 
1C20: 00 00 00 00 19 3A 6D FA FA 6D 3A 19 00 00 00 00 

If we read the columns from left to right, and wrap at 3, then we might notice that

*..***.. is 0x93, and just below it, *..****. is 0x97, and those are the numbers in the code below, reversed! So, that’s nice.

What do we “know” about this data? It represents three alien pictures. Ohh, I can almost see them, if you look at the asterisk pictures from the left edge. Ah, and each row of hex is symmetric. So each row probably represents one alien. And the pictures are 16 pixels wide by 8 high, and the hex rows are 16 8 bit hex numbers.

I try this, with an interesting result:

    alien1 = (0x00, 0x00, 0x39, 0x79, 0x7A, 0x6E, 0xEC, 0xFA, 0xFA, 0xEC, 0x6E, 0x7A, 0x79, 0x39, 0x00, 0x00)
    s = Surface((16, 8))
    count = 0
    for byte in alien1:
        for z in range(8):
            bit = byte & 1
            byte = byte >> 1
            col = int(count % 16)
            row = int(count / 16)
            if bit:
                s.set_at((col, row), "white")
            count += 1
    self.alien_surface = s
    self.alien_surface = pygame.transform.scale_by(self.alien_surface, 8)

two aliens, sideways not identical

I fill with red and draw again, for better contrast:

two aliens, sideways not identical

The good news is that they look a lot like the first alien. The bad news is they don’t look exactly like it. What am I missing?

Well. I’m drawing sideways, so I need to reverse my row and column thinking. Probably x and y would be better anyway. x will vary from 0 to 15, y from 0 to 7. y varies faster.

A picture on a card, some counting by hand, and this code:

    count = 0
    for byte in alien1:
        for z in range(8):
            bit = byte & 1
            byte = byte >> 1
            x = int(count / 8)
            y = 7 - int(count % 8)
            if bit:
                s.set_at((x, y), "white")
            count += 1
    self.alien_surface = s
    self.alien_surface = pygame.transform.scale_by(self.alien_surface, 8)

Produces this picture:

good alien, white pixels on red

Perfect! And it almost makes sense, doesn’t it? Since the picture is 8 pixels high, we increment x every 8 pixels, and since things are upside down, we subtract the raw y value of 0-7 from 7 to invert the picture.

From here, I can clearly convert anything I need to, which will include six alien pictures (3 aliens, two pictures each), a player, and those odd bunker things that we can hide behind. On, and some other things: some number of pictures per missile, perhaps four. The saucer.

We’ll want to work out more of these and generalize our conversion code, I imagine.

Let’s do the three aliens whose bytes we looked at above.

three aliens, white on red

Commit: Game spike draws three aliens on red background.

Here’s Game in all its glory:

class Game:
    def __init__(self, testing=False):
        self._testing = testing
        alien1 = (0x00, 0x00, 0x39, 0x79, 0x7A, 0x6E, 0xEC, 0xFA, 0xFA, 0xEC, 0x6E, 0x7A, 0x79, 0x39, 0x00, 0x00)
        alien2 = (0x00, 0x00, 0x00, 0x78, 0x1D, 0xBE, 0x6C, 0x3C, 0x3C, 0x3C, 0x6C, 0xBE, 0x1D, 0x78, 0x00, 0x00)
        alien3 = (0x00, 0x00, 0x00, 0x00, 0x19, 0x3A, 0x6D, 0xFA, 0xFA, 0x6D, 0x3A, 0x19, 0x00, 0x00, 0x00, 0x00)
        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((256, 256))
            self.alien1 = self.alien_surface(alien1)
            self.alien2 = self.alien_surface(alien2)
            self.alien3 = self.alien_surface(alien3)
            self.alien1 = pygame.transform.scale_by(self.alien1, 8)
            self.alien2 = pygame.transform.scale_by(self.alien2, 8)
            self.alien3 = pygame.transform.scale_by(self.alien3, 8)
        self.player_location = Vector2(128, 128)

    def alien_surface(self, alien1):
        s = Surface((16, 8))
        s.fill("red")
        count = 0
        for byte in alien1:
            for z in range(8):
                bit = byte & 1
                byte = byte >> 1
                x = int(count / 8)
                y = 7 - int(count % 8)
                if bit:
                    s.set_at((x, y), "white")
                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
            dest = (32, 16)
            self.screen.blit(self.alien1, dest)
            dest = (32, 16+80)
            self.screen.blit(self.alien2, dest)
            dest = (32, 16+160)
            self.screen.blit(self.alien3, dest)
            pygame.display.flip()
            self.delta_time = self.clock.tick(60) / 1000
        return "done"

Summary

All pretty ad hoc, but you can see the generality starting to form.

I don’t consider that extracted method to be the perfect end point of creating surfaces, but it’s the method that PyCharm’s extract method did without my intervention, which means that it works.

Lots to do, but we now know that we can import the bitmap images from the original source in to our program. We’ll decide later whether to create the surfaces from the hex lists, or to save the surfaces as files and read them back in, but I think we’ll likely just create them when we start up.

This is the “real” Game

I’m obviously experimenting and spiking here, but I hope it’s also clear that I’m moving gently toward code that we can actually use in the real game. I am not working in separate experimental files. This is the real “invaders” source, and it will grow and evolve toward the actual game. We will probably do a bit more importing but we should get a game in there quite soon, because importing is basically solved, and playing the game isn’t solved, so it should get priority quite soon.

Visible progress

This morning we can tell our customer (me): Look! We have the ability to import the graphics from the original source, and it’s only 0609 on the second day.

See you next time!