Asteroids 46
After some discussion of things we might do, we decide to do GAME OVER. The results will surprise you.
Well, I don’t really have a plan today. There are a few possibilities that come to mind:
- Ship Spawning
- The game is supposed to start you off with three or four ships, and when your last ship is destroyed, GAME OVER. You earn a new ship every 10,000 points.
- Hyperspace
- There should be a hyperspace button that you can press when the going gets too tough. Your ship disappears, then after an interval, appears somewhere randomly, hopefully better.
- Missiles Destroy Saucer Missiles
- I don’t know that the original game supported this but yesterday I could have saved myself with this feature. Maybe.
- Small Saucer
- There are two saucer sizes. The larger one is supposed to fire randomly, the smaller more accurately. Right now, when the “fire accurately” occurs, you’re a goner. There is just about no escaping accurate fire.
- Less Accurate “Accurate”
- We could invent a semi-accurate way of deciding where to fire saucer missiles. That might be fun, but mostly it is a paper exercise, though perhaps the code would be interesting as well.
- Improved Timing Logic
- Timed behaviors are a bit ad hoc. Some use tweens, which I really like except when they fire after its too late. And they are hard to interlock. Some timing is done in line here and there, based on storing a time and comparing it to current time during the display cycle. I think there is more than one way of doing it now. We could probably build a nice centralized timing mechanism.
- Game Adjustments
- There might be some value to some control switches and sliders for the game, to set general speed, and so on.
- Portrait Mode
- The game might be better in portrait mode on the iPad, where the play area could remain square and controls could be outside the play area.
- Turn control
- I’m not entirely happy with the current control for rotating the ship, which instantly sets ship rotation to the position of your finger on a big disc. The original game had a fixed rate of rotation left or right, and two buttons. That might be better if we can find an easy-to-play scheme. User reports are mostly playing with two thumbs, so single controls are perhaps better.
- Pluggable Controls
- The controls are already fairly isolated, and a bit more work might make it easy for people to plug in alternative control mechanisms. Not that many people would. But there could be a few built in, see Game Adjustments above.
Boring
None of these sound like much fun to me this morning. I suspect the finite ships til game over is the biggest bit of functionality we need. I guess we’ll do that.
Game Not Over Yet
Well, what do we need? We need some number of ships per game. The original had DIP switches allowing selection of three or four ships per game. We need to count that number down when a ship dies, we need to count it up whenever the score rolls over another 10,000 (not often, the way I play). And we need to display the remaining number of ships just below the score.
Speaking of score, it seems that we might do well to make this capability part of the Score
object. It already displays itself, in roughly the right area, and it could even handle the bonus ships.
Which reminds me, there is a bonus ship sound that is played when you get a new ship.
We’ll perhaps have a small amount of fun dealing with detecting the rollover just once per 10,000.
Initial design decision: This capability will be put on Score
, and we’ll start with the game-play aspect first.
We’ll surely have to remove or at least change the respawning tween in Ship
, and it’d be nice if we’d remember not to run the saucer when there is no ship to harass. That may or may not turn up as part of this job. I suspect not.
Here’s an idea. What if we actually rely on Score
to spawn ships. When it’s time to get one, we send a message to Score. If there are ships available, it spawns one and returns true
and otherwise returns false
. Or maybe, hear me out now, maybe it just posts GAME OVER and puts the game into attract mode.
Hmm, I like that. Let’s try it.
Doing the doing …
Right now, Main
handles game start:
function draw()
U:draw(ElapsedTime)
if U.attractMode then
pushStyle()
fontSize(50)
fill(255,255,255, 128)
text("TOUCH SCREEN TO START", WIDTH/2, HEIGHT/4)
text(Console, WIDTH/2, HEIGHT - 200)
popStyle()
end
end
function touched(touch)
if U.attractMode and touch.state == ENDED then U:startGame(ElapsedTime) end
if touch.state == ENDED or touch.state == CANCELLED then
Touches[touch.id] = nil
else
Touches[touch.id] = touch
end
end
For now, let’s leave that alone. I’m already feeling a little troubled by delegating too much to Score. But let’s see what happens.
Don’t be afraid to try things.{:.pull-quote}
One thing to learn about programming is that we mustn’t be afraid to try things. The other side of that coin is that we must expect to need to change things. Our first idea may be good enough to go with, but it’s not likely to be the best idea we ever had.
So we try, we learn, and we improve ourselves and the code.
But I digress. Universe:startGame
looks like this:
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.saucerTime = currentTime
self.attractMode = false
self.objects = {}
self.indestructibles = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
createButtons()
Ship()
Score()
self:newWave()
end
We’d better look at Score
about now as well. It’s pretty simple and should be easy enough to deal with:
Score = class()
local Instance = nil
function Score:init()
self.totalScore = 0
Instance = self
U:addIndestructible(self)
end
function Score:draw()
local s= "000000"..tostring(self.totalScore)
s = string.sub(s,-5)
pushStyle()
fontSize(100)
text(s, 200, HEIGHT-60)
popStyle()
end
function Score:instance()
return Instance
end
function Score:addScore(aNumber)
self.totalScore = self.totalScore + aNumber
end
function Score:score()
return self.totalScore
end
function Score:move()
end
Let’s see if we can TDD this. I envision that Universe
will be changed to instantiate Score
and then tell Score
to spawnShip
. It should return true
.
Let’s also say that Score
is passed the number of ships on init. Let me try a test:
_:test("spawn only N ships", function()
s = Score(4)
assert(s:spawnShip()).is(true)
assert(s:spawnShip()).is(true)
assert(s:spawnShip()).is(true)
assert(s:spawnShip()).is(false)
end)
This isn’t much of a test, is it? I stalled thinking about how I was going to make sure that ships got spawned, and so on. That told me to test this much and then write another test.
This test fails of course, on spawnShip
:
28: spawn only N ships -- TestAsteroids:276: attempt to call a nil value (method 'spawnShip')
function Score:spawnShip()
return true
end
28: spawn only N ships -- TestAsteroids:276: attempt to index a boolean value
Hmm, what’s that? Ah. Fool:
_:test("spawn only N ships", function()
s = Score(4)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(false)
end)
28: spawn only N ships -- Actual: true, Expected: false
So far so good.
function Score:init(shipCount)
self.shipCount = shipCount or 1
self.totalScore = 0
Instance = self
U:addIndestructible(self)
end
function Score:spawnShip()
if self.shipCount <= 1 then return false end
self.shipCount = self.shipCount - 1
return true
end
Test runs. Of course no ships are created.
Narrator (v.o.): He’s not as right as he thinks he is …
I want to test that in a fake universe, which presently doesn’t even count created objects. Let’s imagine that it does and test this way:
_:test("ships actually spawned", function()
U = FakeUniverse()
s = Score(3)
s:spawnShip()
s:spawnShip()
s:spawnShip()
s:spawnShip()
_:expect(U.objectCount).is(3)
end)
_:test("ships actually spawned", function()
U = FakeUniverse()
s = Score(3)
s:spawnShip()
s:spawnShip()
s:spawnShip()
s:spawnShip()
_:expect(#U.objects).is(3)
end)
This fails as expected, returning zero.
function Score:spawnShip()
if self.shipCount <= 1 then return false end
self.shipCount = self.shipCount - 1
Ship()
return true
end
I expect this to work or I’ll know the reason why.
29: ships actually spawned -- Actual: 2, Expected: 3
I don’t know the reason why. First I enhance the test:
_:test("ships actually spawned", function()
U = FakeUniverse()
s = Score(3)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(false)
_:expect(#U.objects).is(3)
end)
If the upper asserts don’t work, well, hmm.
Wait. Even that first test should be failing. I’m off by one.
Narrator (v.o.): <clears throat meaningfully>
Compensating errors. Very rare in TDD. That’s a thing I haven’t seen in a long long time.
_:test("spawn only N ships", function()
s = Score(4)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(false)
end)
_:test("ships actually spawned", function()
U = FakeUniverse()
s = Score(3)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(true)
_:expect(s:spawnShip()).is(false)
_:expect(#U.objects).is(3)
end)
function Score:spawnShip()
if self.shipCount <= 0 then return false end
self.shipCount = self.shipCount - 1
Ship()
return true
end
Now the tests pass. The game needs to use this:
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.saucerTime = currentTime
self.attractMode = false
self.objects = {}
self.indestructibles = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
createButtons()
Score():spawnShip() -- <---
self:newWave()
end
And let me fix Ship right now before I forget:
function Ship:die()
local f = function()
if U.attractMode then return end
Ship()
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
if not U.attractMode then
tween.delay(6, f)
end
end
Becomes …
function Ship:die()
local f = function()
if U.attractMode then return end
Score:instance():spawnShip() -- <---
end
U:playStereo(U.sounds.bangLarge, self, 0.8)
Explosion(self)
U:deleteObject(self)
Instance = nil
if not U.attractMode then
tween.delay(6, f)
end
end
With this in place, the game spawns just one ship and when it dies, another one never comes back. We need to init the count in U
:
function Universe:startGame(currentTime)
self.currentTime = currentTime
self.saucerTime = currentTime
self.attractMode = false
self.objects = {}
self.indestructibles = {}
self.waveSize = nil
self.lastBeatTime = self.currentTime
createButtons()
Score(4):spawnShip()
self:newWave()
end
Now I should get four. And I do. Now let’s do Game Over and a transition back to attract mode:
function Score:spawnShip()
if self.shipCount <= 0 then self:gameOver() end
self.shipCount = self.shipCount - 1
Ship()
return true
end
Well, that was easy. Oh, yeah, have to write the GAME OVER bit …
Here’s what I came up with. It did take a few tries, which we’ll talk about in a moment:
function Score:spawnShip()
if self.shipCount <= 0 then
self:stopGame()
return false
else
self.shipCount = self.shipCount - 1
Ship()
return true
end
end
function Score:stopGame()
local f = function()
self.gameIsOver = false
U.attractMode = true
end
self.gameIsOver = true
if not U.attractMode then tween.delay(10,f) end
end
Note that the delay is conditioned by attract mode, which is a hack I use so as not to fire tweens during testing, which happens to take place in attract mode. And I renamed things because having a function named gameOver
and a member variable of the same or similar name was too confusing.
This actually works:
Followed by:
Commit: 4 ships game over.
Summing Up
I’m experiencing some missed shifts on the new iPad Magic keyboard. So far I’m blaming myself.
I renamed the Git branch to be “trunk”.
There remains work to be done on this. The use of tweens for timing is convenient but it seems to have a built-in tendency to having odd things happen, such as needing not to do them during testing and so on. There are at least two or three places now where there’s tricky code handling the tweens.
I think we’ll need to think about the question of timed events a bit, and maybe build something. I’d prefer not to build some complex event conditioned upon event thing. There are just a few too many different objects doing timed events. We’ll see.
I’m glad I started with the tests. They helped me quickly drive out the code, and helped me detect my strange inability to do one-based arithmetic any more.
I could probably have gone further, testing the game over transition into attract mode and such, but the fact that those are timed events made me lean away from that. We’ll see what happens when we go after the timing.
Im calling today good. Two hours in or a bit less and we have a great new feature that makes everyone sad: GAME OVER.
Here’s the code.