GitHub Decentralized Repo
GitHub Centralized Repo

Let’s do interactions in our new type-sensitive style. Let’s do it with an object. Let’s test-drive the object.

In yesterday’s implementation, which turned out to be an experimental spike, I added over 50 lines and at least six methods, to do what was requiring about 14 lines in the old form. All that code was added to the Game object, which is 155 lines even without much collision code. I think Game probably already needed some things extracted from it, even before I put in the collision code.

In addition, it turns out that collisions are only partially tested. They do work fine, but if we’re going to do it anew, we need better testing. This is proven, or at least emphasized, by the fact that yesterday I forgot to implement the saucer and ship colliding.

Therefore, today I plan to implement a helper object to do the collision calculations. I’m thinking that it should work like this:

  1. It’s created with the collections for missiles, ship, saucer, and asteroids, plus a transaction.
  2. When it’s created, it will immediately calculate all the interactions, chucking the results into its transaction.
  3. When it’s done, it will return.

For testing purposes, I plan to give it tiny collections with various combinations of object, probably just two at a time, and I’ll check the resulting transaction for correctness.

We already have an interface named Collider and an object called Collision, so I guess we’ll have to call this one Interaction. Here goes.

Interaction TDD

I try this for my first test. Kind of large, but I wanted to get my thoughts down.

    @Test
    fun `empty returns empty`() {
        val missiles = mutableListOf<Missile>()
        val ships = mutableListOf<Ship>()
        val saucers = mutableListOf<Saucer>()
        val asteroids = mutableListOf<Asteroid>()
        val trans = Transaction()
        Interaction(missiles, ships, saucers, asteroids, trans)
        assertThat(trans.adds).isEmpty()
        assertThat(trans.removes).isEmpty()
    }

IDEA offers to build the class for me. With some judicious selections by Yours Truly, we get this shell:

class Interaction(
    missiles: List<Missile>,
    ships: List<Ship>,
    saucers: List<Saucer>,
    asteroids: List<Asteroid>,
    trans: Transaction
) {

}

I believe our first test will pass. And it does. Commit: Interaction and InteractionTest created.

I get a bunch of warnings about my unused stuff. Thanks, IDEA.

Let’s begin to test things. We need to do these combinations:

  • missiles vs ships
  • missiles vs saucers
  • missiles vs asteroids
  • ships vs saucers
  • ships vs asteroids
  • saucers vs asteroids

For checking purposes, these are the combinations of 4 things taken two at a time. We may choose also to do missiles against missiles. I’d have to look at the code to see if we’re doing them now: I know we’ve done it both ways.

Let’s just start ticking through.

    @Test
    fun `missile and ship`() {
        val missile = Missile(Point(100.0, 100.0))
        missile.position = Point(100.0, 100.0)
        val ship = Ship(Point(100.0, 100.0))
        val trans = Transaction()
        Interaction(listOf(missile), listOf(ship), emptyList(), emptyList(), trans)
        assertThat(trans.removes).contains(ship)
        assertThat(trans.removes).contains(missile)
    }

This seems a decent start. Test will fail not finding ship.

Expecting LinkedHashSet:
  []
to contain:
  [Ship Vector2(x=100.0, y=100.0) (12.0)]
but could not find the following element(s):
  [Ship Vector2(x=100.0, y=100.0) (12.0)]

As anticipated. I love it when a plan comes together. We have to do some work in the Interaction now. I plan to just kick it all off in the init, since we’re not sending it any messages.

    init {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
        }
    }

I rather expect my test to run now. It does. We should commit: missile vs ship.

Another test:

    @Test
    fun `missile and saucer`() {
        val missile = Missile(Point(100.0, 100.0)).also { it.position = Point(100.0, 100.0)}
        val saucer = Saucer().also { it. position = Point(100.0, 100.0) }
        val trans = Transaction()
        Interaction(listOf(missile), emptyList(), listOf(saucer), emptyList(), trans)
        assertThat(trans.removes).contains(saucer)
        assertThat(trans.removes).contains(missile)
    }

Should fail. Does. Make it go:

    init {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
        }
    }

I expect green. Yes. Commit: missile vs saucer.

Another test, asteroid:

    @Test
    fun `missile and asteroid`() {
        val missile = Missile(Point(100.0, 100.0)).also { it.position = Point(100.0, 100.0)}
        val asteroid = Asteroid(Point(100.0, 100.0))
        val trans = Transaction()
        Interaction(listOf(missile), emptyList(), emptyList(), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(missile)
    }

It’s about time to make these tests a bit easier to write. We’ll make this one work first. It is red.

    init {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithAsteroid(asteroid, trans)
            }
        }
    }

About time to clean that up as well, We are green. Commit: missile vs asteroid.

Note, by the way, that I’m just checking the removals in these tests. We could check the adds for the various splats and so on. Since I’m just using the existing interaction code, I don’t feel the need to do that, but I’d probably be better off to check things. I am not a good person.

Let’s do improve the tests a bit. I’ll start by extracting Point(100.0, 100.0) for convenience.

class InteractionTest {
    val target = Point(100.0, 100.0)

And I use it throughout. Note that I still use the also feature to init the locations that are otherwise initialized in the objects. Now the typical test looks like this:

    @Test
    fun `missile and asteroid`() {
        val missile = Missile(target).also { it.position = target}
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(listOf(missile), emptyList(), emptyList(), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(missile)
    }

Honestly, I don’t see much that would help with that. There are just three more to do, let’s just do them.

What about the Interaction object? It looks like this:

    init {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithAsteroid(asteroid, trans)
            }
        }
    }

Looks like we ought to extract that bit. For some reason, IDEA just can’t extract it without trying to make all the interactWith things into parameters. I’ll have to do it by hand.

To satisfy Kotlin, IDEA, and myself, I wind up with this:

class Interaction(
    private val missiles: List<Missile>,
    private val ships: List<Ship>,
    private val saucers: List<Saucer>,
    private val asteroids: List<Asteroid>,
    private val trans: Transaction
) {

    init {
        missilesVsShipSaucerAsteroids()
    }
    
    private fun missilesVsShipSaucerAsteroids() {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithAsteroid(asteroid, trans)
            }
        }
    }

}

Should be green. Yes. Commit: extract method.

Another test, next on the list is ship vs saucer.

    @Test
    fun `ship and saucer`() {
        val ship = Ship(target)
        val saucer = Saucer().also { it.position = target }
        val trans = Transaction()
        Interaction(emptyList(), listOf(ship), listOf(saucer), emptyList(), trans)
        assertThat(trans.removes).contains(saucer)
        assertThat(trans.removes).contains(ship)
    }

Red. Make it green:

    init {
        missilesVsShipSaucerAsteroids()
        shipVsSaucerAsteroids()
    }

    private fun shipVsSaucerAsteroids() {
        ships.forEach { ship ->
            saucers.forEach { saucer ->
                ship.subscriptions.interactWithSaucer(saucer, trans)
                saucer.subscriptions.interactWithShip(ship, trans)
            }
        }
    }

I named the method with a bit of antici … (say it) … pation, so sue me. I expect green. I get it. Commit: ship vs saucer.

Another test.

    @Test
    fun `ship and asteroid`() {
        val ship = Ship(target)
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(emptyList(), listOf(ship), emptyList(), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(ship)
    }

Red.

    private fun shipVsSaucerAsteroids() {
        ships.forEach { ship ->
            saucers.forEach { saucer ->
                ship.subscriptions.interactWithSaucer(saucer, trans)
                saucer.subscriptions.interactWithShip(ship, trans)
            }
            asteroids.forEach { asteroid -> 
                ship.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithShip(ship, trans)
            }
        }
    }

Expect green. Green. Commit: ship vs asteroid.

One more test:

    @Test
    fun `saucer and asteroid`() {
        val saucer = Saucer().also { it.position = target }
        val asteroid = Asteroid(target)
        val trans = Transaction()
        Interaction(emptyList(), emptyList(), listOf(saucer), listOf(asteroid), trans)
        assertThat(trans.removes).contains(asteroid)
        assertThat(trans.removes).contains(saucer)
    }

Red.

    init {
        missilesVsShipSaucerAsteroids()
        shipVsSaucerAsteroids()
        saucerVsAsteroids()
    }

    private fun saucerVsAsteroids() {
        saucers.forEach { saucer->
            asteroids.forEach { asteroid ->
                saucer.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithSaucer(saucer, trans)
            }
        }
    }

Expect green. Green. Commit: saucer vs asteroids.

This object is done. We could enhance the tests to check adds. I’ll make a sticky for that, because I think it’s worth doing.

I think I’m ready to plug this thing in. Here’s how we do interactions now:

class Game
    fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())


    fun changesDueToInteractions(): Transaction {
        val trans = Transaction()
        knownObjects.pairsToCheck().forEach { p ->
            p.first.callOther(p.second, trans)
            p.second.callOther(p.first, trans)
        }
        return trans
    }

I don’t entirely love returning the transaction like that, but clearly we can just plug our new thing in here.

    fun changesDueToInteractions(): Transaction {
        val trans = Transaction()
        with (knownObjects) {
            Interaction(missiles, ships, saucers, asteroids, trans)
        }
        return trans
    }

I expect green and I expect the game to work. Yes and yes. Commit: use new Interaction to process interactions.

Now we have some things we can remove, like the old interaction stuff. There should be no live calls to pairsToCheck in SpaceObjectCollection.

There is just one test. Remove it. Remove the method. Green. Commit: Remove pairsToCheck and test.

Let’s review the Interaction and see if we like it.

class Interaction(
    private val missiles: List<Missile>,
    private val ships: List<Ship>,
    private val saucers: List<Saucer>,
    private val asteroids: List<Asteroid>,
    private val trans: Transaction
) {

    init {
        missilesVsShipSaucerAsteroids()
        shipVsSaucerAsteroids()
        saucerVsAsteroids()
    }

    private fun saucerVsAsteroids() {
        saucers.forEach { saucer->
            asteroids.forEach { asteroid ->
                saucer.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithSaucer(saucer, trans)
            }
        }
    }

    private fun shipVsSaucerAsteroids() {
        ships.forEach { ship ->
            saucers.forEach { saucer ->
                ship.subscriptions.interactWithSaucer(saucer, trans)
                saucer.subscriptions.interactWithShip(ship, trans)
            }
            asteroids.forEach { asteroid ->
                ship.subscriptions.interactWithAsteroid(asteroid, trans)
                asteroid.subscriptions.interactWithShip(ship, trans)
            }
        }
    }

    private fun missilesVsShipSaucerAsteroids() {
        missiles.forEach { missile ->
            ships.forEach {  ship ->
                ship.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithShip(ship, trans)
            }
            saucers.forEach {  saucer ->
                saucer.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithSaucer(saucer, trans)
            }
            asteroids.forEach {  asteroid ->
                asteroid.subscriptions.interactWithMissile(missile, trans)
                missile.subscriptions.interactWithAsteroid(asteroid, trans)
            }
        }
    }
}

I think that’s reasonable. Further refactoring would be possible but doesn’t really add much communication.

One more thing: I just looked at the original code, and we do allow missiles to destroy each other. We’ll need to add that. Were we wrong to ship this? Maybe. Shall we fix it now? OK:

    @Test
    fun `missile and missile`() {
        val missile1 = Missile(target).also { it.position = target }
        val missile2 = Missile(target).also { it.position = target }
        val trans = Transaction()
        Interaction(listOf(missile1, missile2), emptyList(), emptyList(), emptyList(), trans)
        assertThat(trans.removes).contains(missile1)
        assertThat(trans.removes).contains(missile2)
    }

Red.

    private fun missilesVsMissileShipSaucerAsteroids() {
        missiles.forEach { missile ->
            missiles.forEach { other ->
                if (other != missile ) {
                    missile.subscriptions.interactWithMissile(other, trans)
                    other.subscriptions.interactWithMissile(missile, trans)
                }
            }

I even remembered not to have missiles destroy themselves. Should be green. Yes. Commit: missile vs missile bug fixed.

That could be done more nicely, but this is the only case of a type colliding with itself, so this is arguably the place for it. It’ll stand for now, and perhaps for all time.

This will do for a Sunday, let’s sum up.

Summary

As so often happens, doing it over goes much better than doing it the first time. There were no noticeable missteps along the way. We just went tick tick tick, red green red green throughout. Ten commits in an hour and ten minutes, counting writing all these words.

Part of what made this go in small steps was the decision to use the subscriptions as they exist. We do intend to get rid of those, but now we can do it incrementally, one object, one method at a time. That will be our next step, I suspect.

I’d like to check the adds as well as the removes on all those tests, not because I doubt them, but because the test will then better express everything that should happen and … since we do plan to refactor the implementation of interactions in each object, we’d do well to have some checking going on, just in case we make a mistake. It could happen.

For today, I am pleased with the outcome.

What about the code? We have a class of 61 lines replacing a method, pairsToCheck, that was just a few lines. So far, this is not what you’d call a win. However, we’ll be able to unwind the bulk of the subscriptions model now, and ultimately we’ll have a much simpler object hierarchy.

I still rather like the subscriptions idea, even though I didn’t invent it. I think that what we have here is a bit more like what one would do in a language like Kotlin. I’ll explore that question with my colleagues later this week.

For now, we have done what we set out to do, and done it rather nicely. Yay, us!

See you next time!