GitHub Decentralized Repo
GitHub Centralized Repo

With changes to Transaction, I think I can get rid of most of the type checking in the system. I could be right …

In SpaceObjectCollection (knownObjects), we sort out incoming objects by class:

    val asteroids = mutableListOf<Asteroid>()
    val missiles = mutableListOf<Missile>()
    val saucers = mutableListOf<Saucer>()
    val ships = mutableListOf<Ship>()
    val splats = mutableListOf<Splat>()
    fun add(spaceObject: SpaceObject) {
        addActualSpaceObjects(spaceObject)
    }

    private fun addActualSpaceObjects(spaceObject: SpaceObject) {
        when (spaceObject) {
            is Missile -> add(spaceObject)
            is Asteroid -> add(spaceObject)
            is Ship -> add(spaceObject)
            is Saucer -> add(spaceObject)
            is Splat -> add(spaceObject)
        }
    }

Kotlin is clever enough to call a specific add operation after the is, as if one had typed this:

    private fun addActualSpaceObjects(spaceObject: SpaceObject) {
        when (spaceObject) {
            is Missile -> add(spaceObject as Missile)
            is Asteroid -> add(spaceObject as Asteroid)
            is Ship -> add(spaceObject as Ship)
            is Saucer -> add(spaceObject as Saucer)
            is Splat -> add(spaceObject as Splat)
        }
    }

If you actually type that, IDEA offers to “remove useless cast”. Then SpaceObjectCollection sorts everything into separate collections:

    private fun add(asteroid: Asteroid) {
        asteroids.add(asteroid)
    }

    fun add(missile: Missile) {
        missiles.add(missile)
    }

    fun add(saucer: Saucer) {
        saucers.add(saucer)
    }

    fun add(ship: Ship) {
        ships.add(ship)
    }

    fun add(splat: Splat) {
        splats.add(splat)
    }

So this is fine. But Transactions do not preserve the types yet. Transaction looks like this:

    val adds = mutableSetOf<SpaceObject>()

    fun add(spaceObject: SpaceObject) {
        adds.add(spaceObject)
    }

    fun applyChanges(spaceObjectCollection: SpaceObjectCollection) {
        if (shouldClear ) spaceObjectCollection.clear()
        spaceObjectCollection.addScore(score)
        removes.forEach { spaceObjectCollection.remove(it)}
        deferredActionRemoves.forEach { spaceObjectCollection.remove(it)}
        adds.forEach { spaceObjectCollection.add(it)}
        deferredActionAdds.forEach { spaceObjectCollection.add(it)}
    }

So, anything added in to a Transaction just goes into a vanilla set of SpaceObjects and is thereafter send in on the untyped add command. We can avoid all that type checking if we keep separate collections in Transaction. My plan this afternoon is to do that.

I’ll add the subcollections and the individual adds. It should “just work”.

    val asteroids = mutableListOf<Asteroid>()
    val missiles = mutableListOf<Missile>()
    val saucers = mutableListOf<Saucer>()
    val ships = mutableListOf<Ship>()
    val splats = mutableListOf<Splat>()

    fun add(asteroid: Asteroid) {asteroids.add(asteroid)}
    fun add(missile: Missile) {missiles.add(missile)}
    fun add(saucer: Saucer) {saucers.add(saucer)}
    fun add(ship: Ship) {ships.add(ship)}
    fun add(splat: Splat) {splats.add(splat)}

I’ll remove the generic add. An issue arises, this function:

    fun addAll(adds: List<SpaceObject>) {
        adds.forEach { add(it) }
    }

It can’t sort out types. I’ll need to fix calls to it. There’s only one:

        trans.addAll(fire(ship))

This is a hack, because fire(ship) either returns one or zero items:

    private fun fire(obj: Ship): List<SpaceObject> = missilesToFire(obj).also { fire = false }

    private fun missilesToFire(obj: Ship): List<SpaceObject> {
        return if (fire) {
            listOf(Missile(obj))
        } else {
            emptyList()
        }
    }

Let’s return a list of Missile here:

    private fun fire(obj: Ship): List<Missile> = missilesToFire(obj).also { fire = false }

    private fun missilesToFire(obj: Ship): List<Missile> {
        return if (fire) {
            listOf(Missile(obj))
        } else {
            emptyList()
        }
    }

And unwind the list here instead of in Transaction:

    fun control(ship: Ship, deltaTime: Double, trans: Transaction) {
        if (hyperspace) {
            hyperspace = false
            ship.enterHyperspace(trans)
        }
        turn(ship, deltaTime)
        accelerate(ship, deltaTime)
        fire(ship).forEach { trans.add(it) }
    }

That calls the Missile version now. And we can remove the addAll from Transaction. Now we have to send all our individual collections down, which is a bit of a drag:

    fun applyChanges(spaceObjectCollection: SpaceObjectCollection) {
        if (shouldClear ) spaceObjectCollection.clear()
        spaceObjectCollection.addScore(score)
        removes.forEach { spaceObjectCollection.remove(it)}
        deferredActionRemoves.forEach { spaceObjectCollection.remove(it)}
        asteroids.forEach { spaceObjectCollection.add(it)}
        missiles.forEach { spaceObjectCollection.add(it)}
        saucers.forEach { spaceObjectCollection.add(it)}
        ships.forEach { spaceObjectCollection.add(it)}
        splats.forEach { spaceObjectCollection.add(it)}
        deferredActionAdds.forEach { spaceObjectCollection.add(it)}
    }

I think this should be ready for prime time.

Not quite, we have this method used in tests:

    fun firstAdd(): SpaceObject = adds.toList()[0]

Fix the tests:

        val missile: Missile = trans.firstAdd() as Missile

That line turns into this:

        val missile: Missile = trans.missiles.first()

I find lots of references to adds in the tests. Grr. Easy to fix but lots of them.

I find that game supports add(SpaceObject) as a convenience for testing. Defer all those through knownObjects.

Quickly enough, I am green. But it was tedious, lots of changes all the same.

I have put a TODO in the method that sorted our individual space objects. I will now remove it entirely, and the generic one that called it.

We are green. Commit: Transaction now preserves all types.

There are a lot of warnings, mostly things like “no cast needed”. I’ll clean them up and report if any are interesting.

I remove most of them. None were interesting, just things like redundant casts left over from before we preserved the types.

The commit was 13 files, most of them tests, plus SpaceObjectCollection, from which we removed two methods, and Transaction, to which we added five collections and matching methods.

We are not preserving types on removes: there is no reason to do so and we apply them like this:

    fun allCollections(): List<MutableList<out SpaceObject>> {
        return listOf (asteroids, deferredActions, missiles, saucers, ships, splats)
    }

    fun remove(spaceObject: SpaceObject) {
        for ( coll in allCollections()) {
            coll.remove(spaceObject)
        }
    }

So that’s done. I wonder how many other type checks are in the system. I’ll search for is. There are a few in tests, probably redundant now but uninteresting in the grand scheme of things. And for as. None, other than in a commented-out test. It’s the one that was intermittent. It was checking that the Saucer reverses direction. We have another test for that now. Remove the commented stuff.

Test, green, commit: remove redundant commented-out test.

Let’s sum up.

Summary

The good news is that we have removed all the type-checking from the program, and nearly all of it from the tests. I am confident that the rest could readily be removed from the tests if we wanted to go to the trouble.

The price of preserving the types is a handful of type-specific collections in SpaceObjectCollection and transaction. The benefit comes from simplifying object interaction. I say “simplifying” … what we have now is a specific interaction for each possible type combination:

  • missile-missile
  • missile-asteroid
  • missile-saucer
  • missile-ship
  • asteroid-saucer
  • asteroid-ship
  • saucer-ship

The pairs are all executed both ways, a.interact(b) and b.interact(a). The interact methods are each defined in the relevant receiver class, with an overloaded method for each possible colliding “partner”. May of the resulting behaviors are the same, but some are a bit different.

There is a bit of duplication among the various collisions, but I don’t see how to remove it. Perhaps my betters will look at it this evening.

For now, we’ve done a good thing, and have completed the Type Preservation phase.

See you next time!