Python 204 - Moving Invaders
Python Asteroids+Invaders on GitHub
I believe that I’ve made a mistake. This will not be my first, nor, I fondly hope, my last. CW: Contains brief Jira abuse.
Our mission today is to make the Invaders move similarly to how they move in the original game, one at a time until all have moved. In the original game, this was done because in every refresh cycle, there wasn’t time enough to move them all. We will echo that behavior, not because of time, but because we want our game to look much like the original. And I think I’ve made a mistake that will require a bit of adjustment.
Somewhere in the descriptions of the old program I found the statement, something to the effect that the individual invaders were drawn relative to the first one. I think I had that idea in mind when I arranged for our invaders to be drawn relative to the position of the InvaderFleet object. I think that’s a mistake, since if they move separately, they can’t be kept in the same relationship to any fixed point.
Here is update:
class InvaderFleet(Flyer):
def update(self, delta_time, _fleets):
self.origin += self.step*delta_time
for invader in self.invaders:
invader.move_relative(self.origin)
Let’s follow that down. We might be nearly OK after all.
class Invader:
def move_relative(self, origin):
pos = Vector2(origin.x + 64*self.row, origin.y - 64*self.column)
self.rect.center = pos
OK, each invader has her own row and column, and those stay fixed. When we shoot an invader, they don’t close ranks, they stay spaced out as they were. I’m not saying they’re buzzed, just that their spacing from one to another doesn’t close up to fill a gap.
Let’s think about what we want. I think we are OK.
We’re going to change update to move only one invader at a time. We want to move them starting from the lower left one, across the ranks, until we’ve moved the last one. We should keep the InvaderFleet origin constant while we move them, and only update it when we are about to start a new move cycle.
One mistake I’ve surely made is that the invaders are presently set up in column order, not row order. I had something in mind about using that information to find the left-most or right-most invader easily. But we’re handling the edges differently than I had been planning—Thanks, Bruce!—so we can change that.
Reminder to self: Don’t forget to deal with invaders disappearing between moving the first and moving the last.
Let’s just move them individually first, and then deal with the order separately. Might as well find out whether we really need the change.
So we have a list of invaders in the fleet. We should have an index of the invader whose position should change, starting at zero and incrementing to however many there are. I think I have to ignore the reminder above, it’s distracting me.
When we’ve moved the last invader, we should move the fleet’s origin and start back at zero.
So: we’ll init a new variable, next_invader
or something, and start it at the length of the invaders list.
class InvaderFleet(Flyer):
def __init__(self):
self.invaders = [Invader(x//5, x % 5) for x in range(55)]
self.origin = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
self.step = Vector2(30, 0)
self.reverse = False
self.update(0, None)
self.next_invader = len(self.invaders)
Now in update …
def update(self, delta_time, _fleets):
if self.next_invader >= len(self.invaders):
self.origin += self.step*delta_time
self.next_invader = 0
self.invaders[self.next_invader].move_relative(self.origin)
self.next_invader += 1
If we’re past the end, we update origin and start at zero. We move one invader and increment next. I think this might actually work. But six tests are failing.
Curious error:
def update(self, delta_time, _fleets):
> if self.next_invader >= len(self.invaders):
E AttributeError: 'InvaderFleet' object has no attribute 'next_invader'
How can that be? I don’t know. Roll back.
Oh, now I see. I called update before setting next_invader
. Could have saved that rollback. Do again.
class InvaderFleet(Flyer):
def __init__(self):
self.invaders = [Invader(x//5, x % 5) for x in range(55)]
self.origin = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
self.step = Vector2(30, 0)
self.reverse = False
self.next_invader = len(self.invaders)
self.update(0, None)
But what is that update about? Something about setting all invaders to their first position. I’m going to try living without that, but I suspect I might get a screen glitch. Comment it out. Two tests fail. OK, we want the invaders properly positioned. Let’s do it in line and then extract it.
I am thrashing just a bit, trying to get back to working. I think I’ve got a handle on it: we’ll see.
class InvaderFleet(Flyer):
def __init__(self):
self.invaders = [Invader(x//5, x % 5) for x in range(55)]
self.origin = Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
self.step = Vector2(30, 0)
self.reverse = False
self.next_invader = len(self.invaders)
# self.update(0, None)
for invader in self.invaders:
invader.move_relative(self.origin)
We’re green. Change the update again:
def update(self, delta_time, _fleets):
if self.next_invader >= len(self.invaders):
self.origin += self.step*delta_time
self.next_invader = 0
self.invaders[self.next_invader].move_relative(self.origin)
self.next_invader += 1
Still green. Note to self: more tests for update.
I want to see this on the screen. They do move individually, but column-wise, as I suspected. I’ll fix that up before I waste space on a video for you.
class InvaderFleet(Flyer):
def __init__(self):
self.invaders = [Invader(x%11, x//11) for x in range(55)]
This breaks a test or three.
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.row == x
assert invader.column == y
count += 1
This is checking for the former column-first order. I think we reverse those two for
statements to fix this one. Right. Next:
def test_fleet_origin(self):
fleet = InvaderFleet()
assert fleet.origin == Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
invader = fleet.invaders[5*5] # bottom row middle column
assert invader.position.x == 512
OK, that’ll be an entirely different guy, number 5.
def test_fleet_origin(self):
fleet = InvaderFleet()
assert fleet.origin == Vector2(u.SCREEN_SIZE / 2 - 5*64, 512)
invader = fleet.invaders[5] # bottom row middle column
assert invader.position.x == 512
Right. One more:
def test_fleet_motion(self):
fleet = InvaderFleet()
assert fleet.step == Vector2(30, 0)
pos = fleet.invaders[0].position
fleet.update(1.0, None)
new_pos = fleet.invaders[0].position
assert new_pos - pos == fleet.step
fleet.at_edge()
fleet.end_interactions(None)
assert fleet.step == (Vector2(-30, 0))
Hm. What is the failure here?
Expected :<Vector2(30, 0)>
Actual :<Vector2(240, 0)>
My bad. I hammered the step because it wasn’t moving the gals as rapidly as I wanted. Let’s not assert that constant. Instead let’s set it in our test, we have a private copy anyway.
def test_fleet_motion(self):
fleet = InvaderFleet()
fleet.step = Vector2(30, 0)
pos = fleet.invaders[0].position
fleet.update(1.0, None)
new_pos = fleet.invaders[0].position
assert new_pos - pos == fleet.step
fleet.at_edge()
fleet.end_interactions(None)
assert fleet.step == (Vector2(-30, 0))
Green. Commit: Invaders move one at a time. Motion step needs adjustment.
Here’s a video of the action.
A thing that happened, that I didn’t think about, was that moving them one at a time slows things down. When I was moving the entire fleet at once, the motion step was 30, comes down to 30 pixels per second for all of them. Now we’re moving one at a time, we are almost 60 times slower (55 invaders, one each sixtieth). I’ll have to watch the original videos and see what they do, how fast they move, how far in one step.
Reflection
This went fairly well. My worries about moving relatively were ill-founded: I made a mistake when I thought I had made a mistake. But I had a few more opportunities, including calling update before I had set up the next_invader
. That cost me a rollback, but when you don’t see the bug right away and you haven’t changed much, it can be the right thing to do. Even if I was wrong to do it, it only cost me about a minute.
Tests fixed up OK. We could probably use more: we have no test that checks the incremental aspect of motion.
I noticed, when I set the move to quite large, that the boundary checking, with our Bumper objects, may need some adjustment: if the invaders are moving in big steps, they can be pretty far across the Bumper in one step, so they may need to be moved inward for best results. That’s certainly easily tuned.
We still need the downward step at the edge. I think that’s going to be tricky. Since we’re moving the invaders individually, we’ll discover the need to reverse multiple times before we actually get reversed. In fact, I think we have a subtle bug even now. I think if we had an even number of rows we would not reverse correctly.
A quick check verifies that. We check for any invader hitting the bumper. When they were moving as a unit, probably five hit the bumper at the same time. Now, one will hit. We reverse. Number 2 hits, we reverse back. 3, reverse, 4, reverse back, 5, reverse. With four rows they run off the screen, because four reversals is no reversal at all.
A defect without a mistake?
When I thought I had made a mistake I was wrong, but this seems like a defect for sure.
What we have, of course, is that an extension to behavior has broken something that was perfectly good under the prior scheme. What is a bit distressing is that we had no test fail. I had to think of the problem. Counting on me, or any programmer, to think of everything, well, that’s a bad bet.
I’m done for the morning, though, so let’s make a few notes:
- Write test for incremental motion
- Write test for incremental reversal
- Write test for stepping down.
- Fix reversal to work correctly for incremental motion. OK for now.
- Adjust InvaderFleet origin downward on reversal.
The latter two are implied by the first three but I wanted to have the reminder.
- Aside
- I have to share this. You may recall that my “Jira” is a bunch of yellow sticky notes on my keyboard tray. I was going to write the things above on stickies and put them on the tray.
-
There are so many stickies on the tray that there is no room for more!
-
I’ve seen real Jira systems get so full that no one knew what was up. Now it has happened to me. Most of them are no longer relevant. I’ll have a look and toss them. Maybe. Sometime soon. Like your Jira? I hope not, but across the nation, I know how I’d bet.
Enough for today. Our invaders can move incrementally, we have some progress to show the customer.
See you next time!