I’ve decided to extend the Asteroids program to include Space Invaders in addition to Asteroids. I expect this to be—well—not easy but not too difficult.
Starting today, I’ll be working in the Asteroids repo shown above. The Invaders one will mostly remain static, but I might use it to develop the new surface creation that Invaders needs. Most likely, we’re done with that one.
Why this change of plan? Well, I realized that I’d been saying that what’s nice about the Asteroids decentralized design is that it should be able to support different games by “just” creating new kinds of Flyer subclasses and properly initializing the mix. Since Asteroids is pretty done, I wanted something new to work on and thought I might do Space Invaders. I was assuming that I’d start from scratch, until a bit of planning slapped me in the face and said “you could do this like Asteroids”. Having heard it, I felt I should try it. So here we are.
Objects in the Fleets collection, which I think of as “the mix”, all interact with each other in pairs. There are 55 Invaders when the game starts and they cannot affect each other in any way. So there would be, allowing for other objects, about 60x60 interactions, which seems like a lot when most of them do nothing.
So my plan is to have a single object, InvaderFleet or some such name, which will contain the individual invaders. It will forward the various
draw messages to the individual invaders. But when it comes to collisions, it will only forward one of the possibilities,
interact_with_bullet or whatever we call the object that the player’s gun fires. For that message, it’ll iterate through the invaders asking them if they collide with it. If they do, the InvaderFleet will remove that invader and add in an explosion.
Something like that, anyway. There are some interesting options.
Movement is special in Space Invaders. The invaders march from side to side, stepping downward when they get to one side or the other of the screen. But they don’t all move at once. Instead, they move one at a time with a sort of rippling effect. We’ll manage that from the InvaderFleet as well.
Beyond that, we’ll have a Player that moves back and forth, some bombs from the invaders, a bullet from the player, and a saucer. We’ll have the usual kind of scoring, and some finite number of player instances before GameOver. I expect all of that to be quite similar to what we already have in Asteroids.
It seems to me that the largest “risk” here is in the InvaderFleet and Invader classes, if my plan works, it’s all good, and if not … we need a new plan. So we should start with InvaderFleet and Invader.
Rules of the Road
Do the work in Asteroids, at HEAD, no persistent branches, and don’t break the Asteroids game. Use TDD as much as possible. Show your work. Don’t work when tired. Listen to Breakfast with the Beatles, even if it is hosted this week by David Frick.
Let’s Get To It
I think I can start with some TDD on InvaderFleet and Invader classes, without any possible harm to the game. It might be wise—this just popped into my head—it might be wise to move the existing asteroids code into a separate folder from the invaders code, or at least to put the invaders code into a separate folder.
I’ll start by figuring out how to move things between folders in PyCharm.
The obvious choice, Move File, doesn’t fix up the imports. OK, belay the reorganization, I’ll defer that until I can get some expert advice. No, wait. I named the folder src-asteroids and it didn’t fix up the imports but given a hint on line, “src_asteroids” does refactor correctly. Weird, JetBrains, really weird. I’ll move more.
It will move batches correctly. OK, we can do this. Grr. This is terrible. Batches don’t work well after all. I’ve fixed up the errors, continuing one at a time. Bah. This is no fun. Roll back. Leave the files where they are until I learn more.
Wind Out of Sails
That sort of thing takes the wind right out of my sails. Give me a moment to find some new wind.
Thanks, I Feel Better Now.
I think I’m OK to start TDDing the InvaderFleet object. New test file.
from invaderfleet import InvaderFleet class TestInvaderFleet: def test_exists(self): fleet = InvaderFleet() class InvaderFleet: def __init__(self): pass
OK, what do we want this baby to do? It has to be a Flyer, so we’ll ask it to inherit from Flyer, and then we’ll let PyCharm fill in the necessary methods. PyCharm adds pass methods for
Let’s drive out an Invader class indirectly.
def test_makes_invaders(self): fleet = InvaderFleet() invaders = fleet.invaders assert len(invaders) == 55
This posits a member
invaders that contains 55 somethings. Fails for want of the member.
I have this:
class Invader: def __init__(self): pass
But with InvaderFleet marked as a subclass of Flyer, some of my integrity checks are failing. In particular, the one that wants to be sure that
interact_with_invaderfleet is implemented.
I fix the tests. Modified the one to ignore InvaderFleet entirely.
This is an issue with using this engine for a second game. In principle, every Flyer needs to be prepared to deal with every other kind of flyer, but in fact we “know” that we’ll never put an InvaderFleet in with an Asteroid. The basic methods for the original Flyers have been made abstract, so that everyone has to implement them. That has already required me to implement things like
interact_with_asteroid in InvaderFleet.
I think we’ll push forward with this for a while, but I expect that it’ll come up again, and we’ll want to figure out some way to partition things.
Moving right along
OK, we have 55 Invader instances in our fleet. Let’s move on a bit.
Invaders will have an x,y position on the screen. At the beginning there are five rows of eleven invaders. We will need to know when the leftmost invader reaches far enough to the left, and vice versa. So I plan to store the invaders in order of y, within x, so that they’ll be at
(x0, y0), (x0, y1), (x0, y2) ... (x11, y3), (x11, y4)
Unfortunately, I have no idea right now what those x and y values will be. Let’s instead just give them integer x and y, with x ranging 0-10 and y ranging 0-4. They can look up their coordinates somehow, using that information.
def test_invaders_order(self): fleet = InvaderFleet() count = 0 for x in range(11): for y in range(4): invader = fleet.invaders[count] assert invader.x == x assert invader.y == y count += 1
I think that’s what I want. Unfortunately, the implementation is going to look just like that, so we’ll do a bit more in a moment.
Well, the test was off by one:
def test_invaders_order(self): fleet = InvaderFleet() count = 0 for x in range(11): for y in range(5): invader = fleet.invaders[count] assert invader.x == x assert invader.y == y count += 1
And the implementation is different, so that’s nice:
class InvaderFleet(Flyer): def __init__(self): self.invaders = [Invader(x//5, x % 5) for x in range(55)]
I think we’re far enough along to commit this. I’m a bit concerned that this may all fall apart in my hands, but only a bit. Commit: initial test and classes InvaderFleet and Invader.
We are under way. What’s next? Well, I’m concerned about the real coordinates, but perhaps we shouldn’t be. Maybe we’ll gin those up at the last minute somehow.
I’m also concerned that I’d like to have something visible to show to my customer (me). Let’s take a page from Mary Rose Cook, let’s just display some squares. No, we’ll go wild: circles!
We’ll need a new coin that inserts an InvaderFleet instead of whatever coins usually do. Clearly that coin should be on the “i” key.
In the coin.py file:
def invaders(fleets): fleets.clear() fleets.append(InvaderFleet())
And in Game
class Game: def accept_coins(self): keys = pygame.key.get_pressed() if keys[pygame.K_q]: coin.quarter(self.fleets) elif keys[pygame.K_a]: coin.no_asteroids(self.fleets) elif keys[pygame.K_2]: coin.two_player(self.fleets) elif keys[pygame.K_i]: coin.invaders(self.fleets)
This should be enough to try. I expect that it produce a blank screen, because InvaderFleet has no behavior of note.
That’s what happens. Life is good.
Now let’s make it draw something, anything at all.
class InvaderFleet(Flyer): def __init__(self): self.invaders = [Invader(x//5, x % 5) for x in range(55)] def draw(self, screen): pos = u.CENTER hw = Vector2(100, 200) rect = (pos - hw/2, hw) pygame.draw.rect(screen, "blue", rect)
Let’s do some figuring now. Our screen is 1024x1024. The invaders game was nearly square, so that’s OK. We need 11 invaders across, and looking at images of the original game, it looks like about two invaders of space on each side. For convenience, let’s say 16 across, so they’ll be spaced 1024/16 or 64 pixels apart. Our actual invader surface, by the way, is 16 by 8 and I think they butt up against each other exactly. We’ll not worry about that but it came to mind.
Invader #6 wants to be centered on the screen. So if #0 is at the starting position sx, #6 will be at 512 = 5*64 + sx, so sx = 512 - 5*64 = 512 - 320 = 192.
Let’s have InvaderFleet tell the invaders to draw. They’ll need the starting position and the step. Maybe like this:
def draw(self, screen): pos = u.CENTER hw = Vector2(100, 200) rect = (pos - hw/2, hw) pygame.draw.rect(screen, "blue", rect) start = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512) step = 64 for invader in self.invaders: invader.draw(screen, start, step)
class Invader: def __init__(self, x, y): self.x = x self.y = y def draw(self, screen, start, step): pos = (start.x+self.x*step, start.y - step*self.y) pygame.draw.circle(screen, "red", pos, 16)
And again Voila!
I call that outstanding. The only mistake I made there was to set the initial y point too low, less than the 512 I showed you above. We have some magic numbers in there, but actually they’re not terribly magic, they just need sensible names.
I think we’ll break here. I could do a little more but I deserve a rest and to quit on a high. Commit: initial game, coin is “i”, just displays an array of red circles arranged like invaders.
So. What has happened?
I’ve learned that moving files around in PyCharm isn’t bulletproof, at least not if you move them in batches. That has left me with a lot of files right at the top level, which is a bit of a pain but livable. I’ll ask my pals for advice, but it’s not urgent. It did waste a bunch of time until I decided to roll back and leave things at the top.
Then a little bit of TDD drove out a simple InvaderFleet, subclass of Flyer, which contains 55 instances of Invader, each with an x and y value amounting to their column and row index in the picture. We might want to consider renaming x and y to col / row. Or not.
The fleet knows to draw the invaders, and it sends them a starting address and a step, which they use to decide where to draw. I’m not sure whether that idea will hold water. We’ll at least have to be careful to allow the fleet to compute the column and row indexes far enough outside the screen to allow an inner invader to reach the edge of the screen.
I apologize that that last sentence isn’t clear. My thoughts aren’t clear yet either. When there are only a few invaders left, they could have any given x and y, col / row. And the leftmost invader always wants to get clear over to the turnaround value on the left, and similarly on the right. So the Fleet will have to take the x (col) value of the left- or right-most invader into account when setting up the coordinate values.
Still not clear, because I’ll have to figure it out to make it clear. I hope you get the idea. I think that if the end point on screen was, say, 16, we’d want something like this:
If the smallest column number in the list is c, then we want start such that start + c*step is 16, so start = 16 - c*step.
Probably. We’ll find out.
I expect that next time we’ll work on moving the invaders, which will include the ripple style of moving, and will require us to begin to settle in on the end points.
For now, we have something to show. It’s running early code but the objects are TDDed, and just about in the right place, with the InvaderFleet holding the Invaders. It can draw them. Could moving them be much more difficult? I think not.
See you next time!