P-223 - Firing?
Python Asteroids+Invaders on GitHub
Invader shots emanate from invader columns, according to a pattern. How are we going to do that?
Requirements
I’m following the Alien Shots section at Computer Archaeology. It tells me that there can only be three shots on the screen, one of each type, or two shots plus the saucer. It tells me that, in the original game, the shots keep a running counter of how long they’ve been running, and the controller compares the least of these values to a magic timing number to decide whether or not to fire.
There is a table of column numbers that determines which column will fire a shot next, for the squiggly and plunger shots. The rolling shot “drops right over the player” when its turn comes. My attempted reading of the code tells me that it still starts from an invader column, picking the one closest to being above the player. I could be wrong, but I’m pretty sure that it doesn’t just appear in the sky.
The rate of fire gets faster as the player’s score increases. We’ll clearly wait for that until we have a score.
Plan
I do have a possibly useful function, InvaderGroup.bottom_of_column
that returns the invader at the bottom of a provided column, or None if there are no invaders in that column. I wrote tests for that speculatively, some time ago, thinking that it might be useful in the firing process.
If we’re going to follow the requirements above, it seems that we’ll need firing control code, probably a separate object, that does things like this:
- Keep track of live shots and how long they’ve been alive;
- Determine whether to fire based on the least firing time;
- Decide which type to fire;
- Determine a desired column;
- Find lowest position in column, or select a new column;
- Start a shot from that position.
As I write down that list, I am thinking that a separate Flyer subclass would be the standard decentralized way to do this job. We’d use the begin / interact / end logic to collect the facts and then drop the shot.
The ideas above kind of have two phases. One phase is determining whether it is time to fire. That’s based on looking at the existing shots. The other phase is deciding where to fire from. There’s no reason why one object couldn’t accumulate both kinds of facts, but there is a bit of a glitch.
If our new object were interacting with all the invaders, it could keep track of sufficient column information to make its decisions locally. But as a rule we do not have objects interacting with all the invaders, just with their group. Our object could just make note of the group and ask its questions of the group before firing.
Let’s try that sort of scheme, checking invader shots, and the invader group.
TDD?
I certainly want to test-drive things if I can. This one may be a bit tricky: we’ll need to find out.
One thing pops out as an early need: shots need a timer. The CA article suggests that it is just a move counter. Let’s look at the Shot:
class InvaderShot(InvadersFlyer):
def __init__(self, position, maps):
self.maps = maps
self.masks = [pygame.mask.from_surface(bitmap) for bitmap in self.maps]
self.map = maps[0]
self.map_index = 0
self._rect = self.map.get_rect()
self.rect.center = position
self.count = 00
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, 16)
if self.position.y >= u.SCREEN_SIZE:
fleets.remove(self)
Now, the original game actually cycled each shot in a separate refresh. We probably do that by chance but not on purpose. Shouldn’t matter. We do have some tests for InvaderShot.
Let’s add a new one:
def test_counts_moves(self, shot):
fleets = Fleets()
assert shot.moves == 0
for _ in range(3):
shot.update(1/60, fleets)
assert shot.moves == 1
for _ in range(3):
shot.update(1/60, fleets)
assert shot.moves == 2
That should do the trick. We do the update 3 times because we only move on every third update.
class InvaderShot(InvadersFlyer):
def __init__(self, position, maps):
self.maps = maps
self.masks = [pygame.mask.from_surface(bitmap) for bitmap in self.maps]
self.map = maps[0]
self.map_index = 0
self._rect = self.map.get_rect()
self.rect.center = position
self.count = 00
self.moves = 0
def move(self, fleets):
self.moves += 1
self.update_map()
self.position = self.position + Vector2(0, 16)
if self.position.y >= u.SCREEN_SIZE:
fleets.remove(self)
And green. Commit: InvaderShot counts its moves.
Reflection
So that’s nice. What’s next? I think we want to drive out a ShotController object. New test file and new test.
class TestShotController:
def test_exists(self):
ShotController()
class ShotController(InvadersFlyer):
def __init__(self):
pass
And, of course, I let PyCharm fill in the required methods. Which teaches me one thing right away, namely that we’ll see the InvaderFleet, not the InvaderGroup, during our interactions. The fleet will have to moderate for us, passing requests to the group as needed. Should be no real problem
It does make me wonder a bit about the InvaderFleet / InvaderGroup separation. I’ve been wondering anyway whether that is helping as much as it might, since a day or so ago I had to bend over a bit backward to get a Fleets instance into an Invader, so that it could create an explosion. I try to notice and think about those bits where the code pushes back a bit. Over time, I might get an idea to improve things.
I think we’ll want to test the ShotController’s observation of the existing shots and tracking of the minimum moves
observed. The basic firing rate is … what? The CA article says the starting reload rate is 0x30. Is that 48 moves? The shots move at a rate of 4 or 5 pixels per move in the original game so that would be 192 pixels of motion? That seems unlikely to be right.
Reading the code as well as I can, it really looks like they just increment the step count. I guess we’ll just try it and see what happens.
So when we begin interactions, we should set the “minimum time” to our shot reload time, because if we see no shots we want to fire. In interact_with_invadershot
we should check the shot’s moves
and if less, save it. In end_iteractions
or tick
, we can do our firing.
We’ll have Fleets available but not InvaderFleet, so we should save a copy of that as it goes by as well. Let’s test.
def test_keeps_lowest_move(self):
fleets = Fleets()
shot = InvaderShot()
controller = ShotController()
controller.begin_interactions(fleets)
assert controller.time_since_firing == 0x30
This is enough to drive out the member, but it’s also enough to remind me that we really need to deal with these magic numbers. Let’s try something. Let’s put this one into the ShotController class.
def test_keeps_lowest_move(self):
fleets = Fleets()
shot = InvaderShot()
controller = ShotController()
controller.begin_interactions(fleets)
assert controller.time_since_firing == controller.max_firing_time
Instead of having a big global constants pool, let’s try having individual objects know their own constants. Might be better, might be worse. We could argue that since we have the u
construct for Asteroids, we should use it, but the Invaders team is separate and are trying this other way. We’ll see what we think.
I am getting a test failure on people not implementing interact_with_shotcontroller
. No one needs to do that. I’ll stub it in the superclass. Yes, this is a sin, but so is editing every other class.
We need a better solution here but I don’t know what it is.
Updating the test to create a legal shot, it passes:
def test_keeps_lowest_move(self):
fleets = Fleets()
maker = BitmapMaker.instance()
shot = InvaderShot(u.CENTER, maker.squiggles)
controller = ShotController()
controller.begin_interactions(fleets)
assert controller.time_since_firing == controller.max_firing_time
I had in mind extending it, of course …
def test_keeps_lowest_move(self):
fleets = Fleets()
maker = BitmapMaker.instance()
shot = InvaderShot(u.CENTER, maker.squiggles)
controller = ShotController()
controller.begin_interactions(fleets)
assert controller.time_since_firing == controller.max_firing_time
moves = controller.max_firing_time - 5
shot.moves = moves
controller.interact_with_invadershot(shot, fleets)
assert controller.time_since_firing == moves
And
def interact_with_invadershot(self, shot, fleets):
if shot.moves < self.time_since_firing:
self.time_since_firing = shot.moves
Test passes.
Let’s test that it does fire. We’ll do a new test, I think.
def test_fires_shot(self):
fleets = Fleets()
fi = FI(fleets)
controller = ShotController()
assert not fi.invader_shots
controller.begin_interactions(fleets)
controller.end_interactions(fleets)
assert fi.invader_shots
We aren’t saying what kind or where, but we’re saying a shot has been fired. Test fails on the last assert. Note that I’ve decided to fire in end_interactions
.
def end_interactions(self, fleets):
if self.time_since_firing <= self.max_firing_time:
pos = Vector2(random.randint(50, u.SCREEN_SIZE - 50), 64)
shot = InvaderShot(pos, BitmapMaker.instance().squiggles)
fleets.append(shot)
This is sort of a fake it till you make it thing. I’m just creating a squiggles shot somewhere near the top of the screen. Tests are green. What will happen if I put a ShotController into the mix? I think it probably fires a lot. Let’s find out.
Well, yes:
I think we have a bug in the test for firing.
def end_interactions(self, fleets):
if self.time_since_firing <= self.max_firing_time:
pos = Vector2(random.randint(50, u.SCREEN_SIZE - 50), 64)
shot = InvaderShot(pos, BitmapMaker.instance().squiggles)
fleets.append(shot)
Let’s see. First of all, real time has not entered into the equation at all. It’s only moves made by existing shots. Second, if they haven’t moved far enough yet, we don’t want to fire. So the if is backward there.
Changing that <=
to >=
is better, but as I was suspecting, it doesn’t seem quite right. Now it only drops a squiggle just before the previous one goes off screen, and we’re firing from the top of the screen and will really only be firing from near the middle. I don’t understand these numbers.
Calling them time
isn’t helping.
I looked at my Codea Invaders program and it seems to be counting update cycles, which will give a base firing rate of about two shots per second. Clearly I need to do something that we can live with until I figure out more about the original game (or give up).
Let’s drop a missile every 0x30 update cycles and see how that looks.
class ShotController(InvadersFlyer):
max_firing_time = 0x30
def __init__(self):
self.time_since_firing = 0
def begin_interactions(self, fleets):
self.time_since_firing += 1
def end_interactions(self, fleets):
if self.time_since_firing >= self.max_firing_time:
self.time_since_firing = 0
pos = Vector2(random.randint(50, u.SCREEN_SIZE - 50), u.SCREEN_SIZE / 2)
shot = InvaderShot(pos, BitmapMaker.instance().squiggles)
fleets.append(shot)
def interact_with_invadershot(self, shot, fleets):
pass
That drops shots pretty rapidly, more than one per second. We’ll let that ride. Tests went red, since I changed the rules.
New tests:
def test_tracks_cycles(self):
fleets = Fleets()
maker = BitmapMaker.instance()
shot = InvaderShot(u.CENTER, maker.squiggles)
controller = ShotController()
assert controller.time_since_firing == 0
controller.begin_interactions(fleets)
assert controller.time_since_firing == 1
def test_fires_shot(self):
fleets = Fleets()
fi = FI(fleets)
controller = ShotController()
assert not fi.invader_shots
controller.begin_interactions(fleets)
controller.time_since_firing = controller.max_firing_time
controller.end_interactions(fleets)
assert fi.invader_shots
Commit: Firing random shots from midscreen.
Let’s see about making a fuss if we hit the player. I don’t want to destroy it, since I have no way to get it back. But maybe we can explode it a bit. Here’s the player with a little bit of patching:
class InvaderPlayer(InvadersFlyer):
def __init__(self):
maker = BitmapMaker.instance()
self.players = maker.players # one turret, two explosions
...
self.fire_request_allowed = True
self.explode_time = 0
def interact_with_invadershot(self, shot, fleets):
collider = Collider(self, shot)
if collider.colliding():
self.explode_time = 1
def draw(self, screen):
self.explode_time -= 1/60
if self.explode_time > 0:
player = self.players[1]
else:
player = self.player
screen.blit(player, self.rect)
If we get hit by a shot, we display players[1] for one second. That is one of the two explosion patterns. It looks like this:
Commit: player shows one explosion graphic for one second if hit by invader shot.
Let’s sum up.
Summary
Small Steps
I want to begin by talking about small steps. We have seen that the overall “requirements” for the shots are complicated and not entirely clear. In particular, the timing description doesn’t make sense. No, I am mistaken. I do not understand1 the timing description.
So we’ve set a fixed timing value. Clearly we can adjust that based on things like the number of invaders or score. We’d have to get access to those numbers, of course. It might be more important to figure out what the original code does.
And the invader shots don’t destroy the player, they just display a dumb explosion thing for a second.
How is this a reasonable step? It doesn’t meet the requirements!
Now some teams would have rules and those rules might say that I can’t push the shot code until it meets the shot requirements and that I can’t push an explosion until it meets the explosion requirements, and so on. But if those are the team rules, it’s going to be several sessions, probably a few days, before I can push any of the code. And so far, just this morning, I’ve made eight changes to seven different code and test files.
If this keeps up, over the next few days, other developers are surely going to touch some of those files, or change something related to them, and someone is going to have a hellish time integrating my changes. Probably me, and if I’m lucky, I can rope in whoever stepped on “my files”.
The invaders game is not released to customers. There is a secret key that runs it, and no one knows the secret. If they guess it, they can play with the partially implemented game. It doesn’t do any harm.
“But my product manager only thinks in terms of the full feature!” My advice—if I gave advice, which I do not2—would be to fix your product manager and get them used to looking at partially completed work, and to get in a situation where you can push this perfectly good but incomplete code when you have the tests green.
Anything else will slow down product development by causing big merge conflicts and slow checking by the PM or some rando in QA or whatever other procedural obstacle is in your way. People always want the product before we can finish it and when they don’t get it they are going to say “development takes too long” and “the developers need to go faster” and “we’d better whip the ponies harder”.
I, for one, do not wish to be whipped harder.
When we get into a situation where our product manager, customer, product owner, product champion, whatever we call them, when we get into a situation where they are following along with our small steps and see that the product is safe with those small steps in it, we’ll be able to remove some, perhaps most of the built-in checking and delay that is causing something done today to stay out of the code for days or even weeks after it is ready. Our observers become more confident in our progress and, with luck, the pressure on us will be less.
If I gave advice—which you will recall I do not—I’d advise working toward a situation like that.
Small steps make the actual implementation go faster. They make it possible to remove delays. They provide better feedback from management, customers, users. They’re just plain better.
Where we are
Where we are isn’t bad at all. We have a place for controlling shots, ShotController, and we have a couple of decent tests for it. We have some rudimentary logic for firing, and it’s situated in just about the right place, with access, so far, to everything it needs.
And as a bonus, because we wanted to see it happen, we have a rudimentary exploding player. We have a place to put the animation (there are two explosion pictures, not just the one) and a place to destroy the Player once we have a PlayerMaker to bring in the next one.
Our small steps have left us with visible proof of progress and internally, the new code bits are mostly where we need them.
Inheritance, though.
I did make the decision, again, to put a concrete “pass” method in InvadersFlyer, so as to avoid having to implement interact_with_shotcontroller
into all the other classes.
We are between a rock and a hard place here. If we inherit that method, there’s a chance that we’ll forget to implement it in some class that should see it. As the game gets more complicated, that oversight becomes harder to detect in play, and can lead to shipping defects. It has happened in the past, which is why we created the abstract classes to begin with.
But if we do push the method into each subclass, we have to update more and more files and it is tedious, error-prone, irritating, bothersome, and troubling. What we need is a solution that provides the error-detection of using the abstract class facility, and the convenience of only implementing the method where its needed.
While walking down the hall just now, I had an idea. What if the superclass implemented the method something like this:
Class InvadersFlyer(Flyer):
def interact_with_invadershot(self, shot, fleets):
list = [InvaderPlayer, Shield]
assert self.__class__ in list, self.__class__.__name__ + " should implement"
This method would serve to ignore all the calls that can be ignored, and would trap all the calls that should be trapped. The cost, of course, would be a lookup in the list. Maybe we could make it a dictionary for improved speed or something. Maybe we wouldn’t care.
Another possibility would be to declare the necessary classes somehow and check them in a pytest test.
Or, maybe we would declare the ones that do not need to implement the method. That might be safer.
I’ll let that idea perk. We might find a better way that would give us a better approach than either of the two we have now.
Bottom line
A new feature is taking shape. It is visible, it looks similar to what we ultimately want, and we can proudly show it to anyone without crashing the system. Life is good!
See you next time!
-
Wisdom begins when we learn the difference between “that makes no sense” and “I don’t understand”. – Mary Doria Russell ↩
-
I really do try not to give advice, though sometimes I forget and blurt some out. I don’t know your situation, abilities, inclinations, colleagues, or what you had for lunch. Instead I show you what I do and try to explain why I do it, and I try hard to help you understand the what and why. Then, I leave it to you to decide what you want to do in that light. You might try what I do; you might try something completely different; you might continue as before. It’s all good: it’s your life and your decision. ↩