I’d never have guessed there was so much to do in a little game, and so much worth writing about. But I feel that there has been.
This is the 50th Asteroids article. The first one was May 11th. 42 days ago. What’s most interesting, to me, at least, is that there have been things to learn on most of those days. Some of them multiple times. Highlights include:
- Things go so much better test-driven.
- I feel much better, test-driven. Relaxed vs tense.
- Most of the problem we encounter in large code bases occur in small ones like this.
- No matter how much we clean up, there seems always to be something in need of cleaning.
- Doing something, almost anything, works better than just thinking.
- Moving things around creates insight and understanding. Sometimes things may need to move more than once.
- Making things better, we can do. Making them perfect, probably not.
- It’s OK to have a second, third, nth better idea.
I should have listed test-driven about nine times there. I still don’t know how I could have test driven a lot of this, but I’m writing more tests than I could have at the beginning, and they seem always to pay off.
I know that a few folks have read all of most of these and found value. I suspect, however, that the real value to be found is in the doing, trying out these ideas in one’s own world. I hope folks are doing that as well.1
I think there’s still plenty to learn here, although I am pretty sure we won’t get another 50 articles out of the exercise. But let’s get to it.
I was reading the code and documentation for the original asteroids, and in the operating manual I found it saying that the maximum number of asteroids is 10. I had formerly thought it was 11, and that’s what the code says. As a warmup, let’s fix that.
I wonder if we have a test for that. Yes, we have:
_:test("Wave size", function() local u = Universe() u.waveSize = nil _:expect(u:newWaveSize()).is(4) _:expect(u:newWaveSize()).is(6) _:expect(u:newWaveSize()).is(8) _:expect(u:newWaveSize()).is(10) _:expect(u:newWaveSize()).is(11) _:expect(u:newWaveSize()).is(11) end)
Wonderful. Let’s change that:
_:test("Wave size", function() local u = Universe() u.waveSize = nil _:expect(u:newWaveSize()).is(4) _:expect(u:newWaveSize()).is(6) _:expect(u:newWaveSize()).is(8) _:expect(u:newWaveSize()).is(10) _:expect(u:newWaveSize()).is(10) _:expect(u:newWaveSize()).is(10) end)
I could have removed one of the 10s, but decided to leave it in because it adds an ominous sense of forever. This test fails
7: Wave size -- Actual: 11, Expected: 10
I wonder if we can fix that here:
function Universe:newWaveSize() self.waveSize = (self.waveSize or 2) + 2 if self.waveSize > 11 then self.waveSize = 11 end return self.waveSize end
I note that this little function is self-initializing, in that it expects the initial wave size to be nil and if it is starts at 4. Note also that if we wanted it to start at 5 we’d initialize save size to 3.
What’s beginning to happen here is that a tiny object, a WaveSizer perhaps, is nearly ready to be born. It has a single member variable, which is the size of the current wave. I suppose if we were into immutable objects, we’d do it differently, returning a new WaveSizer after each use. Yeah, no. We’re not even going to make a new class for this tiny purpose. But we do notice that there’s interesting stuff going on here, which is independent of everything else in the game.
For now, we’ll just change those 11s to 10s.
function Universe:newWaveSize() self.waveSize = (self.waveSize or 2) + 2 if self.waveSize > 10 then self.waveSize = 10 end return self.waveSize end
And the tests all run. No surprise of course. Let’s commit: wave size max is 10.
Free ship sound
I forgot that when you get a free ship, there’s a sound that goes with it. It’s
U.sounds.extraShip, and because I’ll probably never hear it legitimately, I’ll put a copy here for our amusement.
Let’s play it. No test for this, though it would be nice if there were. That’ll be in
Score, I reckon …
function Score:addScore(aNumber) self.totalScore = self.totalScore + aNumber if self.totalScore >= self.nextFreeShip then self.shipCount = self.shipCount + 1 self.nextFreeShip = self.nextFreeShip + U.freeShipPoints end end
function Score:addScore(aNumber) self.totalScore = self.totalScore + aNumber if self.totalScore >= self.nextFreeShip then self.shipCount = self.shipCount + 1 self.nextFreeShip = self.nextFreeShip + U.freeShipPoints sound(U.sounds.extraShip) end end
We do have a test for the logic and point-setting for the extra ship, and that conveniently plays the sound as a side effect:
_:test("Free ship every N points", function() U = FakeUniverse() local score = Score(3) score:addScore(U.freeShipPoints) _:expect(score.shipCount).is(4) score:addScore(U.freeShipPoints/2) _:expect(score.shipCount).is(4) score:addScore(U.freeShipPoints/2) _:expect(score.shipCount).is(5) end)
Commit: extra ship sound.
It’s Tuesday afternoon. This article started Monday morning.
After I finished these two little things, I undertook to work on targeting. I drew some interesting pictures on the screen and in Paper™, I wrote some code and some tests.
I had two schemes in mind, as I’ve written about before. One was to divide the screen into sections of some kind and predict where the ship would be at some point in the future, and fire into that section. The other scheme was to project the ship position to a few positions, determine firing solutions to those points, and interpolate to pick an actual firing point.
Both those ideas have come to nothing (so far) despite at least an hour yesterday and at least three hours on Tuesday. It could just be that my facility with linear algebra has left me, but I think that what’s going on is that the solution really is that nasty quadratic that I don’t understand, and therefore the other schemes turn out to be infeasible unless I commit to what amounts to that same solution.
I could be wrong. I know I’m confused. I have pages of writing and drawing of me floundering on this problem. I’ll spare you all of it, though I have saved it as part of the Scrivener version of this article.
I’m shipping this version, and the article now. My new plan for targeting is to live with it, or tweak the probabilities or inject an error if it’s too good. I’ll surely keep trying for a bit but I’m pretty sure that until I get an insight, this one isn’t coming together.
Sometimes the bear bites you.
I know that some are now saying ‘folx”, I think to avoid the paternal aspects of the original word, although possibly just to save typing. I’m trying to be woke and all, but I’m not ready for “folx” in my “formal” writing. Not yet. ↩