GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

This morning we’ll try to do the “safe emergence” feature for the Ship. I am filled with troubled antici …

(Say it!) … pation!

The name of the game is that … well the name of the game is “Asteroids”, but the issue with safe emergence is that after the ship has been destroyed, it returns at center screen, but the return is delayed until the saucer is gone, all the missiles have timed out, and the center is reasonably clear. All this happens after the ShipDelay timer has ticked down to zero.

Currently, the ship uses a Timer to decide to restart. It looks like this:

fun newShip(): SpaceObject = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)
    .also { spaceObject ->
        val shipTimer = Timer(spaceObject, U.ShipDelay, false) { activateShip() }
        addComponent(spaceObject, shipTimer)
    }

Now the rules of the timer are that when it has counted down, it does this:

fun updateTimer(timer: Timer, deltaTime: Double) {
    with(timer) {
        if (entity.active == processWhenActive) {
            time -= deltaTime
            if (time <= 0.0) {
                action(this)
                time = delayTime
            }
        }
    }
}

The point being, when the time runs out, the Timer update calls the action and resets the timer. We’d really kind of like the code to be this, in the ship’s case:

    if (entity.active == processWhenActive && safeToEmerge())

What are our options? Let’s see how many I can think of:

  1. Put the safe to emerge check in the activateShip function, and set the timer to some small positive value. That’s possible, because this is the Timer, so it could be passed to activateShip.
  2. Create a new kind of timer and use it here. That’s pretty naff, because we’d be down to just one use of Timer, which hardly justifies its existence, or the existence of the new one.
  3. Extend Timer to include an additional check block, pass it a true for other uses and a safeToEmerge for ship.
  4. Go back to timing the ship with specialized code. This would almost demand removing the timer, and, frankly, the entire E-C-S “architecture”.

With only two objects using our Timer, and the third possibility hardly justifying it, my honest assessment is that, for this program, the E-C-S style doesn’t justify itself, and we ought to just go back to something simpler. That said, however, I think that option 3, the additional check block, might be the quickest change. It would involve three steps:

  1. Install the second check into Timer, presumably driven by tests;
  2. Default the check to true for both missile and ship.
  3. Insert the safeToEmerge check into ship’s creation function.

The check block will be a function from Timer to Boolean, but it will want to ask a very general question about all the space objects. But everything is pretty public here anyway, so I think we can manage it.

We’ll try. I expect this to work, but I am also aware that I may not like it. So I’ll hold off committing quite as often as I might, because I have never quite figured out how to back out multiple git commits to revert well back in the stack. (Advice welcome on Twitter or Mastodon.)

We do have some nice tests for the Timer to start from. We’ll either need a new constructor, or to change a lot of them. Let’s see …

What if we change Timer like this:

class Timer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val processWhenActive: Boolean = true,
    val action: (Timer) -> Unit
): Component {
    var time = delayTime
    var extra: (Timer) -> Boolean = { true }
}

Then we change the update like this:

fun updateTimer(timer: Timer, deltaTime: Double) {
    with(timer) {
        if (entity.active == processWhenActive) {
            time -= deltaTime
            if (time <= 0.0 && extra(this)) {
                action(this)
                time = delayTime
            }
        }
    }
}

This should pass all the tests just fine. It does. Then can’t we just do this?

fun newShip(): SpaceObject = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)
    .also { spaceObject ->
        val shipTimer = Timer(spaceObject, U.ShipDelay, false) { activateShip() }
        shipTimer.extra = { timer:Timer -> safeToEmerge(timer)}
        addComponent(spaceObject, shipTimer)
    }

fun safeToEmerge(timer: Timer): Boolean {
    println("safe for now")
    return true
}

I expect to see “safe for now” printed before the ship emerges. And I do. I’m not sure how to make the construction nicer. I think I need a base constructor that looks like this:

class Timer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val processWhenActive: Boolean,
    var extra: (Timer) -> Boolean,
    val action: (Timer) -> Unit
): Component {
    var time = delayTime
}

We know we want action last so that it can be in braces outside the parens. Let’s see about a couple of constructors now.

class Timer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val processWhenActive: Boolean,
    var extra: (Timer) -> Boolean,
    val action: (Timer) -> Unit
): Component {
    constructor(
        entity: SpaceObject,
        delayTime: Double,
        action: (Timer) -> Unit
    ): this(entity, delayTime, true, { true }, action)
    constructor(
        entity: SpaceObject,
        delayTime: Double,
        processWhenActive: Boolean,
        action: (Timer) -> Unit
    ): this(entity, delayTime, processWhenActive, {true}, action)

    var time = delayTime
}

Now to change ship creation to use the full constructor.

fun newShip(): SpaceObject = SpaceObject(SpaceObjectType.SHIP, 0.0, 0.0, 0.0, 0.0, 0.0, false)
    .also { spaceObject ->
        val shipTimer = Timer(
            spaceObject,
            U.ShipDelay,
            false,
            { timer:Timer -> safeToEmerge(timer)}
        ) { activateShip() }
        addComponent(spaceObject, shipTimer)
    }

I actually think that this might work. Test. Tests run green. Try game, looking for “safe for now”. We get it. This works!

Commit: Timer accepts extra function returning Boolean, checked before triggering action.

So that’s nice. Now we can, I presume, implement a more capable version of safeToEmerge.

Safe to Emerge?

Sigh. I should test-drive this, dammit. Maybe it won’t be so bad … I just want to code, though.

    @Test
    fun `safeToEmerge detects saucer`() {
        createGame(4,4)
        val timer = Timer(Ship, U.ShipDelay) {}
        assertThat(safeToEmerge(timer)).isEqualTo(true)
        Saucer.active = true
        assertThat(safeToEmerge(timer)).isEqualTo(false)
    }

That should be a good start. Fails, as one might imagine. Start the real implementation.

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    return true
}

I expect the test to run. It does. Commit: safeToEmerge checks for saucer active.

What else? Missiles have to all be inactive. New test.

    @Test
    fun `safeToEmerge detects missiles`() {
        createGame(4,4)
        val timer = Timer(Ship, U.ShipDelay) {}
        assertThat(safeToEmerge(timer)).isEqualTo(true)
        withAvailableMissile { missile-> missile.active = true }
        assertThat(safeToEmerge(timer)).isEqualTo(false)
    }

Should fail. Does. Fix:

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    if (activeMissiles(SpaceObjects).isNotEmpty()) return false
    return true
}

Test should run. Green. Commit: safeToEmerge checks for active missiles.

What else? Asteroids too close to home. What is “too close”? In the other games it is universe size / 10. OK

object U {
    const val SafeShipDistance = U.ScreenHeight/10.0
}

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    if (activeMissiles(SpaceObjects).isNotEmpty()) return false
    val center = Vector2(U.ScreenWidth/2.0, U.ScreenHeight/2.0)
    for (asteroid in activeAsteroids(SpaceObjects)) {
        if ( asteroid.position.distanceTo(center) < U.SafeShipDistance ) return true
    }
    return true
}

Oh heck, I got so excited that I forgot to do the test.

    @Test
    fun `safeToEmerge detects close asteroids`() {
        createGame(4,4)
        val timer = Timer(Ship, U.ShipDelay) {}
        assertThat(safeToEmerge(timer)).isEqualTo(true)
        val asteroid = activeAsteroids().first()!!
        asteroid.position = U.CenterOfUniverse + Vector2(50.0 50.0)
        assertThat(safeToEmerge(timer)).isEqualTo(false)
    }

That gives me the chance to define center and use it in the code.

    const val CenterOfUniverse = Vector2(ScreenWidth/2.0, ScreenHeight/2.0)

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    if (activeMissiles(SpaceObjects).isNotEmpty()) return false
    for (asteroid in activeAsteroids(SpaceObjects)) {
        if ( asteroid.position.distanceTo(U.CenterOfUniverse) < U.SafeShipDistance ) return true
    }
    return true
}

I expect tests to run. Grr, that constant won’t compile. Just val without const will work.

However the test needs work. We have no active asteroids as yet. So let’s recast:

    @Test
    fun `safeToEmerge detects close asteroids`() {
        createGame(4,4)
        val timer = Timer(Ship, U.ShipDelay) {}
        assertThat(safeToEmerge(timer)).isEqualTo(true)
        activateAsteroids(1)
        val asteroid = activeAsteroids(SpaceObjects).first()!!
        asteroid.position = U.CenterOfUniverse + Vector2(50.0, 50.0)
        assertThat(safeToEmerge(timer)).isEqualTo(false)
    }

Test fails, because the code I wrote was wrong:

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    if (activeMissiles(SpaceObjects).isNotEmpty()) return false
    for (asteroid in activeAsteroids(SpaceObjects)) {
        if ( asteroid.position.distanceTo(U.CenterOfUniverse) < U.SafeShipDistance ) 
        	return true
    }
    return true
}

Return should be false, i.e. not safe.

fun safeToEmerge(timer: Timer): Boolean {
    if ( Saucer.active) return false
    if (activeMissiles(SpaceObjects).isNotEmpty()) return false
    for (asteroid in activeAsteroids(SpaceObjects)) {
        if ( asteroid.position.distanceTo(U.CenterOfUniverse) < U.SafeShipDistance ) 
        	return false
    }
    return true
}

We are green. Commit: safeToEmerge checks saucer, missiles, and close asteroids. Done.

Summary

I am tempted to blame that last mistake on not having the test in place. I’m of course quite glad that I went ahead and wrote it. As it turned out, the tests were pretty easy to do, so my initial resistance to doing them wasn’t justified. This is almost always the case, and even when a test is harder to write, they always seem to pay off in understanding and defect reduction. That means that they pay off in getting to done faster, both for the current feature, because the defect is easy to fix when we find it right away, and later, because better understanding always speeds us up.

Working with tests in small steps is faster. For me. What about you? I don’t know. Do you? Are you sure?

The underlying code for the new feature is a bit clever, putting a secondary condition into Timer:

class Timer(
    override val entity: SpaceObject,
    val delayTime: Double,
    val processWhenActive: Boolean,
    var extra: (Timer) -> Boolean,
    val action: (Timer) -> Unit
): Component {
    constructor(
        entity: SpaceObject,
        delayTime: Double,
        action: (Timer) -> Unit
    ): this(entity, delayTime, true, { true }, action)
    constructor(
        entity: SpaceObject,
        delayTime: Double,
        processWhenActive: Boolean,
        action: (Timer) -> Unit
    ): this(entity, delayTime, processWhenActive, {true}, action)

    var time = delayTime
}

The hard part was relearning how to do constructors but the effect was that despite completely changing the Timer’s primary constructor, I didn’t have to change any of the existing calls, except of course the one that matters, in newShip.

So that code is a bit intricate but mostly we don’t have to pay attention to it.

And we preserved the Timer, and got emergence checking with easy changes. A successful morning.

See you next time!