GitHub Decentralized Repo
GitHub Centralized Repo

We’ll try another one of our CollisionStrategy things. I was hoping for some reuse but am not clear yet how to get it, nor whether its really worth it. I think we’ll complete all four Strategies and then look around.

Let’s just pick another class to move. Which one looks easiest?

Looks like Asteroid should be pretty straightforward:

    override val collisionStrategy: Collider
        get() = this
    
    override fun interactWith(other: Collidable, trans: Transaction)
        = other.collisionStrategy.interact(this, trans)

    override fun interact(asteroid: Asteroid, trans: Transaction) {}
    override fun interact(missile: Missile, trans: Transaction) 
    	= checkCollision(missile, trans)
    override fun interact(saucer: Saucer, trans: Transaction) 
    	= checkCollision(saucer, trans)
    override fun interact(ship: Ship, trans: Transaction) 
    	= checkCollision(ship, trans)

    private fun checkCollision(other: Collidable, trans: Transaction) {
        Collision(this).executeOnHit(other) {
            dieDueToCollision(trans)
        }
    }

    fun dieDueToCollision(trans: Transaction) {
        trans.remove(this)
        trans.add(Splat(this))
        splitIfPossible(trans)
    }

Seems we can just make another Strategy and refer to it. It looks like this:

class AsteroidCollisionStrategy(val asteroid: Asteroid): Collider {
    override val position: Point
        get() = asteroid.position
    override val killRadius: Double
        get() = asteroid.killRadius
    override fun interact(asteroid: Asteroid, trans: Transaction) {}
    override fun interact(missile: Missile, trans: Transaction) 
    	= checkCollision(missile, trans)
    override fun interact(saucer: Saucer, trans: Transaction) 
    	= checkCollision(saucer, trans)
    override fun interact(ship: Ship, trans: Transaction) 
    	= checkCollision(ship, trans)

    private fun checkCollision(other: Collidable, trans: Transaction) {
        Collision(asteroid).executeOnHit(other) {
            dieDueToCollision(trans)
        }
    }

    fun dieDueToCollision(trans: Transaction) {
        trans.remove(asteroid)
        trans.add(Splat(asteroid))
        splitIfPossible(trans)
    }


    private fun splitIfPossible(trans: Transaction) {
        if (asteroid.splitCount >= 1) {
            trans.add(asSplit(asteroid))
            trans.add(asSplit(asteroid))
        }
    }

    private fun asSplit(asteroid: Asteroid): Asteroid =
        Asteroid(
            position = asteroid.position,
            killRadius = asteroid.killRadius / 2.0,
            splitCount = asteroid.splitCount - 1
        )
}

I have a raft of tests that call dieDueToCollision, which can’t be called on the strategy because it isn’t in the interface. nor do I want it there. I comment those out and add a few indirections through collisionStrategy and am green, with three removed tests. I want to see if the game works … and it works just fine.

Now how to revive those tests? Let’s inspect one.

    @Test
    fun `asteroid dieOnCollision`() {
        val asteroid = Asteroid(Point.ZERO)
        val trans = Transaction()
        asteroid.dieDueToCollision(trans)
        val splits = trans.asteroids()
        assertThat(splits.size).isEqualTo(2)
    }

I guess we could create something for the asteroid to hit and call that. I’ll try it. Ah, easy:

    @Test
    fun `asteroid dieOnCollision`() {
        val asteroid = Asteroid(Point.ZERO)
        val ship = Ship(Point.ZERO)
        val trans = Transaction()
        asteroid.collisionStrategy.interact(ship, trans)
        val splits = trans.asteroids()
        assertThat(splits.size).isEqualTo(2)
    }

That runs green. Easy enough to repair the others similarly. Yes and we are green. Commit: Asteroid now collides using AsteroidCollisionStrategy.

Summary

I think we’ll stop here, this is just an afternoon dalliance, but let’s compare the two strategies and speculate about how to deal with any duplication.

class ShipCollisionStrategy(val ship: Ship): Collider {
    override val position: Point
        get() = ship.position
    override val killRadius: Double
        get() = ship.killRadius
    override fun interact(asteroid: Asteroid, trans: Transaction) 
    	= checkCollision(asteroid, trans)
    override fun interact(missile: Missile, trans: Transaction) 
    	= checkCollision(missile, trans)
    override fun interact(saucer: Saucer, trans: Transaction) 
    	= checkCollision(saucer, trans)
    override fun interact(ship: Ship, trans: Transaction) { }

    private fun checkCollision(other: Collidable, trans: Transaction) {
        Collision(other).executeOnHit(ship) {
            trans.add(Splat(ship))
            trans.remove(ship)
        }
    }
}

class AsteroidCollisionStrategy(val asteroid: Asteroid): Collider {
    override val position: Point
        get() = asteroid.position
    override val killRadius: Double
        get() = asteroid.killRadius
    override fun interact(asteroid: Asteroid, trans: Transaction) {}
    override fun interact(missile: Missile, trans: Transaction) 
    	= checkCollision(missile, trans)
    override fun interact(saucer: Saucer, trans: Transaction) 
    	= checkCollision(saucer, trans)
    override fun interact(ship: Ship, trans: Transaction) 
    	= checkCollision(ship, trans)

    private fun checkCollision(other: Collidable, trans: Transaction) {
        Collision(asteroid).executeOnHit(other) {
            dieDueToCollision(trans)
        }
    }

    fun dieDueToCollision(trans: Transaction) {
        trans.remove(asteroid)
        trans.add(Splat(asteroid))
        splitIfPossible(trans)
    }


    private fun splitIfPossible(trans: Transaction) {
        if (asteroid.splitCount >= 1) {
            trans.add(asSplit(asteroid))
            trans.add(asSplit(asteroid))
        }
    }

    private fun asSplit(asteroid: Asteroid): Asteroid =
        Asteroid(
            position = asteroid.position,
            killRadius = asteroid.killRadius / 2.0,
            splitCount = asteroid.splitCount - 1
        )
}

Obviously they’re the same down to checkCollision.

We could imagine, perhaps, a standard method dieDuetoCOllision as part of the Collidable interface, and each object implements it uniquely. Or, perhaps, we could provide a code block to the strategy, to be executed inside the checkCollision. Or, I suppose there could be yet another Strategy, a What To Do When Colliding Strategy.

However, it’s easy to miss that each of the four classes may want to fill in a particular one of the interact methods differently. Here, asteroid v asteroid is empty in Asteroid, and ship v ship is empty in Ship. I’m not sure if we’ll be able to roll these together or not. Possibly not. Even so, we are gaining cohesion but it would be nice to save some code as well.

We’ll wait to see what it looks like with all four of them. So far it has been pretty easy and is at least improving cohesion. Certainly an interesting kind of program transformation.

See you next time!