GitHub Decentralized Repo
GitHub Centralized Repo

After an idea that doesn’t pan out, we complete the installation of CollisionStrategy objects. We begin assessing what we like — and what’s troubling.

As I was feeding the cat this morning1, it suddenly came to me that Kotlin’s by delegation might be applicable in the new CollisionStrategy objects. Currently the simplest one looks like this:

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)
        }
    }
}

It turns out that I am mistaken, but perhaps not forever. What I wanted to do was to delegate the position and killRadius to ship using by. Unfortunately, the interface that is requiring us to provide point and killRadius is Collider, and Ship class does not implement that.

I think that if position and killRadius were in a separate interface, say Positionable, we might then be able to do it. On the other hand, I think it likely that once we are finished with putting strategies into all four of our objects, we may well have no need for those vals to be in the interface at all.

Oh well, not every idea pans out.

Why did you mention it?

Why do I mention it, you ask? Because my articles aren’t about perfect code handed down on Nth metal tablets as if from some god. They are about the process of programming, the thinking, the musing, the wondering, the mistakes, the confusion, the fear, and the joy. Programming is a human activity, so far, and I want to share with you my actual experience, in the hope that it will entertain you and perhaps prepare you in some way for your own experience. So that’s why I mention it. Thanks for asking!

Another Strategy

OK, in the absence of a fun discovery, let’s do another CollisionStrategy. I just about have it down to a science, if a science can be described like this:

  1. Create a new file for the new strategy class;
  2. Grab all the code needed for the class, cut from the source class, paste to the target class;
  3. Change the references that need to refer back to the source object;
  4. Grab all the code I should have grabbed the first time and move it over;
  5. Find out why it won’t compile;
  6. Find out why tests won’t quite run;
  7. Enjoy the fact that it works.

So far, it has gone just that smoothly. And, honestly, that’s smoothly enough. I have high hopes for the next two. We’ll do Missile next.

Items I forgot:

  • Declare the class with a parameter missile and inheriting Collider;
  • Create the position and killRadius forwarding methods;
  • Make missileIsFromShip public in Missile;
  • Make timeOut public in Missile;
  • Change the collisionStrategy in Missile to create the strategy.

About three minutes later, I am now ready to compile.

I also forgot:

  • Change Missile to no longer implement Collider.

Now I have some tests that do interact, which I expected. The fix is easy:

    missile.interact(asteroid, trans)

becomes

    missile.collisionStrategy.interact(asteroid, trans)

The tests are green. Test the game. Works as advertised. Commit: convert Missile to use MissileCollisionStrategy.

class MissileCollisionStrategy(val missile: Missile): Collider {
    override val position: Point
        get() = missile.position
    override val killRadius: Double
        get() = missile.killRadius

    override fun interact(asteroid: Asteroid, trans: Transaction) {
        checkAndScoreCollision(asteroid, trans, asteroid.getScore())
    }

    override fun interact(missile: Missile, trans: Transaction) {
        checkAndScoreCollision(missile, trans, 0)
    }

    override fun interact(saucer: Saucer, trans: Transaction) {
        checkAndScoreCollision(saucer, trans,saucer.getScore())
    }

    override fun interact(ship: Ship, trans: Transaction) {
        checkAndScoreCollision(ship, trans, 0)
    }

    private fun checkAndScoreCollision(other: Collidable, trans: Transaction, score: Int) {
        Collision(other).executeOnHit(missile) {
            terminateMissile(trans)
            if (missile.missileIsFromShip) trans.addScore(score)
        }
    }

    private fun terminateMissile(trans: Transaction) {
        missile.timeOut.cancel(trans)
        trans.remove(missile)
    }

Now, unless I miss my guess2, we just have Saucer to do. I will repeat my plan, paying attention to all the items.

class SaucerCollisionStrategy(val saucer: Saucer): Collider {
    override val position: Point
        get() = saucer.position
    override val killRadius: Double
        get() = saucer.killRadius

    override fun interact(asteroid: Asteroid, trans: Transaction) = checkCollision(asteroid, trans)

    override fun interact(missile: Missile, trans: Transaction) {
        if (missile == saucer.currentMissile) saucer.missileReady = false
        checkCollision(missile, trans)
    }

    override fun interact(saucer: Saucer, trans: Transaction) { }

    override fun interact(ship: Ship, trans: Transaction) {
        saucer.sawShip = true
        saucer.shipFuturePosition = ship.position + ship.velocity * 1.5
        checkCollision(ship, trans)
    }

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

This time I do it perfectly, and there are no tests that fail. I hope that’s not a bad sign. We are green. I’ll test the game. Everything works perfectly. The Saucer even managed to shoot me down. Wonderful.

Commit: Saucer uses SaucerCollisionStrategy.

Whoa!

I forgot to stop Saucer from being a Collider. I didn’t cut the methods, I copied them. Tests are running for the wrong reason! I thought that was too easy. Make the standard change in about six tests. Green. Commit: Make Saucer no longer Collider.

I thought it was odd that no tests broke. I should have taken that as a clue. All better now.

Assessment

We’ll do a bit of assessment now, based on things I noticed as I worked, and I think we’ll do a more broad assessment in a day or so. What I particularly noticed this time was that our strategy needed to access and adjust various values in the object it was supporting. In the Saucer case, we have these lines:

    if (missile == saucer.currentMissile) saucer.missileReady = false
    saucer.sawShip = true
    saucer.shipFuturePosition = ship.position + ship.velocity * 1.5

These lines provide important information to Saucer. The first one is used to decide whether it can fire another missile: it only gets one missile in flight at a time. The second is used to decide whether to fire or not: it doesn’t fire missiles when the ship is not present. And the third is used as the target for firing when it fires an accurate missile: it aims 1.5 seconds ahead of the ship on its current path.

We need to know these things. However, given that we’re doing them in our object’s collision strategy, we’re requiring that strategy to know some rather intimate facts about how its client works. But it gets worse. In the case of Saucer, we do all those calls before checking collisions. But in the case of Missile, it wants to know something only if there is a collision:

    private fun checkAndScoreCollision(other: Collidable, trans: Transaction, score: Int) {
        Collision(other).executeOnHit(missile) {
            terminateMissile(trans)
            if (missile.missileIsFromShip) trans.addScore(score)
        }
    }

When an object is killed by a missile, and only if that missile is from the ship, we add in the score for whatever we hit. This extra behavior only occurs after we know there was a hit.

Ship has no special behavior. Asteroids do have special behavior: they may split.

What is this telling us?

I think this is telling us that we do not have a clear seam between dealing with interactions and collisions, and the rest of the client object. We’re using interaction in at least two ways, for detection of collisions, but also for determination of interesting facts about the universe. And it should be no surprise that there are various ways of responding to the fact of an interaction or collision:

Asteroid (collision)
Possibly split into two asteroids
Missile (collision)
Deal with timing out; only score if you’re from the ship.
Ship
Nothing special
Saucer (just interacting)
Find out if you can fire; find out if ship is on screen; estimate target position.

Now these little things aren’t terribly concerning—but they aren’t quite right. The Strategy has to know too much about its client. OK, it is type-specific: it is a SaucerCollisionStrategy. But that doesn’t mean we want it reaching into the Saucer and tweaking its private members3. That’s more coupling than would be ideal.

At this writing, I don’t quite know what we should do. For the post-collision special behavior, we could perhaps provide a block, or a special call-back that a Strategy user must implement. Or perhaps we should return a result from interact commands. No, I don’t think we can do that … the base object doesn’t even know that a call was made. Its strategy completely intercepts the calls to interact

I can see two possibilities, as through a glass, darkly:

  1. Provide a standard callback, e.g. terminate, that is made back to the client when it is colliding, and move special termination behavior, like splitting, over there.
  2. Do special processing in interactWith, on the client side, prior to calling one’s type-specific interact.

The first can almost certainly be made to work. The second, I’m not so sure, because in interactWith, we do not have the type of the object we’re interacting with, and the special stuff that Saucer does all relates to that.

Maybe we’ll have to devise some other special deal for Saucer. But it would be nice to get type-specific code out of these Strategies … we might be able to coalesce them into fewer than four.

We’ll think about that, do a more broad assessment of the Strategies, and try new and different things, in future articles. I hope you’ll join me then!



  1. Not some kind of euphemism with which you were previously unfamiliar. 

  2. Which is entirely possible … 

  3. Quiet, you! I meant no such thing!