Features
Python Asteroids+Invaders on GitHub
The Invaders game, while playable, isn’t quite as done as it might be. It’s well past time to be adding some capability. We briefly discuss the point of software development.
- RIL
- Recently I learned that there are people who enjoy reading these articles, but who, for reasons they seem to find sufficient, cannot or will not read, say, all 289 of the Python articles, not counting the 18 or so code-signs ones.
-
That’s fine with me. In some senses they are all the same, covering a real programmer’s real experience while programming, trying to use the most valuable approaches he knows, learning along the way how to refine his technique, how to use new languages and libraries, and demonstrating what it’s like to be where he is, and describing what he thinks and learns. Details vary, lessons vary, but it’s all somewhat the same, yet different every day.
-
And so it is with programming itself. It is all somewhat the same, yet different every day. We do well to face each day with a combination of beginner’s willingness to learn, and an ancient practitioner’s bag of tools. So if you’re here often, or only once in a while, you’re welcome, and I welcome any feedback you wish to send me.
The Point of Software Development
In my view, the point of software development, when it comes down to the bottom, is to develop software. Yes, we might be developing software to cure diseases, or to sell widgets, but when we are in the code mines, it’s all rather similar.
That’s not to say that the ability to build software well is some fundamental true purpose: I think that being with other people, including our team, working well with them, helping each other … all the truly human things we can engage in … those are the most important things.
But much of the time will be spent programming, and I enjoy it and would like others to enjoy it, and because I can write about whatever I want to write about, I write about the building of the software more than any other subject. It’s my joy, and probably the thing about which I know the most.
Features
In my view, when we are in the business of developing software, the most important deliverable is what I call “features”. The people we work for, and the users of our software, want the software to do things, and I call those things “features”. It’s not just that we should do more and more features: some of the best software is good because of how few features, well chosen, it has. But, by and large, the people who pay us want more—or at least different—software.
All the techniques that I write about and enjoy, the TDD, the refactoring, the code refinement, the patterns … all those things generally need to be in the service of producing the changes, the features, that our organization wants. It is my view that if a team can get to a point of delivering “running, tested features” on a regular basis, they have the best chance of thriving inside the larger organization. I have written about that often, as far back as 2004 and probably earlier.
Lately, in my articles, I’ve been focusing on improving the code, discovering better ways, and the like. In a real situation I wouldn’t take a few weeks to refactor and polish the code: I think that would be dangerous to my work situation. I would blend feature creation with code improvement, mostly focusing improvement on where I was working because of requested changes.
So, it is well past the time for some features. Let’s make a list of things we “need”.
Invaders Features
-
Game ends suddenly. When your final gunner is hit, the game flips instantly to the attract mode, which is in fact the asteroids attract mode. You don’t even get a moment to see your final score.
-
Scary sound. The game is supposed to make a scary sort of thumping sound that goes faster and faster.
-
Multiple waves. When you manage to shoot down all the invaders, you’re supposed to get a new rack. Those new racks start a bit lower down each time, increasing your need to take them out quickly.
-
Damage at bottom. When the invaders near the bottom of the screen, they should damage the shields, and when they get to the level of the player, the player should be destroyed. Possibly it’s game over: I’ll have to consider the ancient scrolls.
-
Saucer too fast? Watching old recorded games, it looks to me as if our saucer is moving too fast.
-
Pause on rez. When a new player is rezzed, the invaders, if a column is right over the left side, will fire instantly, often killing the player before he (I) even realizes he’s back. There should be a short delay before firing begins.
There’s probably more that I haven’t thought of, but it’s easy to see that we have work ahead of us.
Let’s look at having the invaders damage the shields.
Invaders Damage Shields
In principle, we “just” need to have an interaction between invaders and shields and go from there. Since the invaders are managed via the InvaderFleet and InvaderGroup, the action will start there.
The shields are instances of RoadFurniture. The bottom line is also RoadFurniture. No problem there, I think.
How do invaders interact with the player shot, which is all they really interact with now?
class Invader(Spritely):
def interact_with_group_and_playershot(self, shot, group, fleets):
if self.colliding(shot):
player.play_stereo("invaderkilled", self.x_fraction())
shot.hit_invader(fleets)
group.kill(self)
fleets.append(InvaderScore(self._score))
fleets.append(InvaderExplosion(self.position))
That’s sent down like this:
class InvaderGroup:
def interact_with_playershot(self, shot, fleets):
for invader in self.invaders.copy():
invader.interact_with_group_and_playershot(shot, self, fleets)
class InvaderFleet(InvadersFlyer):
def interact_with_playershot(self, shot, fleets):
self.invader_group.interact_with_playershot(shot, fleets)
We need a test or three. I propose to work on this at the micro level, perhaps winding up with a test from top down, perhaps not. We should remember to talk about that decision, but the arrangement of interactions is so rote that I feel tests are more trouble than they are worth.
Let’s see what a test might say. The shields are created at [242, 816], [422, 816], [602, 816], [782, 816], so we might as well use those numbers in a test.
def test_invader_damages_shield(self):
maker = BitmapMaker.instance()
sprite = Sprite(maker.invaders)
invader = Invader(1, 1, sprite)
shield = RoadFurniture.shield(Vector2(242, 816))
I get this far and then wonder what to do. I think I’ll work up to it, asking questions like whether they are colliding.
def test_invader_damages_shield(self):
maker = BitmapMaker.instance()
sprite = Sprite(maker.invaders)
invader = Invader(1, 1, sprite)
shield = RoadFurniture.shield(Vector2(242, 816))
assert not invader.colliding(shield)
invader.position = shield.position
assert invader.colliding(shield)
That passes, how nice. What will really happen during game play? I think that the invader will be sent an interact_with_roadfurniture
message, it will check colliding, and if colliding it will damage the shield and carry on. How does an InvaderShot do it?
Um. Actually the RoadFurniture deals with it:
class RoadFurniture(Spritely, InvadersFlyer):
def interact_with_invadershot(self, shot, fleets):
self.process_shot_collision(shot)
def process_shot_collision(self, shot):
if self.colliding(shot):
self._tasks.remind_me(lambda: self.mash_image(shot))
def mash_image(self, shot):
self._sprite.mash_from(shot)
class Sprite:
def mash_from(self, shot):
masher = ImageMasher.from_flyers(self, shot)
new_mask, new_surface = masher.update(self.surface)
self._masks = (new_mask,)
self._surfaces = (new_surface,)
Why is that being done with a remind_me
? Ah. After some research, I recall that if we don’t defer the damage to the shield until after interactions are over, and if the shot checks for collision after the shield does, then since the damage will erase the pixels that the shot hit, the shot thinks it has not hit anything yet. That could be made more clear in the code somehow.
def process_shot_collision(self, shot):
if self.colliding(shot):
self._tasks.remind_me(lambda: self.show_damage_only_after_all_collision_checking(shot))
def show_damage_only_after_all_collision_checking(self, shot):
self._sprite.mash_from(shot)
That should give me a clue next time I pass through here.
Anyway, I’m not sure we should handle invader collisions here. There is no interact_with_invader
, because the invaders are moderated by the InvaderFleet and InvaderGroup. So I think it’ll be better to do the shield damage from the invader side.
But how can we test it? Let me put in the code and look at it.
class InvaderFleet(InvadersFlyer):
def interact_with_roadfurniture(self, shield, fleets):
self.invader_group.interact_with_roadfurniture(shield)
class InvaderGroup:
def interact_with_roadfurniture(self, shield):
for invader in self.invaders.copy():
invader.interact_with_roadfurniture(shield)
class Invader(Spritely):
def interact_with_roadfurniture(self, shield):
if self.colliding(shield):
shield.sprite.mash_from(self)
@property
def explosion_mask(self):
return self.sprite.mask
This turns out to work in the game. The mash_from
code needed an explosion mask, so I just returned the sprite mask a second time. A bit of a hack, I grant.
Let’s try something in our test. We’ll check to see if the mask has been replaced.
def test_invader_damages_shield(self):
maker = BitmapMaker.instance()
sprite = Sprite(maker.invaders)
invader = Invader(1, 1, sprite)
shield = RoadFurniture.shield(Vector2(242, 816))
assert not invader.colliding(shield)
invader.position = shield.position
assert invader.colliding(shield)
old_mask = shield.sprite.mask
invader.interact_with_roadfurniture(shield)
assert shield.sprite.mask is not old_mask
Perfect! The mashing replaces the mask (but not the surface, which it just damages), so this test is righteous.
Commit: Invaders eat shields when they get there.
Let’s review and sum up.
Summary
This went very nicely. We were able to start with a test for collision, and (after actually coding the solution) were able to see how to extend the test to check the actual behavior of invader-shield collision.
The code itself just trips right down the standard path from game to fleet to group to invader. It is, however, a bit odd that the invader issues damage to the shield. We would normally expect that the shield would, upon detecting the collision, issue damage to itself. The usual fashion is for objects to manage themselves.
To do that we would need to introduce a new method, probably interact_with_invader
, and possibly make it one of the official methods in the hierarchy. Of course, we default all those to pass
so it wouldn’t really affect any other objects. In the case of a shot hitting the invader, the invader does send hit_invader
to the shot, whereupon the shot removes itself, so perhaps we would do well to send a similar message to the shield.
I might make a note of that, but I think that what really ought to happen is that we’d make the Invader object a first-class Flyer and allow all of them to interact with everything. I am reluctant to do that because it would generate about 3000 more interactions per cycle and that seems undesirable. Another hack would be to allow the invaders to interact with other objects but somehow remove their one on one interactions with each other from the pairs list. Also a bit undesirable.
For now, we’ll let that concern perk. What we have isn’t bad, it’s just not great, and there’s even a test to lead us to how it works.
We’ve shipped a feature, and it won’t be our last. See you next time!