I think today I get to start on the invader shots. How much of the original game’s pattern must I follow? Probably it’s too soon to ask. Squiggles!
The Space Invaders description at Computer Archaeology is quite helpful. More helpful than the assembly code, which I no longer have at my mental fingertips.
I think a fact that we’ll want to deal with ab initio1 is that shots only update every three frames. That’s good: if they moved as fast as the player’s shot, I could never escape them. Each shot has four frames of animation, and we have the bitmaps for those already imported.
The plunger and squiggly shot follow a pattern of which column they fall from, while the rolling shot always starts above the player. I’ll have to watch an old recording to get a sense of how they do that. And I’m a bit concerned about the column idea because the columns don’t close up when a full column has been shot down. What does the game do if it wants to drop from column n but there are no invaders in that column? We’ll figure it out.
The shot needs to emanate from just below the bottom-most invader in the column. I think I have code for finding that somewhere, don’t I? Yes.
InvaderGroup.bottom_of_column. Even has a test.
I think we can have a single InvaderShot object which we’ll prime with different graphics. Everything else inside will probably be the same. So let’s TDD such a thing.
class TestInvaderShot: def test_exists(self): shot = InvaderShot(u.CENTER)
This is enough to require the class. I assumed a position2, and then in the init, I’m remembering that I need the bitmaps as well:
class InvaderShot: def __init__(self, position, maps): pass
The code is ahead of the test. I don’t mind this: I’m not a fanatic. Let’s change the test to pass in a map. Squiggly, because I like it best.
class TestInvaderShot: def test_exists(self): maker = BitmapMaker.instance() shot = InvaderShot(u.CENTER, maker.squiggles)
That’s passing. I think I want to try a pytest fixture again, to see if I can learn to make them useful.
class TestInvaderShot: @pytest.fixture def make_shot(self): maker = BitmapMaker.instance() return InvaderShot(u.CENTER, maker.squiggles) def test_exists(self, make_shot): pass
Now I can refer to
make_shot as a shot. I already hate the name, change it to shot.
class TestInvaderShot: @pytest.fixture def shot(self): maker = BitmapMaker.instance() return InvaderShot(u.CENTER, maker.squiggles) def test_exists(self, shot): pass
OK, what to test? Motion? On every three updates, the shot should move once. (We’ll say, on the third.)
This is enough to fail:
def test_moves_once_per_three_updates(self, shot): assert shot.position == u.CENTER
Because, of course, the little darling doesn’t know anything yet. So:
class InvaderShot: def __init__(self, position, maps): self.position = position
The new partial test passes, keep on:
def test_moves_once_per_three_updates(self, shot): fleets = Fleets() assert shot.position == u.CENTER shot.update(1/60, fleets) assert shot.position == u.CENTER shot.update(1/60, fleets) assert shot.position == u.CENTER shot.update(1/60, fleets) assert shot.position == u.CENTER + Vector2(0, 4)
This test demands more than it appears. The shot should really be an InvadersFlyer. As soon as I type that in, I’ll have the honor and privilege of implementing all the nice abstract methods. Here goes. I’ll spare you all the methods saying
pass. Oddly, for some reason,
update is not in the list of required methods, although
tick is. Weird decisions people make.
Anyway we need update, and it has to count calls.
class InvaderShot(InvadersFlyer): def __init__(self, position, maps): self.position = position self.count = 0 def update(self, _dt, _fleets): self.count = (self.count + 1) % 3 if self.count == 0: self.position = self.position + Vector2(0, 4)
The new test passes. Let’s reflect and plan.
So far so good. But that shot is going to run right off the screen. And that means we need to stop it. And that means that either we can just check its coordinate, or we can do as Bruce will surely suggest, and build a BottomBumper much like out TopBumper and side Bumpers. And we’ll do the bumper, but not now. For now, we’ll stop the thing. Let’s write a test for that:
def test_dies_past_edge(self, shot): fleets = Fleets() fi = FI(fleets) fleets.append(shot) current_pos = u.CENTER while current_pos.y < u.SCREEN_SIZE: assert shot.position == current_pos current_pos = current_pos + Vector2(0, 4) shot.update(1/60, fleets) shot.update(1/60, fleets) shot.update(1/60, fleets) assert shot.position.y >= u.SCREEN_SIZE assert not fi.invader_shots
This test will fly the invader past the bottom edge and then check to be sure it’s gone from the fleets. Presently the test fails on that last assertion. We will put in our hack, with a nod in Bruce’s direction, assuring him that we will do a bottom-bumper in due time.
def update(self, _dt, fleets): self.count = (self.count + 1) % 3 if self.count == 0: self.position = self.position + Vector2(0, 4) if self.position.y >= u.SCREEN_SIZE: fleets.remove(self)
Test passes. Let’s commit: initial InvaderShot travels at 1/3 speed, dies at bottom of screen. No animation yet.
I guess I could have committed a time or two already. I am not a good person3.
I want to see this thing go. For that we need to deal with drawing it, and firing it. Let’s see about drawing. We want the map to change on each move. I think we can test that:
def test_map_changes_on_movement(self, shot): maps = shot.maps assert shot.map == maps shot.move() assert shot.map == maps shot.move() assert shot.map == maps shot.move() assert shot.map == maps shot.move() assert shot.map == maps
I’ve made some design decisions here. First, the shot will have a member
map which is the map it will display. Second, it will have a method
move that moves the shot. I intend to extract that from
update. Here we go:
def update(self, _dt, fleets): self.count = (self.count + 1) % 3 if self.count == 0: self.move(fleets) def move(self, fleets): self.position = self.position + Vector2(0, 4) if self.position.y >= u.SCREEN_SIZE: fleets.remove(self)
Extract Method, type “move”, PyCharm does the work. I love those people!
Now the map stuff:
class InvaderShot(InvadersFlyer): def __init__(self, position, maps): self.maps = maps self.map = maps self.map_index = 0 self.position = position self.count = 0 def update(self, _dt, fleets): self.count = (self.count + 1) % 3 if self.count == 0: self.move(fleets) def move(self, fleets): self.update_map() self.position = self.position + Vector2(0, 4) if self.position.y >= u.SCREEN_SIZE: fleets.remove(self) def update_map(self): self.map_index = (self.map_index + 1) % 4 self.map = self.maps[self.map_index]
I had to add
fleets to the test:
def test_map_changes_on_movement(self, shot): fleets = Fleets() maps = shot.maps assert shot.map == maps shot.move(fleets) assert shot.map == maps shot.move(fleets) assert shot.map == maps shot.move(fleets) assert shot.map == maps shot.move(fleets) assert shot.map == maps
I can sense the test and code working hand in hand to give me what I want. I knew I wanted to extract
move but I didn’t know what its parameters would be. I could have figured it out, but instead I just let PyCharm do the extract, it determined that it needed fleets, and then the test wouldn’t run until its calls to
move included the fleets parameter.
The test is green. Commit: InvaderShot changes map on each move.
That implementation of the map change isn’t particularly fine, but it does the job. We’ll have occasion to review the code in more detail later.
What I want to do now is display one of these things to see what it does. I have tested it more extensively than my usual work on objects that display, but I still want to see it. I’ll put in a draw function and then I think I’ll just fire one from high up when I press the player’s firing key.
When I look at how we draw the player shot, I am reminded that I need a rectangle and sill surely need a mask. Here’s that one:
class PlayerShot(InvadersFlyer): def draw(self, screen): self.rect.center = self.position screen.blit(self.bits, self.rect)
I’ll work that out:
class InvaderShot(InvadersFlyer): def __init__(self, position, maps): self.maps = maps self.map = maps self.map_index = 0 self.rect = self.map.get_rect() self.rect.center = position self.count = 0 @property def position(self): return Vector2(self.rect.center) @position.setter def position(self, vector): self.rect.center = vector def draw(self, screen): screen.blit(self.map, self.rect)
I think that’ll do the trick. The tests are happy. Now let’s drop one of these babies.
It turns out to be easy:
class InvaderPlayer(InvadersFlyer): def attempt_firing(self, fleets): if self.free_to_fire: fleets.append(PlayerShot(self.rect.center)) pos = Vector2(self.rect.centerx, 32) maps = BitmapMaker.instance().squiggles fleets.append(InvaderShot(pos, maps))
And it works a treat, with one caveat. Here, enjoy this movie:
max-width: 80%; height: auto; vertical-align: middle;
Just one issue: they fall too slowly. I forgot to scale the velocity by our scale, which is 4. Need to fix the tests and then the code. Also need some decent constants.
I just changed all the 4s to 16, and now it looks right:
Let’s commit: Demo of squiggles falling from sky every time player fires.
I’ve done enough for one session, let’s sum up.
Test driving that object all the way down to cycling the map was easy and very helpful. It even helped drive out the
move method, which makes sense from both the testing side and the game side.
The fixture turned out to be convenient this time. I think the idea is to give it a name that makes sense in the test and have it return a single thing that you want to test. Well, one idea. There are surely more than one.
I am not in love with the map handling. It’s possible that a tiny object should handle that for us. Perhaps we’ll explore that later on. Similarly, a tiny object might help with the “move only every third time” behavior of the shot itself.
Looking forward, we’ll need a shot timer, which I think we’ll make as some kind of Flyer, and we need a BottomBumper to match the TopBumper, and to replace the perfectly fine check for position with something “better” by our standards of decentralized behavior.
I do agree with my readers, Bruce and Tomas, both of whom feel that if you’re going to have a decentralized model, you should always use it. The in-line check is more ad hoc. Perfectly OK by some standards, but still just kind of jammed in there.
And the magic constants are getting out of control. What is holding me back? Two things. First and foremost, laziness. When I get the bit in my teeth4 I just want to type in what I know and get the thing working. That’s admirable in some regards, but it tends to leave bizarre dangling things in places where no one will think to look for them. Right now, there are two 16s in the tests that match a 16 in InvaderShot. Who knows that? Right now, we all do. By tomorrow at least one of us will forget.
The second reason is that I’m not sure whether to make a new constants space for invaders or to use the existing
u. So far, I think there have been no changes, zero, not any, to the basic game framework, the Fleets object and so on. Invaders has been created solely by adding new invaders-oriented objects. And that, boys and girls and very welcome others, is quite marvelous.
But the code is not divided up into central, asteroids, invaders sections. I was not, and am not, adept enough with larger-scale Python to know how to do that, and my consultant hasn’t been available to ask. Probably just folders, but I had trouble moving things to new folders and now I am stymied. If only I were smarter!5
So we have cleanup to do and things to improve, and new objects to create. We’ll proceed with that as time goes on. For today, I am delighted to see those squiggles falling down the screen.
See you next time!
I’m not here to explain everything to you. ↩
Actually, I am a perfectly good person, who often does not do the optimal thing, but who keeps trying to do the right things. So are you, I am quite sure. ↩
What even is this? I know nothing about horses. ↩
When you assume … oh, never mind! ↩
You gotta know the territory! ↩