Thinking about how we’ll replace a destroyed player has led me to a strange idea. Perhaps too strange, yet fascinating.
We’re on the way to replacing destroyed players. We have the new TimeCapsule object that holds another flyer, and after a timed interval, adds that object to the mix. A TimeCapsule with a two second delay, containing an InvaderPlayer, will add that player in two seconds.
In the game, you get some defined number of player instance before game over, typically three or four. The game displays player instances along the bottom of the screen, and after one is destroyed, one disappears from the bottom and shows up in the player area. I was thinking about how to do that.
And I thought, what if there were new objects, instances of ReservePlayer, in the mix? Suppose there were three of them, and suppose they somehow knew their instance number and used it to space themselves along the bottom of the screen, 1, 2, 3. You’d have your row of available reserve players.
And I thought, so there would be a PlayerMaker in the mix, always noticing the reserve players and remembering the one with the highest instance number. The PlayerMaker would also watch for real Players. If it sees no real players, but has seen a reserve player, it would do these things:
- Add a TimeCapsule with a new Player instance;
- Remove the ReservePlayer.
Voila! Reserve player disappears, two seconds later you have a new one to play with. That’s almost good. But not good enough, because we really want the reserve to seem to move. From the human player’s viewpoint, you’re shot down. Your eyes go to the reserves. You see at least one. Two seconds later, a reserve disappears and appears in the playing area. You’re back in business.
- I only thought of that timing issue as I was writing this. In my initial idea I really was thinking of removing the reserve in the PlayerMaker, but that just won’t quite do. What options have we for dealing with this? Here are some:
- Make PlayerMaker more sophisticated, removing the ReservePlayer later somehow;
- Pass the Reserve to the Player on creation. If a Player receives a Reserve on creation, it removes the reserve. That would manage the timing nicely.
- Enhance TimeCapsule to have, not just an instance to add, but an instance to remove. That would be straightforward and now that I’ve thought of it, seems like the right thing to do.
At this point …
At this point, I think we have a decent starting plan for reserve players. I even rather like the name “reserve player”.
But Then ..
But then it starts to get weird.
I was thinking about how we’ll get the Fleet to pause when there is no player. The behavior we want is that the instant the player is hit, the fleet stops, and it doesn’t start again until a bit after the new player returns, taking up motion right where it left off. Again we have options, and the most straightforward one is for the fleet to notice no player present and stop, and then somehow have a short timer and only move when a) there’s a player and then b) the timer has elapsed.
The code would be kind of like: if there’s a player and the timer is not elapsed tick the timer; and if the timer is elapsed start moving, and oh by the way probably don’t tick the timer when you’re moving because we don’t really want it to overflow. (Not that it likely would, but there’s a principle here.)
So I had this strange idea. Suppose that the Fleet was rigged to accept start and stop messages from some other object. Then we could have some kind of DelayedMessage object in the mix, like that thing where you give a letter to your lawyer to be mailed to the FBI only after you’ve had two months to heal from the plastic surgery changing your appearance to be quite different really from the wanted posters, and only after you are well ensconced, hidden away in your new lair on the secret island covered by perpetual fog and a mysterious electrical effect that hides it even from radar, and populated entirely by attractive and compliant servants who cater to your every desire and I do mean every. What, only me? Well, anyway …
So everyone who might get one would listen for DelayedMessages and if the message were addressed to them, would perform accordingly. Then, nothing to it, Some object in the mix could notice that there are no players and if not, could send a zero-delay message saying “fleet stop”, and when the PlayerMaker created the TimeCapsule to move a player from reserve to active, it could create a DelayedMessage … except now we see that we don’t really need a Delayed message, we can just have a regular Message and put it in a TimeCapsule if we want a delayed message.
Like I said, it gets weird.
Up until now, we have had objects keeping track of things they care about. Some of them are specialized, like the ShipMaker or WaveMaker in Asteroids. Some are the actual active objects. For example, the InvaderPlayer assumes when interactions begin that it will be free to fire a shot. But if it interacts with a PlayerShot (which it will have fired earlier), it cannot yet fire: there can be only one. So it sets itself to not free to fire.
In one more intricate case, if an invader hits a side bumper, it sends a message back, not to the InvaderGroup of which it is a member but to the InvaderFleet that holds the group. It is passed that object in a specialized interaction call. So it’s already a bit weird sometimes.
I think it might be too weird, and I have a pretty good reason for suspecting that.
The core notion of this “decentralized” design is that Flyer objects, in the Fleets mix, look out for themselves. It’s as if they are all independent operators. They don’t talk with each other. They can observe that there are others, because each pair of objects gets a chance to interact, but they almost always limit what they do to dealing with their own concerns, and possibly creating new objects into the mix. Generally, the only messages sent to other objects that they’ve seen have to do with getting information, such as finding their positions, not telling them what to do.
A TimeCapsule today does not violate that rule, and even if we let it remove the ReservePlayer, it’s not a big violation. I would like it better, from a design consistency point of view, if the ReservePlayer had an interaction of its own that caused it to remove itself.
What if the Player had a reserve number? The ReservePlayer could interact with Player and if the Player’s number matched the Reserve’s number, the Reserve would remove itself. That matches the design pretty well.
Maybe we’ll follow that path a while. But I want to digress further, because I’m encountering another issue and it’s bugging me.
What’s bugging you, Ron?
What’s bugging me is that in every InvadersFlyer, there are as many
interact_with_(class) methods as there are subclasses of InvadersFlyer, and similarly for AsteroidsFlyer. Each class is required to implement each of those methods if we make it an abstract method. The spirit of not inheriting implementation tells us to do that. The upshot of that is noise in the code.
Take InvaderPlayer for example. It’s pretty complicated on its own, with motion and firing and all that. The class is 137 lines long and among those we have these 29 lines:
def interact_with_bumper(self, bumper, fleets): pass def interact_with_invaderexplosion(self, explosion, fleets): pass def interact_with_invaderfleet(self, fleet, fleets): pass def interact_with_invaderplayer(self, player, fleets): pass def interact_with_playerexplosion(self, _explosion, _fleets): pass def interact_with_shield(self, shield, fleets): pass def interact_with_shotcontroller(self, controller, fleets): pass def interact_with_shotexplosion(self, bumper, fleets): pass def interact_with_topbumper(self, top_bumper, fleets): pass def tick(self, delta_time, fleets): pass
Over twenty percent, one fifth of this object is useless, redundant, wasteful, irritating, confusing noise! When I want to know what objects interact with the shot controller, I can’t just ask PyCharm for implementations of
interact_with_shotcontroller and get the ones I want. I have to trek through all the empty ones as well.
It’s a pain, not a sharp one, not one that causes errors, just a dull ongoing pain that slows me down, drains energy, probably causes me not to look for things that I’d do well to look for, and sometimes even hides something I should remember in a forest of pass pass pass that causes me to miss what I should have seen.
However, it seems that it is safer. In the past, when these methods were not abstract and were thus optional to implement, there was at least one case where I should have implemented interact with something and did not, resulting in a somewhat subtle defect where something should have happened but didn’t. Imagine forgetting to implement the interaction between the Asteroids ship and the Saucer, so that when the Ship runs into the Saucer it doesn’t explode. Defect in the game and one that could easily slip through testing.
So, it would seem that making the methods abstract would require the programmer to implement
interact_with_saucer everywhere and it would thus require them to think about the issue and have a better chance of remembering to deal with the issue. Still not certain, but it seems that it would help.
You are waffling, sir!
Yes, I am waffling1. There are strong forces pulling in each direction and every solution I’ve found leaves things out of balance. So far, I have no solution that I like. I am coming to suspect that something more “declarative” is needed, so that when we create a new kind of Flyer, we declare as part of that process all the classes with which it must interact. That would be a good time to think about that, because we’re really focused on that new object, what it’s for, and how it should work. I’m not quite sure how to do that with this model.
In a true publish-subscribe model we would have a place to stand to say these objects may subscribe to this message, these other ones must subscribe to it, and we could probably enforce that rule. We’d have to be a bit more invasive with this implementation, where we simply double-dispatch to every pair in existence, so that each object is subject to hearing from each other type of object (and even all the objects of its own type2).
Ah well. I guess that’s all for another day. For today, are we set to deal with Reserve Players?
The idea goes something like this.
- Rez N ReservePlayer instances, a new class, with each coin, giving them an integer index that they’ll use to select their position along the bottom.
- Rez one PlayerMaker, a new class, which observes ReservePlayers, keeping track of the one with the highest index. PlayerMaker also keeps track of the Player.
- If PlayerMaker finds no Player, and it has not seen a ReservePlayer, Game Over.
- If PlayerMaker finds no Player and it has seen a ReservePlayer, it rezzed a TimeCapsule with a new Player to add, and the ReservePlayer to remove.
- TimeCapsule, after its delay, removes the Reserve and adds the Player. Play resumes.
That will deal with everything except locking up the InvaderFleet when no player is on the screen. We can handle that as a separate issue easily by having InvaderFleet watch for the player and only move if it sees one. That would cause motion to stop as soon as the Player is hit, and to restart as soon as it comes back. If we need a brief delay, we’ll have to deal with that somehow as yet another separate issue in some way yet to be invented.
There is also the matter of the PlayerExplosion. We remove the Player instantly now upon its being hit. It used to just sit there exploding, and now we need a new object to do the animation, which we’ll have the Player emit when it is hit.
A Starting Plan
Let’s do the PlayerExplosion first. That will give us an infinite game where the hit player explodes and then returns two seconds later, off to the left. Then maybe we do freezing the invader fleet. Then the PlayerMaker and ReservePlayer stuff, probably ReservePlayer first. They’ll just sit there until PlayerMaker is done, which will be fine, that’s all they do anyway.
We have explosion code in Player now, it’s just that it never runs.
class InvaderPlayer(InvadersFlyer): def hit_by_shot(self, fleets): self.explode_time = 1 fleets.remove(self) fleets.append(TimeCapsule(InvaderPlayer(), 2)) def draw(self, screen): self.explode_time -= 1/60 if self.explode_time > 0: player = self.players[random.randint(1,2)] else: player = self.player screen.blit(player, self.rect)
Time was, when we set the
explode_time, we’d do that flashing of the two player drawings #1 and #2, which shows a flickering explosion. Now, since we removed
self, that never gets a chance to run. It is that code that we need in our new PlayerExplosion.
There’s not much to TDD, but let’s at least start with a test.
class TestPlayerExplosion: def test_exists(self): PlayerExplosion((100, 200))
I’m glad I created this trivial test, because focusing on the use, the creation of the object, caused me to remember that the explosion needs to have a starting position wherever the destroyed Player was. So that test is actually helping me. Let’s implement.
class PlayerExplosion: def __init__(self, position): pass
The test passes. Let’s drive out a bit of behavior. We could test that it has the position. A bit trivial. Or we could make the explosion inherit properly. Let’s just do that and that will cause PyCharm to push us to do all those methods I’m complaining about. (We will also be faced with the dilemma of whether to make everyone else deal with this guy. I think we will not do that yet.)
The class blossoms to 55 lines, with only these marginally useful:
class PlayerExplosion(InvadersFlyer): def __init__(self, position): pass def interact_with(self, other, fleets): pass def draw(self, screen): pass
And a test is failing, probably that checker one. Yes. I make this class an exception to the testing:
def check_class(self, test_class): subclasses = get_subclasses(test_class) ignores = ["BeginChecker", "EndChecker", "TimeCapsule", "PlayerExplosion"] subclasses = [klass for klass in subclasses if klass.__name__ not in ignores] attributes = dir(test_class) pass_code = just_pass.__code__.co_code for klass in subclasses: name = klass.__name__.lower() self.check_top_class_has_interact_with_each_subclass(attributes, name, test_class) self.check_interact_with_present_and_not_just_pass(klass, name, pass_code)
What else could we test à la TDD? Not much, the thing just draws itself for a time and then dies. Oh, we could tick the time, that would be somewhat interesting. No, but me a break here, I’m not going to do it. I’m grumpy this morning and you can’t make me test if I don’t want to. I’m sure nothing will go very wrong.
We’ll steal some code from Player, who knows how to draw the explosion now.
- This went OK but it did not go perfectly. I’ll show the code and then reflect on what went wrong.
class PlayerExplosion(InvadersFlyer): def __init__(self, position): maker = BitmapMaker.instance() self.players = maker.players # one turret, two explosions self.player = self.players self._mask = pygame.mask.from_surface(self.player) self._rect = self.player.get_rect() self._rect.center = position # error self._explode_time = 1 @property def mask(self): return self._mask @property def rect(self): return self._rect # error twice def draw(self, screen): player = self.players[random.randint(1,2)] screen.blit(player, self.rect) def tick(self, delta_time, fleets): self._explode_time -= delta_time if self._explode_time <= 0: fleets.remove(self)
Nice little class, fairly simple. Works perfectly, as I’ll show in a video below. And I made at least three mistakes that I only detected when testing.
(1) I had put
return None in the rect property and that caused an error because the blit had no rectangle. I “fixed” that by saying
return self.player.get_rect. (2) That failed in a strange way. The code all ran but no explosion appeared. I realized that the explosion was probably off screen somewhere because I had not set the position in
__init__. So I added that position setting line. (3) And it still didn’t work, because
self.rect was returning a rectangle that had not had the center properly set.
So three defects, none of them detected by tests, and a fair amount of confusion, first printing just that the thing was running and then printing the rect to see that it was wrong.
All that could have been done in TDD. Like this, perhaps:
def test_position(self): player_position = (100, 200) explosion = PlayerExplosion(player_position) assert explosion.position == player_position
That test will not run. There is no
position property on PlayerExplosion. I might still have made the mistake of just setting a position member, but that seems unlikely. More likely I’d look at Player and see this:
class InvaderPlayer(InvadersFlyer): @property def rect(self): return self._rect @property def position(self): return Vector2(self.rect.center)
And that would trigger me to set up the rect and its center properly, with any luck at all.
So there we have it. I swear to you that I fully intended to get it right and say, see, sometimes an experienced programmer like me can get away without writing a test, if the object is small enough and graphical enough. And we would all have seen that a rigid insistence of test driving even when it seems unnecessary is just for beginners, and experts like ourselves can use our own judgment on when to TDD and when not.
Well that plan is right down the tubes, isn’t it? I have more programming experience than any two of you, and there’s just about no object simpler than this one, and I flubbed the simple implementation not once, not twice, but three times, and a simple test would almost certainly have avoided the problems entirely.
Huh. I really think I’d better try harder to use TDD
more of the time, no, all the time except ok, all the time. I’m sure I’ll still skip tests and still get in trouble. I am not a perfect example other than a perfect example of a sincere person with flaws. That one, I have covered.
Commit This Baby
Commit: PlayerExplosion appears for one second before new player arrives.
I deserve a rest. Let’s sum up.
We’re one step closer to having players explode and be replaced by reserve players, until the reserves run out. The new PlayerExplosion is simple. The Player is simplified by not having to do the explosion, it just files an explosion with the fleets object and the explosion takes care of itself.
We learned a new lesson for the 371st time, that writing a test pays off with such a high probability that we should just plain default to always doing it. Sometimes it seems difficult. Yet there is strong evidence that it nearly always pays off and we might do better to make it easy to test the thing rather than try to code without the tests.
I don`t mind saying that if you had told me this back in 1962, I would not have believed you. But in 1962, I had to walk down several flights of stairs in the middle of the night, into the Strategic Air Command underground, and sweet-talk Sergeant Whittaker3 into letting me sit at the console of the IBM 7090 and key things into it, or at least punch cards and hand in jobs to be run immediately. TDD really wasn’t practical then. Today, my wrist watch is more powerful than that 7090 and my Mac can run over 200 tests in a second or two, and does so every time I pause my typing. It’s a different game today than it was in 1982. Today, TDD almost invariably pays off for me.
Will it pay off for you? Well, I would predict that after a bit of practice, it would. But determining that is up to you.
I know what would work best for me … and I keep trying to get there. Slowly I improve … step by step … inch by inch …4
Next time, I think we work on ReservePlayer. I could be wrong. As you’ll have noticed, I frequently am. But usually I become less wrong again soon after. Well, often. Well, sometimes.
See you next time!
I have waffled on this many times. If I had saved these waffles, I could have made a fortune opening a breakfast place. I mean seriously, there must be a better way to resolve this! ↩
Invader: Hi! Other Invader: Oh, hi! How are things where you are? Things are getting hot here. ↩
If you want to do something odd, or just you need something done in a military situation, there’s no one better than a grizzled sergeant with a lot of stripes on his sleeve. And even if you’re a young pup, like I was, he’s experienced keeping young officers from getting into trouble and will be able to help you out. He’ll be rolling his eyes, but anything you need done, someone with a lot stripes on his sleeve is the one to ask. ↩
Niagara Falls. Or, if you prefer, Buffalo. ↩