GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

Let’s see if we can shoot down the saucer. That shouldn’t be too difficult, except that I can’t remember what the score is supposed to be.

I’ll have to relearn how collisions work. Easily done, we’ll just review a bit of code:

private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
    if ( Ship.active ) checkShipVsAsteroids(Ship)
}

private fun checkAllMissilesVsAsteroids() {
    for (missile in activeMissiles(SpaceObjects)) {
        checkMissileVsAsteroids(missile)
    }
}

private fun checkMissileVsAsteroids(missile: SpaceObject) {
    for (asteroid in activeAsteroids(SpaceObjects)) {
        checkOneAsteroid(asteroid, missile, U.MissileKillRadius)
    }
}

private fun checkShipVsAsteroids(ship: SpaceObject) {
    for (asteroid in activeAsteroids(SpaceObjects)) {
        checkOneAsteroid(asteroid, ship, U.ShipKillRadius)
    }
}

fun checkOneAsteroid(asteroid: SpaceObject, collider: SpaceObject, colliderKillRadius: Double) {
    if (colliding(asteroid, collider,colliderKillRadius)) {
        Score += getScore(asteroid,collider)
        splitOrKillAsteroid(asteroid)
        deactivate(collider)
    }
}

fun colliding(asteroid: SpaceObject, collider: SpaceObject, colliderSize: Double): Boolean {
    val asteroidSize = U.AsteroidKillRadius * asteroid.scale
    return collider.position.distanceTo(asteroid.position) <= asteroidSize + colliderSize
}

OK, it seems that we might go a couple of different ways. We might just do a checkSaucerAgainstMissiles kind of thing. Or maybe we could plunk the Saucer in with the Asteroids and let it drop through there. For that to work, we’d need colliding to be changed so as not to assume so much about its first parameter. If each SpaceObject knew its kill radius, that might work. It would also simplify the colliding calling sequence.

Could we bend our rules enough to have a “method” on SpaceObject that returns its kill radius? It would have to be a function, because the radius of an asteroid varies with its scale.

I suppose we could redefine scale to be kill radius and then adjust drawing … but no.

I think I’ll try something. Here’s the SpaceObjectType enum:

enum class SpaceObjectType(val points: List<Vector2>) {
    ASTEROID(asteroidPoints),
    SHIP(shipPoints),
    SAUCER(saucerPoints),
    MISSILE(missilePoints)
}

Can we provide a second parameter there, a function from SpaceObject to Double? Let’s try.

val asteroidRadius = { asteroid: SpaceObject -> 
    U.AsteroidKillRadius* asteroid.scale}
val missileRadius = { _: SpaceObject -> U.MissileKillRadius }
val saucerRadius = { _: SpaceObject -> U.SaucerKilLRadius }
val shipRadius = { _: SpaceObject -> U.ShipKillRadius }

enum class SpaceObjectType(
	val points: List<Vector2>, 
	val killRadius: (SpaceObject)->Double) 
{
    ASTEROID(asteroidPoints, asteroidRadius),
    SHIP(shipPoints, shipRadius),
    SAUCER(saucerPoints, saucerRadius),
    MISSILE(missilePoints, missileRadius)
}

I have no test for this and I seriously want one.

    @Test
    fun `kill radius tests`() {
        val missile = newMissile()
        val missileRad = SpaceObjectType.MISSILE.killRadius(missile)
        assertThat(missileRad).isEqualTo(U.MissileKillRadius)
    }

I start easy. What surprises me greatly is that three other tests fail, the ones checking Asteroid score!

    @Test
    fun `scale 1 asteroid increases score by 100`() {
        val asteroid = newAsteroid()
        asteroid.position = Vector2(100.0, 100.0)
        asteroid.active = true
        asteroid.scale = 1.0
        val missile :SpaceObject = newMissile()
        missile.position = Vector2(100.0,100.0)
        missile.active = true
        val oldScore = Score
        checkOneAsteroid(asteroid, missile,U.MissileKillRadius)
        assertThat(asteroid.active).isEqualTo(false)
        assertThat(Score - oldScore).isEqualTo(100)
    }

The failure is that it gets zero, not 100. Oh! Whew! I was experimenting with something else last night and didn’t revert it. Reverting that, let’s see how we do. We’re green.

I think I’d like to have a nice little function to get the killRadius of a SpaceObject. Let’s posit it in the test and continue.

    @Test
    fun `kill radius tests`() {
        val missile = newMissile()
        val missileRad = killRadius(missile)
        assertThat(missileRad).isEqualTo(U.MissileKillRadius)
    }

}

fun killRadius(spaceObject: SpaceObject) = spaceObject.type.killRadius(spaceObject)

That passes nicely so far. I’ll move it over into … the SpaceObject file, I guess.

Let’s test the asteroids, they’re the tricky ones:

    @Test
    fun `asteroid radii`() {
        val asteroid = newAsteroid()
        assertThat(killRadius(asteroid)).isEqualTo(U.AsteroidKillRadius*4.0)
        asteroid.scale = 1.0
        assertThat(killRadius(asteroid)).isEqualTo(U.AsteroidKillRadius)
    }

    @Test
    fun `ship radius`() {
        val ship = newShip()
        assertThat(killRadius(ship)).isEqualTo(U.ShipKillRadius)
    }

    @Test
    fun `saucer radius`() {
        val saucer = newSaucer()
        assertThat(killRadius(saucer)).isEqualTo(U.SaucerKilLRadius)
    }

So that’s nice. Now I can change this:

fun colliding(asteroid: SpaceObject, collider: SpaceObject, colliderSize: Double): Boolean {
    val asteroidSize = U.AsteroidKillRadius * asteroid.scale
    return collider.position.distanceTo(asteroid.position) <= asteroidSize + colliderSize
}

First, to this:

fun colliding(target: SpaceObject, collider: SpaceObject, colliderSize: Double): Boolean {
    return collider.position.distanceTo(target.position) <= killRadius((target)) + killRadius(collider)
}

This should work fine. Test and try game. We’re good. Now we can change the signature of colliding, since it no longer uses the provided radius.

We’re good. Commit: change colliding to use new killRadius() function.

Let’s catch our breath and reflect a bit.

Reflection

It just seemed like a good idea to make colliding able to check any two objects, because it seemed likely that we’d need something like it for checking saucer against missiles. Now we can use it.

The provision of a function as a member of our enum is perilously close to violating our arbitrary rule against methods, but we can rationalize that we could store a function pointer in the enum, which is basically what we have done anyway.

This is an example Kent Beck’s notion of making a hard change by first making the hard change easy and then making the easy change.

Moving on

I think we could probably just dump the saucer in with the asteroids and let missiles kill it, but I’m concerned about score. How does that happen?

fun checkOneAsteroid(asteroid: SpaceObject, collider: SpaceObject, colliderKillRadius: Double) {
    if (colliding(asteroid, collider)) {
        Score += getScore(asteroid,collider)
        splitOrKillAsteroid(asteroid)
        deactivate(collider)
    }
}

We’d wind up here, with asteroid being the saucer. We’ll want to rename the method if we use it. How does getScore work?

private fun getScore(asteroid: SpaceObject, collider: SpaceObject): Int {
    if (collider.type != SpaceObjectType.MISSILE) return 0
    return when (asteroid.scale) {
        4.0 -> 20
        2.0 -> 50
        1.0 -> 100
        else -> 0
    }
}

No, it’s too messy, we’d have to fiddle score and splitOrKill as well. We’ll use collidingbut with a separate function.

I know I don’t have decent tests so I’m just going to put this in. This is troubling, but I am confident. And this works, the first time:

private fun checkCollisions() {
    checkAllMissilesVsAsteroids()
    if ( Ship.active ) checkShipVsAsteroids(Ship)
    if ( Saucer.active ) checkAllMissilesVsSaucer(Saucer)
}

private fun checkAllMissilesVsSaucer(saucer: SpaceObject) {
    for (missile: SpaceObject in activeMissiles(SpaceObjects)) {
        if (colliding(saucer, missile)) {
            Score += 99
            deactivate(Saucer)
            deactivate(missile)
        }
    }
}

I’m not sure what the score should really be, and I’m not sure about the kill radius I used: I just picked 24. I’ll review the other version. The kill radius in the other game is 10, and the comments say that 12 would be better. I think our current version is larger. Playing the game a bit, I think 20 might be better. The saucer score is 200, so I’ll set that.

private fun checkAllMissilesVsSaucer(saucer: SpaceObject) {
    for (missile: SpaceObject in activeMissiles(SpaceObjects)) {
        if (colliding(saucer, missile)) {
            Score += U.SaucerScore
            deactivate(Saucer)
            deactivate(missile)
        }
    }
}

I think we can now kill the saucer and score correctly. Game play confirms. Let’s commit this and then see about tests. I do think I have some small ability to check this. Commit: saucer can be shot down for 200 points.

A warning reminds me that I can remove the colliderKillRadius from checkOneAsteroid. I’ll do that and see what else might arise. Commit: remove unneeded parameter.

I am reminded that MissileTime is unused. I must still be referring to the literal?

fun newMissile(): SpaceObject {
    return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
        .also { addComponent(it, Timer(it, 3.0)) }
}

Aha! Fix that:

fun newMissile(): SpaceObject {
    return SpaceObject(SpaceObjectType.MISSILE, 0.0, 0.0, 0.0, 0.0, 0.0, false)
        .also { addComponent(it, Timer(it, U.MissileTime)) }
}

Commit: remove magic number in favor of U.MissileTime.

Now that test. We have this test that’s perhaps a template:

    @Test
    fun `scale 1 asteroid increases score by 100`() {
        val asteroid = newAsteroid()
        asteroid.position = Vector2(100.0, 100.0)
        asteroid.active = true
        asteroid.scale = 1.0
        val missile :SpaceObject = newMissile()
        missile.position = Vector2(100.0,100.0)
        missile.active = true
        val oldScore = Score
        checkOneAsteroid(asteroid, missile)
        assertThat(asteroid.active).isEqualTo(false)
        assertThat(Score - oldScore).isEqualTo(100)
    }

Let’s replicate that for the Saucer:

    @Test
    fun `saucer increases score by 200`() {
        val saucer = newSaucer()
        saucer.position = Vector2(100.0, 100.0)
        saucer.active = true
        saucer.scale = 1.0
        val missile :SpaceObject = newMissile()
        missile.position = Vector2(120.0,100.0)
        missile.active = true
        val oldScore = Score
        checkSaucerVsMissile(saucer, missile)
        assertThat(saucer.active).isEqualTo(false)
        assertThat(Score - oldScore).isEqualTo(200)
    }

I don’t have checkSaucerVsMissile. I have this, to refactor:

private fun checkAllMissilesVsSaucer(saucer: SpaceObject) {
    for (missile: SpaceObject in activeMissiles(SpaceObjects)) {
        if (colliding(saucer, missile)) {
            Score += U.SaucerScore
            deactivate(Saucer)
            deactivate(missile)
        }
    }
}

Extract function:

private fun checkAllMissilesVsSaucer(saucer: SpaceObject) {
    for (missile: SpaceObject in activeMissiles(SpaceObjects)) {
        checkSaucerVsMissile(saucer, missile)
    }
}

fun checkSaucerVsMissile(saucer: SpaceObject, missile: SpaceObject) {
    if (colliding(saucer, missile)) {
        Score += U.SaucerScore
        deactivate(Saucer)
        deactivate(missile)
    }
}

Expect green. Don’t get it. Reversing the asserts, I find that the score is OK but the saucer is still active. I see the upper case in the deactivate. Fix that.

fun checkSaucerVsMissile(saucer: SpaceObject, missile: SpaceObject) {
    if (colliding(saucer, missile)) {
        Score += U.SaucerScore
        deactivate(saucer)
        deactivate(missile)
    }
}

Expect green. Get it. Commit: add test for saucer-missile collision.

Interesting. Wasn’t really a bug … in the game it would work OK. But it wasn’t right and the test found it. If I had written the test first … who knows, the universe would be entirely different by now.

I wonder whether it’s a Pretty Bad Idea to name the official ship capital Ship and use lower case ship as a variable, ditto saucer/Saucer. Probably it is Pretty Bad if not Quite Bad or at least Pretty Darn Bad.

Let’s sum up and get outa here.

Summary

We set out to provide for shooting down the saucer and we got ‘er done. Along the way we generalized the colliding function to handle any two space objects, using a moderately deep-in-the-bag function pointer in the enum. We have declared that to be righteous in view of the fact that even in C we could have put a function pointer in the enum to provide for the radius.

I have a vague feeling that we should do the same thing for scoring but the way the code breaks out now there’s no ambiguity so no need for that yet.

We do have one bit of inefficiency in that we loop over the missiles twice, once for asteroids and once for saucer. The cost of that? A bit more code, which we don’t care about, and one more initializing of a loop, which we also don’t care about. If we were programming a 6502 we might care about both, but we’re not and we don’t.

One more feature down. A decent morning. See you next time!