Kotlin 168: GAME OVER
The game is supposed to give you a finite number of ships. And it’s supposed to be possible to earn new ships at certain levels of point score. How could my ‘independent’ objects do that?
I started thinking about “how could the ShipMaker know how many ships were left, if it’s possible to earn new ones?” Clearly I can start the ShipMaker with some number of ships and have it trigger game over when it runs out. (How can I trigger Game Over?) But if a certain score total earns new ships, how will ShipMaker know that has happened.
Then I had an idea: what if ScoreKeeper put “inactive” ships in the mix. (I probably got the idea of inactive ships from Hill’s implementation.) ScoreKeeper could even arrange them up by the Score, in a row, and ShipChecker / ShipMaker could determine that there were no active ships, but some inactive ones, and consume one and there we’d be.
In fact, no reason why they would be actual Ships, which would complicate that code. They could be ShipTokens or FreeShips or something like that.
We might have to break the rule about objects not terminating other objects, though we could of course do that, or create a FreeShipConsumer …
It’s so tempting to do it that way. It would be kind of fun. However … if ShipChecker just knew ScoreKeeper … it could ask how many ships are available.
There is precedent: ShipChecker already knows the (single) ship.
Of course once I go down this path, it could call into question some of the other interesting objects, such as the Score, the ShipDestroyer, WaveChecker, and so on.
And, within reason, that’s OK. I’m interested in cooperating objects that know as little as makes sense. Knowing nothing may not be quite the right balance.
I’m glad we had this little chat. I think we have …
A Plan
ScoreKeeper will know how many ships are available. It will display them in a row under the score, just like the real game did. ShipChecker will know ScoreKeeper and can therefore ask how many are left before it fires up ShipMaker to bring the real Ship back to life.
Let’s begin by teaching ScoreKeeper to display some ships under the Score. We’ll start it off with a number of ships, say, three. I don’t see much to TDD here, so let’s just do it.
With a bit of ad-hocery, I get this:
The draw
code for ScoreKeeper now looks like this:
fun draw(drawer: Drawer) {
// scale is 1/10 I think.
drawer.isolated {
translate(100.0, 500.0)
stroke = ColorRGBa.GREEN
fill = ColorRGBa.GREEN
text(formatted(), Point(0.0, 0.0))
}
drawer.isolated {
val ship = Ship(Point.ZERO)
ship.heading = -90.0
translate(250.0, 900.0)
drawer.scale(1/U.DROP_SCALE, 1/U.DROP_SCALE)
for (i in 1..shipCount) {
translate(1000.0, 0.0)
drawer.isolated {
ship.draw(drawer)
}
}
}
We can improve it a bit:
fun draw(drawer: Drawer) {
// scale is 1/10 I think.
drawScore(drawer)
drawFreeShips(drawer)
}
The U.DROP_SCALE
bit is accommodating the fact that Ships start drawing themselves large and then get smaller down to normal size. That scale
command cancels out the up-scaling in the ship draw.
So we have just created a ship of our own and drawn it shipCount
times. Works just as I had hoped.
ScoreKeeper accepts shipCount
as a creation parameter, defaulted to 3:
class ScoreKeeper(var shipCount: Int = 3): ISpaceObject, InteractingSpaceObject {
Now let’s wire ShipChecker and ScoreKeeper together:
fun createContents(controls: Controls) {
val ship = newShip(controls)
val scoreKeeper = ScoreKeeper()
add(ShipChecker(ship, scoreKeeper))
add(scoreKeeper)
add(WaveChecker())
add(SaucerMaker())
}
class ShipChecker(
val ship: Ship,
val scoreKeeper: ScoreKeeper = ScoreKeeper()
) : ISpaceObject, InteractingSpaceObject {
Now maybe a little test.
@Test
fun `ScoreKeeper provides ships to be made`() {
val keeper = ScoreKeeper(2)
val ship = Ship(Point.ZERO)
val checker = ShipChecker(ship, keeper)
checker.subscriptions.beforeInteractions()
val t1 = Transaction()
checker.subscriptions.afterInteractions(t1)
assertThat(t1.adds.size).isEqualTo(1)
}
This much runs. But we need to consume one more and then come up empty.
fun `ScoreKeeper provides ships to be made`() {
val keeper = ScoreKeeper(2)
val ship = Ship(Point.ZERO)
val checker = ShipChecker(ship, keeper)
val t1 = Transaction()
checker.subscriptions.beforeInteractions()
checker.subscriptions.afterInteractions(t1)
assertThat(t1.adds.size).isEqualTo(1)
val t2 = Transaction()
checker.subscriptions.beforeInteractions()
checker.subscriptions.afterInteractions(t2)
assertThat(t2.adds.size).isEqualTo(1)
val t3 = Transaction()
checker.subscriptions.beforeInteractions()
checker.subscriptions.afterInteractions(t3)
assertThat(t3.adds.size).isEqualTo(0)
}
This should fail with 1 looking for 0.
expected: 0
but was: 1
Perfect. So now what? Let’s have the ShipChecker ask for a ship and get true if it can have one, false if not.
class ShipChecker
afterInteractions = { trans ->
if ( missingShip && scoreKeeper.takeShip()) {
trans.add(ShipMaker(ship))
trans.remove(this)
}
}
class ScoreKeeper
fun takeShip(): Boolean {
shipCount -= 1
return shipCount >= 0
}
I expect my test to pass. It does. I try the game. Curiously, it does show two ships, but it doesn’t count down. I still seem to have an infinite number of ships. Curious indeed.
Ah. The problem is, of course, that the ShipChecker destroys itself so as not to get excited while waiting for the ShipMaker. We’ll need to pass the ScoreKeeper back and forth.
That’s easy enough:
class ShipChecker(
val ship: Ship,
val scoreKeeper: ScoreKeeper = ScoreKeeper()
) : ISpaceObject, InteractingSpaceObject {
afterInteractions = { trans ->
if ( missingShip && scoreKeeper.takeShip()) {
trans.add(ShipMaker(ship, scoreKeeper))
trans.remove(this)
}
}
class ShipMaker(val ship: Ship, val scoreKeeper: ScoreKeeper = ScoreKeeper()) : ISpaceObject, InteractingSpaceObject {
private fun replaceTheShip(trans: Transaction) {
trans.add(ship)
ship.dropIn()
trans.add(ShipChecker(ship, scoreKeeper))
trans.remove(this)
HyperspaceOperation(ship, asteroidTally).execute(trans)
}
There is a bit of a problem, however, which is that the ShipChecker keeps on checking, every cycle and can’t ever create a ship because ScoreKeeper won’t ever give it one. Maybe that’s OK … because sooner or later someone will stick in a quarter and we’ll get more ships.
I’m not sure how we’ll know. Let’s do, however, have ScoreKeeper display GAME OVER.
fun draw(drawer: Drawer) {
// scale is 1/10 I think.
drawScore(drawer)
drawFreeShips(drawer)
drawGameOver(drawer)
}
private fun drawGameOver(drawer: Drawer) {
if (shipCount >= 0) return
drawer.isolated{
translate(3500.0,5000.0)
text("GAME OVER")
}
}
And success!!
Commit: Game now can count ships and display how many are left, game over when gone. Might want to display one more than are available i.e. how many counting the one you’re flying now.
I make that change, commit again. Done!
That went well. And best of all, the saucer keeps flying around running into things and accidentally shooting things, triggering new waves as needed. Attract mode is done. :)
See you next time!