GitHub Repo

I’d like to push the new interaction scheme a bit further, returning something less basic than a Pair. A day to make the code a bit better.

In the Game, we have this code dealing with the new begin - interact - finish scheme:

    private fun finishInteractions() {
        val bufferAdds = mutableListOf<ISpaceObject>()
        val bufferRemoves = mutableSetOf<ISpaceObject>()
        knownObjects.forEach {
            val result: Pair<List<ISpaceObject>,Set<ISpaceObject>> = it.finishInteraction()
            bufferAdds.addAll(result.first)
            bufferRemoves.removeAll(result.second)
        }
        knownObjects.addAll(bufferAdds)
        knownObjects.removeAll(bufferRemoves)
    }

We can see a number of things here not to like, including but not limited to:

  1. The result of the call to finishInteraction is a Pair, an object with no brains to speak of, just capable of carrying two other things. Any other things. We use it here, and of course everyone who implements finishInteraction has to create one. An object of our own would probably be better.

  2. Do you think we should add before removing known objects, as we are here? Should we remove before adding? In principle it shouldn’t matter: we don’t expect one pass through interactions to create something and then destroy it. It’s almost impossible.

  3. We are buffering these things locally, then adding and removing them locally. Couldn’t the knownObjects collection give us a bit of help here? We could even move the buffering to it, in which case we’d need to be sure to tell it when to push the buffers into the main collection. Maybe that’s going too far, at least for now.

  4. Some of our adding and removing methods return a List, and some return a Set. I think the Set is used when multiple objects might try to remove the same thing, such as when you’re unfortunate enough to run into two asteroids at once. It may never happen, but in principle it could. It seems that it would be good to standardize on List or Set if we’re going to use them at all. We could conceivably use our own collection types throughout. I don’t see the advantage to that right now, but one would be better than two.

Let’s begin by creating a new object, a Transaction, which will contain one more more objects to be added or removed from our main SpaceObjectCollection. I think I’ll TDD it, because I’ve been derelict in that lately.

Here’s my test. Kind of a lot but I was on a roll:

class TransactionTest {
    fun newAsteroid(): SolidObject {
        return SolidObject.asteroid(U.randomPoint(), U.randomVelocity(1000.0))
    }
    
    @Test
    fun `transaction can add and remove`() {
        val t = Transaction()
        val add = newAsteroid()
        val rem = newAsteroid()
        t.add(add)
        t.remove(rem)
        val coll = SpaceObjectCollection()
        coll.add(rem)
        coll.transact(t)
        assertThat(coll.spaceObjects).contains(add)
        assertThat(coll.spaceObjects).doesNotContain(rem)
        assertThat(coll.size).isEqualTo(1)
    }
}

I thought the newAsteroid function might come in handy. I suspect we’ll use it a couple more times.

Let’s reorder the test so that it tells a better story, with some new names, before I even make it compile. As I think about describing it to you, I see it can be better.

    @Test
    fun `transaction can add and remove`() {
        val coll = SpaceObjectCollection()
        val aOne = newAsteroid()
        coll.add(aOne)
        val t = Transaction()
        val aTwo = newAsteroid()
        t.add(aTwo)
        t.remove(aOne)
        coll.transact(t)
        assertThat(coll.spaceObjects).contains(aTwo)
        assertThat(coll.spaceObjects).doesNotContain(aOne)
        assertThat(coll.size).isEqualTo(1)
    }

Right. We make a SpaceObjectCollection containing aOne. We create a transaction whose job is to add aTwo and remover aOne. We transact that with the collection and check the results. Nice.

We need a Transaction class, add and remove, and transact in SpaceObjectCollection.

class Transaction {
    private val adds = mutableListOf<ISpaceObject>()
    private val removes = mutableListOf<ISpaceObject>()
    
    fun add(spaceObject: ISpaceObject) {
        adds.add(spaceObject)
    }

    fun remove(spaceObject: ISpaceObject) {
        removes.add(spaceObject)
    }
}

IDEA walked me through most of this. Note that I did leave the adds as a list. It seems unlikely that anyone is going to add the same object twice. If they do, we’ll fix it in post. The test now wants a method transact on the SpaceObjectsCollection:

    fun transact(t: Transaction) {
        t.transact(this)
    }

I could have had this object rip the guts out of Transaction and devour them. Better to let the Transaction feed us what we should have:

    fun transact(spaceObjectCollection: SpaceObjectCollection) {
        spaceObjectCollection.removeAll(removes)
        spaceObjectCollection.addAll(adds)
    }

IDEA objects to this because removeAll expects a Set not a list. I reverse direction: I’m going to make both the collections in Transaction Sets. IDEA thinks we’re OK. Test. Green. Commit: Initial implementation of Transaction and SpaceObjectCollection.transact.

Now let’s change finishInteractions to expect a Transaction instead of a Pair. We’ll have to fix all the not very many implementors of finishInteraction as well.

    private fun finishInteractions() {
        val buffer = Transaction()
        knownObjects.forEach {
            val result: Transaction = it.finishInteraction()
            buffer.add(result)
        }
        knownObjects.transact(buffer)
    }

We’re demanding a new capability in Transaction, add. Should be easy enough.

Thinking
I am getting an odd feeling about how this may ultimately unwind. Is a Transaction really all that different from a SpaceObjectsCollection? We’ll see …

I’ll leave the add for now and address the larger issue, which is that I’ve changed expectations on finishInteraction in all the objects. We’ll let IDEA troll us through them.

The default, in ISpaceObject, becomes:

    fun finishInteraction(): Transaction = Transaction()

Now we’ll do the implementors. There might be only one so far:

    override fun finishInteraction(): Pair<List<ISpaceObject>, Set<ISpaceObject>> {
        if ( elapsedTime > 1.0  ) {
            elapsedTime = 0.0
            if (!sawAsteroid) {
                elapsedTime = -5.0
                return Pair(listOf(WaveMaker(4)), emptySet())
            }
        }
        return Pair(emptyList(), emptySet())
    }

We see that we might like to be able to initialize Transactions. First the obvious changes:

    override fun finishInteraction(): Transaction {
        if ( elapsedTime > 1.0  ) {
            elapsedTime = 0.0
            if (!sawAsteroid) {
                elapsedTime = -5.0
                return Pair(listOf(WaveMaker(4)), emptySet())
            }
        }
        return Transaction()
    }

But there in the middle, how should we do that? We can do this:

    override fun finishInteraction(): Transaction {
        if ( elapsedTime > 1.0  ) {
            elapsedTime = 0.0
            if (!sawAsteroid) {
                elapsedTime = -5.0
                val t = Transaction()
                t.add(WaveMaker(4))
                return t
            }
        }
        return Transaction()
    }

This ought to work. I am wondering whether there are other changes needed. Ask the computer: Test. Um yes, this is wrong:

    private fun finishInteractions() {
        val buffer = Transaction()
        knownObjects.forEach {
            val result: Transaction = it.finishInteraction()
            buffer.add(result)
        }
        knownObjects.transact(buffer)
    }

Transaction doesn’t have an add that accepts a Transaction. Let’s build one.

    fun add(transaction: Transaction) {
        transaction.transact(this)
    }

And we need an override in Transaction … no, wait, if we’re going to have that, let’s just have it and use it in finishInteractions: …

ARRGH
I’ve been interrupted and I have too many balls in the air. I dropped them all. Let’s return to my tests to work out what we need here.
    @Test
    fun `accumulate transactions`() {
        val toFill = Transaction()
        val filler = Transaction()
        val toAdd = newAsteroid()
        val toRemove = newAsteroid()
        filler.add(toAdd)
        filler.remove(toRemove)
        toFill.accumulate(filler)
        assertThat(filler.hasAdd(toAdd)).isEqualTo(true)
        assertThat(filler.hasRemove(toRemove)).isEqualTo(true)
    }

I think I’ll stay away from the transact word for this purpose. accumulate will work. I am also a little troubled by saying add and remove in this context. Sounds like we are adding and removing things to/from the transaction. I also added hasAdd and hasRemove to help my testing.

Let’s see if we can make this run.

    fun accumulate(t: Transaction) {
        t.adds.forEach {add(it)}
        t.removes.forEach {remove(it)}
    }

Tests are green. I’ve been interrupted again so am inclined to test the game just to regain my footing. Game’s good. Commit: WaveChecker uses new Transaction.

Let’s reflect. In fact, let’s summarize so far and see if we want to continue.

Summary (up to here)

We have a new object, Transaction, that can accumulate both objects to be added and objects to be removed. The SpaceObjectCollection knows how to “perform” a Transaction, causing the Transaction’s changes to be applied to the collection.

Hmm. I like the phrase apply changes. Let’s rename that in Transaction:

class SpaceObjectCollection ...
    fun applyChanges(t: Transaction) {
        t.transact(this)
    }

And let’s change the method we forward to as well:

class Transaction ...
    fun applyChanges(spaceObjectCollection: SpaceObjectCollection) {
        spaceObjectCollection.removeAll(removes)
        spaceObjectCollection.addAll(adds)
    }

Better, I think. If you want to look at the details, you can browse the project on GitHub, link at the top of the article.

We have no immediate need that I’m aware of, but now that the Transaction exists, we could return one of those from the other methods that return things to add or remove, namely interactWith and update. With those returning a Transaction, they would be free to do adds, removes, or both, while now, the interactions only remove and the update only adds. Doing that would add more capability and flexibility without adding complexity, and would probably open the door to some simpler special objects in the future.

We’ll make a note to make those changes only when we’re working in those areas anyway, or have a specific need calling for them. To do so now would be to incur a cost without a direct benefit.

Let’s think about things we might want to do next:

  1. Saucers, large and small;
  2. Wave sizes don’t change and are supposed to increase at least once, from 4 to 8 asteroids.
  3. Playing the game, I think the universal constants need a bit of tuning. Turning seems a bit slow, as does acceleration. At the same time, fine aiming is difficult even with the current slow turning. Anyway needs tuning.
  4. Limited supply of ships. That should be fun. ShipMonitor needs simplification already.
  5. Sounds. I have no idea how to do sounds. (I hate it when googling “sounds” OPENRNR kotlin returns one of my articles as #1. I know I’m no help on this.)

In terms of learning something useful, I imagine that sound might be the most valuable, depending what I might do next with Kotlin.

What would you find interesting? What would I find interesting? I guess we’ll have to wait and see. See you next time!