GitHub Decentralized Repo
GitHub Centralized Repo

Today, I think I’ll start on moving the WaveMaker logic into the game. There are issues.

Our overall quest is to move “all” the logic of the game into a more centralized form, basically under the control of the Game object, as a comparison to the decentralized design we started with. We require ourselves to work incrementally, keeping the game running all the time. And we’re prepared to make a bit of a mess, but we won’t be satisfied until we have a centralized design that makes sense and that we can be proud of.

The current phase of centralization is aimed at moving all the “special” objects out of the mix. These are all the objects other than missile, ship, saucer, and asteroid, that are in there invisibly, managing things by their interactions with each other and with the visible objects. I love that design, but this is an experiment in doing things a more conventional way, so each of those objects needs to move out of the mix and under more direct control of the Game or the SpaceObjectCollection, knownObjects. Ideally, I think, things should be close to Game, but yesterday it seemed that the ScoreKeeper needed to be held in knownObjects so that it could interact with Score. We did manage to keep both ScoreKeeper and Score out of the mix.

Today, my plan is to move the wave-making logic into a more centralized form. To begin, I think I’d better review how WaveMaker works. Here it is:

class WaveMaker(var numberToCreate: Int = 4): ISpaceObject, InteractingSpaceObject {
    private val oneShot = OneShot(4.0) { makeWave(it) }
    private var asteroidsMissing = true

    override fun update(deltaTime: Double, trans: Transaction) {}
    override fun callOther(other: InteractingSpaceObject, trans: Transaction) = Unit

    override val subscriptions = Subscriptions (
        beforeInteractions = { asteroidsMissing = true},
        interactWithAsteroid = { _, _ -> asteroidsMissing = false },
        afterInteractions = { if (asteroidsMissing) oneShot.execute(it) }
    )

    fun howMany(): Int {
        return numberToCreate.also {
            numberToCreate += 2
            if (numberToCreate > 11) numberToCreate = 11
        }
    }

    fun makeWave(it: Transaction) {
        for (i in 1..howMany()) {
            it.add(Asteroid(U.randomEdgePoint()))
        }
    }
}

WaveMaker uses the OneShot object. We’ll come back to that, but first let’s look at the object’s interactions:

  1. It assumes that the asteroids are missing;
  2. If it interacts with any asteroids, it notes that they are not missing after all;
  3. If they are missing, it executes the OneShot, passing it, which will be the transaction that was passed to the afterInteractions lambda.

The OneShot object looks like this:

class OneShot(private val delay: Double, private val action: (Transaction)->Unit) {
    var deferred: DeferredAction? = null
    fun execute(trans: Transaction) {
        deferred = deferred ?: DeferredAction(delay, trans) {
            deferred = null
            action(it)
        }
    }

    fun cancel(trans: Transaction) {
        deferred?.let { trans.remove(it); deferred = null }
    }
}

And DeferredAction looks like this:

class DeferredAction(
    private val delay: Double,
    initialTransaction: Transaction,
    private val action: (Transaction) -> Unit
) : ISpaceObject, InteractingSpaceObject {
    var elapsedTime = 0.0

    init {
        elapsedTime = 0.0
        initialTransaction.add(this)
    }

    override fun update(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        if (elapsedTime > delay ) {
            action(trans)
            trans.remove(this)
        }
    }

    override val subscriptions: Subscriptions = Subscriptions()
    override fun callOther(other: InteractingSpaceObject, trans: Transaction) {}
}

When it initializes, the DeferredAction adds itself to the mix via the initial transaction that it was passed. After doing that, it will be sent update messages as are all objects in the mix. The DeferredAction just waits out the timer and then applies its action, passing the transaction that comes with the update. Then the DeferredAction removes itself. In our case, the action is to call makeWave, because when we created the OneShot, it was like this:

class WaveMaker
    private val oneShot = OneShot(4.0) { makeWave(it) }

So. Four seconds after we said oneShot.execute(), we’ll get a call to this code:

class WaveMaker
    fun makeWave(it: Transaction) {
        for (i in 1..howMany()) {
            it.add(Asteroid(U.randomEdgePoint()))
        }
    }

That will create howMany() asteroids. The howMany function is this:

    fun howMany(): Int {
        return numberToCreate.also {
            numberToCreate += 2
            if (numberToCreate > 11) numberToCreate = 11
        }
    }

That slightly clever code returns 4, 6, 8, 10, 11, 11, …, the number of asteroids per wave. I think it maxed out at 11 because of memory constraints in the original game, but 11 is plenty for me anyway.

I think we can summarize WaveMaker this way:

Four seconds after the last existing asteroid is destroyed, a new wave of asteroids is created.

Our mission is to refactor or write new code so as to implement this behavior without use of the WaveMaker object or the DeferredAction in the mix. We want to do this in small steps, such that we’re able to commit the game and ship it after each step.

How might we do this? Let’s brainstorm a bit, speculating about ways we might go about this.

  • We’ll surely need some timing behavior like OneShot and DeferredAction provide. We need timing for wave-making, for the saucer, and for the ship’s emergence after being destroyed. So it is not out of the question for our game to have a variable collection of objects much like DeferredAction, doing timing but not in the general mix.

  • Wave-making itself is nearly trivial, amounting to adding a bunch of asteroids to the mix. The original game had a fixed array of asteroids which were either active or inactive, but I think here in the 21st century we’re OK with the variable collection we’re now using. We will change the interaction logic, but according to the sketch done last year, it should work just fine with a variable collection. So wave-making itself probably doesn’t need to change, except to move more into the Game arena.

  • I think I’d be more than OK with Game having a list of timers that it updates, and the DeferredAction could serve that purpose pretty readily. What we might do is cause knownObjects to intercept adding and removing DeferredAction instances, keeping them in a separate collection, not in the mix. Then Game could fetch the list from knownObjects and update it separately, during the update process.

  • There is the issue of what function the new DeferredAction would call. No, wait! In this scheme, surely Game will get the asteroid count explicitly and create the DeferredAction timer itself, so the DeferredAction will have access to Game’s methods. That should work fine.

  • In the interim, it will be possible for both Game and space objects to create DeferredActions. As long as we do the intercept correctly, that should work fine.

I think we’re beginning to come to a plan. It might go something like the list below. I’m trying to make each step as small as I can and able to be committed separately.

  1. Capture DeferredAction instances in a separate collection in the knownObjects SpaceObjectCollection, as well as in the general mix;
  2. Change knownObjects not to add them to the mix, and in the same commit, cause Game to update them directly;
  3. Provide a way to count existing asteroids, probably in knownObjects;
  4. In Game, implement a makeWave function;
  5. Cause Game to check asteroid count and create the DeferredAction to make the wave, and stop adding WaveMaker to the mix.

These seem like reasonable steps. I was thinking that we might need to remove the transaction on update but as long as we keep the DeferredAction instances in knownObjects, removing is appropriate. We’ll see: the code will tell us.

Let’s get to it:

1. Isolating DeferredActions

Might as well do a test:

    @Test
    fun `collection isolates DeferredObject instances`() {
        val s = SpaceObjectCollection()
        assertThat(s.deferredActions.size).isEqualTo(0)
        val deferred = DeferredAction(3.0, Transaction()) {}
        s.add(deferred)
        assertThat(s.deferredActions.size).isEqualTo(1)
    }

This won’t compile, but our implementation will fix that.

class SpaceObjectCollection
    ...
    val deferredActions = mutableListOf<ISpaceObject>()

    fun add(spaceObject: ISpaceObject) {
        if ( spaceObject is Score ) {
            scoreKeeper.addScore(spaceObject.score)
            return
        }
        spaceObjects.add(spaceObject)
        if ( spaceObject is DeferredAction) deferredActions.add(spaceObject)
        if (spaceObject is Missile) attackers.add(spaceObject)
        if (spaceObject is Ship) {
            attackers.add(spaceObject)
            targets.add(spaceObject)
        }
        if (spaceObject is Saucer)  {
            attackers.add(spaceObject)
            targets.add(spaceObject)
        }
        if (spaceObject is Asteroid) targets.add(spaceObject)
    }

I realize at this point that we’ll be switching from having an additional collection of DeferredAction instances to keeping them out of the main collection, so the test needs enhancement. For now, we’ll get this to go green. And it does. Commit: SpaceObjectCollection separately records DeferredAction instances.

The test needs robustification1 in two regards, and in fact I probably shouldn’t have committed, because we’re not dealing with removal. I go all out.

    @Test
    fun `collection isolates DeferredObject instances`() {
        val s = SpaceObjectCollection()
        assertThat(s.deferredActions.size).describedAs("deferred before").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all before").isEqualTo(0)
        val deferred = DeferredAction(3.0, Transaction()) {}
        s.add(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after add").isEqualTo(1)
        assertThat(s.spaceObjects.size).describedAs("all after add").isEqualTo(1) // will change
        s.remove(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after remove").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all after remove").isEqualTo(0)
    }

This will fail a bit, on “deferred after remove” if I’m not mistaken. Sure enough:

[deferred after remove] 
expected: 0
 but was: 1

Fix that:

    private fun removeAll(moribund: Set<ISpaceObject>) {
        spaceObjects.removeAll(moribund)
        attackers.removeAll(moribund)
        targets.removeAll(moribund)
        deferredActions.removeAll(moribund)
    }

Expect green. Get it. Commit: SpaceObjectCollection correctly removes DeferredAction instances from special collection.

OK, step one is complete. Would have been better to have done the remove the first time, but we live and learn. Live, anyway.

2. Remove DA from the mix

This is a two-step process, first not adding them to the mix and second, updating them explicitly in game.

You may be wondering why I broke this into two steps, since I’ll now have to undo work already done. The reason is that the step to isolate the DeferredAction was able to be done separately, all in SpaceObjectCollection and modifying two objects is harder than modifying one. So I wanted to get the basics in place in the SpaceObjectCollection and then figure out whatever we need to do in Game.

We might actually be able to do the job in Game without changing SpaceObjectCollection at all, though I think we’ll want to change it as a matter of safety. Let’s watch and find out.

In Game:

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        drawer?.let { draw(drawer) }
    }

    fun tick(deltaTime: Double) {
        val trans = Transaction()
        knownObjects.forEach { it.update(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

We want to update the deferredActions separately:

    fun tick(deltaTime: Double) {
        val deferredTrans = Transaction()
        knownObjects.deferredActions.forEach { it.update(deltaTime, deferredTrans)}
        knownObjects.applyChanges(deferredTrans)
        val trans = Transaction()
        knownObjects.forEach { it.update(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

Note that I use a separate Transaction for the deferred guys, and as a result, when we do the general update, they’ll be gone. So without changing SpaceObjectCollection, we’ve avoided double-tapping the deferred actions. We should be green, and the game should still work.

Curiously, a test fails, this one:

    @Test
    fun `missile and splat death`() {
        val mix = SpaceObjectCollection()
        val ship = Ship(
            position = U.randomPoint()
        )
        val missile = Missile(ship)
        mix.add(missile)
        val game = Game(mix)
        assertThat(mix.contains(missile)).isEqualTo(true)
        game.cycle(0.0)
        assertThat(mix.any { it is DeferredAction }).describedAs("deferred action should be present").isEqualTo(true)
        game.cycle(3.1)
        assertThat(mix.contains(missile)).describedAs("missile should be dead").isEqualTo(false)
        assertThat(mix.any { it is Splat }).describedAs("splat should be present").isEqualTo(true)
        game.cycle(3.2) // needs a tick to init
        game.cycle(5.3)
        assertThat(mix.any { it is Splat }).describedAs("splat should be gone").isEqualTo(false)
    }

The error isn’t the deferred action one, it is the “splat should be present” one.

This takes the wind out of my sails. I do not instantly see why this should have happened. Curiously enough, the game actually runs fine. I couldn’t resist trying it.

Let’s look at Missile and make sure we understand what it’s trying to do. Missile class is fairly large. We’ll consider the essence:

class Missile
    ...
    private val timeOut = OneShot(U.MISSILE_LIFETIME) {
        it.remove(this)
        it.add(Splat(this))
    }

    override fun update(deltaTime: Double, trans: Transaction) {
        timeOut.execute(trans)
        position = (position + velocity * deltaTime).cap()
    }

The OneShot is called that because we can tell it to execute as many times as we want, and it’ll only add one DeferredAction, the first time we call it. So the Missile should create a DA on the cycle(0.0). And we passed that assertion, so we know the DA is there. We then cycle again with deltaTime 3.1. We’d do better to compute that from U.MISSILE_LIFETIME, just to be sure:

    @Test
    fun `missile and splat death`() {
        val mix = SpaceObjectCollection()
        val ship = Ship(
            position = U.randomPoint()
        )
        val missile = Missile(ship)
        mix.add(missile)
        val game = Game(mix)
        assertThat(mix.contains(missile)).isEqualTo(true)
        game.cycle(0.0)
        assertThat(mix.any { it is DeferredAction }).describedAs("deferred action should be present").isEqualTo(true)
        game.cycle(U.MISSILE_LIFETIME + 0.1)
        assertThat(mix.contains(missile)).describedAs("missile should be dead").isEqualTo(false)
        assertThat(mix.any { it is Splat }).describedAs("splat should be present").isEqualTo(true)
        game.cycle(U.MISSILE_LIFETIME + 0.2) // needs a tick to init
        game.cycle(U.MISSILE_LIFETIME + 2.3) // Splat lifetime is 2.0
        assertThat(mix.any { it is Splat }).describedAs("splat should be gone").isEqualTo(false)
    }

Should be no change in the test. Right, it’s still wrong. Either the DA didn’t run, which seems very unlikely, or it added the Splat to the new deferredTrans and that somehow didn’t get into the mix, which also seems unlikely, because:

    fun tick(deltaTime: Double) {
        val deferredTrans = Transaction()
        knownObjects.deferredActions.forEach { it.update(deltaTime, deferredTrans)}
        knownObjects.applyChanges(deferredTrans)
        val trans = Transaction()
        knownObjects.forEach { it.update(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

Oh! As much as I’d like to do it this way, updating knownObjects after updating the deferred guys causes the new object, in this case the Splat, to be updated with the same deltaTime … so the splat times out. We just can’t do it that way. We need to do it this way:

    fun tick(deltaTime: Double) {
        val trans = Transaction()
        knownObjects.deferredActions.forEach { it.update(deltaTime, trans)}
        knownObjects.forEach { it.update(deltaTime, trans) }
        knownObjects.applyChanges(trans)
    }

To make this legitimate, we need, after all, to ensure that the DeferredAction instances don’t go into the mix:

    fun add(spaceObject: ISpaceObject) {
        if ( spaceObject is Score ) {
            scoreKeeper.addScore(spaceObject.score)
            return
        }
        if ( spaceObject is DeferredAction) {
            deferredActions.add(spaceObject)
            return
        }
        spaceObjects.add(spaceObject)

Now the DeferredActions never go into spaceObjects. I think we’ll be green now. We’re not, with a new error:

[deferred action should be present] 
expected: true
 but was: false

That’s OK because we don’t put the DA in the mix. Change the test:

    assertThat(mix.deferredActions
    	.any { it is DeferredAction })
    	.describedAs("deferred action should be present")
    	.isEqualTo(true)

Now I need a green pretty badly. Two different reds in a row is pretty daunting.

Arrgh. I didn’t notice that a number of other tests failed, they were below the line. They’re all just counting how many objects are in the mix. Any such count will be wrong if one of the ones counted was a DeferredAction. I’ll tick through them.

    @Test
    fun `game-centric saucer appears after seven seconds`() {
        // cycle receives ELAPSED TIME!
        val mix = SpaceObjectCollection()
        val saucer = Saucer()
        val maker = SaucerMaker(saucer)
        mix.add(maker)
        val game = Game(mix) // makes game without the standard init
        game.cycle(0.1) // ELAPSED seconds
        assertThat(mix.size).isEqualTo(1)
        assertThat(mix.deferredActions.size).isEqualTo(1)
        assertThat(mix.contains(maker)).describedAs("maker sticks around").isEqualTo(true)
        game.cycle(7.2) //ELAPSED seconds
        assertThat(mix.contains(saucer)).describedAs("saucer missing").isEqualTo(true)
        assertThat(mix.contains(maker)).describedAs("maker missing").isEqualTo(true)
        assertThat(mix.size).isEqualTo(2)
    }

Right. I needed to break out mix size and deferredActions size. We’re probably OK. Keep ticking.

My brand new deferred action test needs changing. I knew that. As it stands:

    @Test
    fun `collection isolates DeferredObject instances`() {
        val s = SpaceObjectCollection()
        assertThat(s.deferredActions.size).describedAs("deferred before").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all before").isEqualTo(0)
        val deferred = DeferredAction(3.0, Transaction()) {}
        s.add(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after add").isEqualTo(1)
        assertThat(s.spaceObjects.size).describedAs("all after add").isEqualTo(1) // will change
        s.remove(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after remove").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all after remove").isEqualTo(0)
    }

The error is:

[all after add] 
expected: 1
 but was: 0

That’s the line marked // will change. As corrected:

    @Test
    fun `collection isolates DeferredObject instances`() {
        val s = SpaceObjectCollection()
        assertThat(s.deferredActions.size).describedAs("deferred before").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all before").isEqualTo(0)
        val deferred = DeferredAction(3.0, Transaction()) {}
        s.add(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after add").isEqualTo(1)
        assertThat(s.spaceObjects.size).describedAs("all after add").isEqualTo(0) // DA's not added to mix
        s.remove(deferred)
        assertThat(s.deferredActions.size).describedAs("deferred after remove").isEqualTo(0)
        assertThat(s.spaceObjects.size).describedAs("all after remove").isEqualTo(0)
    }

I expect that to go green. It does. WaveMaker has a test failure:

expected: true
 but was: false

I should punish myself for not using describedAs often enough. Here’s the test:

    @Test
    fun `checker creates wave after 4 seconds`() {
        val mix = SpaceObjectCollection()
        val ck = WaveMaker()
        mix.add(ck)
        val game = Game(mix)
        game.cycle(0.1)
        assertThat(mix.any { it is DeferredAction }).isEqualTo(true)
        assertThat(mix.size).isEqualTo(2) // checker and TMW
        game.cycle(4.2)
        assertThat(mix.size).isEqualTo(5) // asteroids plus checker
        ck.update(0.5, Transaction())
    }

Right, we can’t make sure we got the DA that way. We do this:

    @Test
    fun `checker creates wave after 4 seconds`() {
        val mix = SpaceObjectCollection()
        val ck = WaveMaker()
        mix.add(ck)
        val game = Game(mix)
        game.cycle(0.1)
        assertThat(mix.deferredActions.size).isEqualTo(1)
        assertThat(mix.size).isEqualTo(1) // checker
        game.cycle(4.2)
        assertThat(mix.size).isEqualTo(5) // asteroids plus checker
        ck.update(0.5, Transaction())
    }

This should get us fully green. It does. Commit: DeferredAction instances are kept outside the mix and processed directly, in game.tick.

This commit was six files: Game and six separate tests. I’m a bit irritated at the need to change all those tests but the changes all made sense and so I am quite confident that we’re good.

OK, that was step two on the way to removing WaveMaker from the mix. The good news is that we have already removed DeferredAction from the mix. It still needs to be a SpaceObject but it doesn’t really need subscriptions and callOther. However, as I observed the other day, the hierarchy of InteractingSpaceObject and ISpaceObject is kind of upside down, so I can’t really remove the inheritance … unless I change all of SpaceObjectCollection to expect InteractingSpaceObject. We’re green.

Removed Bit …
I tried the experiment of inverting the hierarchy a couple of different ways and they didn’t work out. Because this article got long, I’ll spare you that commentary.

3. Count asteroids

We just want an asteroid-counting function in knownObjects SpaceObjectCollection. We can write a test for that.

    @Test
    fun `can count asteroids`() {
        val s = SpaceObjectCollection()
        s.add(Asteroid(Point.ZERO))
        s.add(Ship(U.CENTER_OF_UNIVERSE))
        s.add(Asteroid(Point.ZERO))
        s.add(Asteroid(Point.ZERO))
        assertThat(s.size).isEqualTo(4)
        assertThat(s.asteroidCount()).isEqualTo(3)
    }

That won’t compile yet. Should be easy:

    fun asteroidCount(): Int = targets.filter { it is Asteroid }.size

Green. Commit: SpaceObjectCollection can count asteroids.

That was easy. We’ve been here a while, what’s next, maybe we’ll break. There are two steps remaining in my original plan.

4. Create makeWave in Game

5. Change over to use the Game version of wave-making

I think this will break things. I’m going to do a spike and I will almost certainly throw it away, but I want to get a sense of how it’ll be done.

The current scheme has WaveMaker detecting that there are no asteroids at the end of interactions, and creating the DeferredAction (via OneShot) then.

I think I’ll just add the makeWave and howMany functions to Game. We’re just spiking here.

class Game
    private fun howMany(): Int {
        return numberOfAsteroidsToCreate.also {
            numberOfAsteroidsToCreate += 2
            if (numberOfAsteroidsToCreate > 11) numberOfAsteroidsToCreate = 11
        }
    }

    fun makeWave(trans: Transaction) {
        for (i in 1..howMany()) {
            trans.add(Asteroid(U.randomEdgePoint()))
        }
    }

Those aren’t quite right, you’ll see better versions in a moment.

Now lets do the real work. I think we need a OneShot so that we can trigger the creation of our making just once:

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        ensureWeHaveAsteroids()
        drawer?.let { draw(drawer) }
    }

And …

    private fun ensureWeHaveAsteroids() {
        if (knownObjects.asteroidCount() == 0 ) {
            val trans = Transaction()
            oneShot.execute(trans)
            knownObjects.applyChanges(trans) // add the DeferredAction
        }
    }
    private fun howMany(): Int {
        return numberOfAsteroidsToCreate.also {
            numberOfAsteroidsToCreate += 2
            if (numberOfAsteroidsToCreate > 11) numberOfAsteroidsToCreate = 11
        }
    }

    private fun makeWave() {
        println("making wave")
        for (i in 1..howMany()) {
            knownObjects.add(Asteroid(U.randomEdgePoint()))
        }
    }

The tricky bit was that I had to remember to build a transaction to contain the new DeferredAction, and apply it to get the action inside knownObjects. Curiously, the game starts and creates asteroids, for the attract screen, but when I insert a quarter, there is an immense delay before the ship appears, and asteroids never appear.

Well, this is a spike, so we’re here to learn.

After some delay …

I’ve learned a lot, and I’m going to stop for the day. Let me record what I’ve done.

I learned that the SpaceObjectCollection did not correctly clear itself, which resulted in more and more asteroids turning up but somehow not always appearing on the screen. The fix is:

    fun clear() {
        spaceObjects.clear()
        targets.clear()
        attackers.clear()
        deferredActions.clear()
    }

I’d like to improve this so that if I add a new collection it gets picked up immediately. There may be a way to do that. I’ll try to remember to do that soon …

In Game, I have this:

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
//        trans.add(WaveMaker())
        trans.add(SaucerMaker())
        val shipPosition = U.CENTER_OF_UNIVERSE
        val ship = Ship(shipPosition, controls)
        val shipChecker = ShipChecker(ship, scoreKeeper)
        trans.add(shipChecker)
    }

That just skips putting the WaveMaker into the mix. Then there’s all this:

class Game ...
    private var numberOfAsteroidsToCreate = 4
    private val oneShot = OneShot(4.0) { println("making in oneShot"); makeWave(it) }

    private fun initializeGame(controls: Controls, shipCount: Int) {
        numberOfAsteroidsToCreate = 4 // newly added
        knownObjects.performWithTransaction { trans ->
            createInitialObjects(trans,shipCount, controls)
        }
    }

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        tick(deltaTime)
        beginInteractions()
        processInteractions()
        finishInteractions()
        ensureWeHaveAsteroids()
        drawer?.let { draw(drawer) }
    }

    private fun ensureWeHaveAsteroids() {
        if (knownObjects.asteroidCount() == 0 ) {
            val trans = Transaction()
            oneShot.execute(trans)
            knownObjects.applyChanges(trans) // add the DeferredAction
        } else {
        }
    }

    private fun howMany(): Int {
        return numberOfAsteroidsToCreate.also {
            numberOfAsteroidsToCreate += 2
            if (numberOfAsteroidsToCreate > 11) numberOfAsteroidsToCreate = 11
        }
    }

    private fun makeWave(ignored: Transaction) {
        for (i in 1..howMany()) {
            knownObjects.add(Asteroid(U.randomEdgePoint()))
        }
    }

I guess I could use the transaction in makeWave and then immediately apply it. Things are different, because we’re in Game and therefore not following the general interaction protocol.

There’s something odd going on here, because once in a while the “making in oneShot” message comes out right in the middle of things and the game adds a bunch more asteroids.

Additionally, if I start the game and insert a quarter before the initial asteroids show up for the attract screen, I never get any asteroids at all.

I think the issue is that the oneShot isn’t triggering because I don’t let it time out. I should probably cancel it when I start the game. I’ll try that but it’s really time to roll this back and let it cook in my brain.

Trying this:

    private fun initializeGame(controls: Controls, shipCount: Int) {
        numberOfAsteroidsToCreate = 4
        knownObjects.clear()
        oneShot.cancel(Transaction())
        knownObjects.performWithTransaction { trans ->
            createInitialObjects(trans,shipCount, controls)
        }
    }

That makes the game create asteroids even if I insert quarter right away. However, I put in a print and it’s telling me something bad:

    var once = true
    private fun ensureWeHaveAsteroids() {
        println("ensure")
        if (knownObjects.asteroidCount() == 0 ) {
            if ( once ) {
                println("need asteroids ${oneShot.deferred}")
//                once = false
            }
            val trans = Transaction()
            oneShot.execute(trans)
            println("adds ${trans.adds.size}")
            knownObjects.applyChanges(trans) // add the DeferredAction
        } else {
            println("${knownObjects.asteroidCount()} asteroids")
        }
    }

That last line should always print however many asteroids are on the screen. I’m sitting here watching it print wrong numbers. For the longest time it said “1 asteroids” while there were at least seven on the screen. But:

    fun asteroidCount(): Int = targets.filter { it is Asteroid }.size

Now can that be wrong? I’m not sure why, but I can see that it is only counting the large asteroids. Medium and small apparently do not count. I also try it this way with the same result:

    fun asteroidCount(): Int = targets.filterIsInstance<Asteroid>().size

If I change that code to check spaceObjects, it works:

    fun forEach(spaceObject: (ISpaceObject)->Unit) = spaceObjects.forEach(spaceObject)

So split Asteroids do not get set into targets? How can that be?

Hmm. Asteroids split in their finalize:

    private fun finalize(): List<ISpaceObject> {
        val objectsToAdd: MutableList<ISpaceObject> = mutableListOf()
        if (splitCount >= 1) {
            objectsToAdd.add(asSplit(this))
            objectsToAdd.add(asSplit(this))
        }
        return objectsToAdd
    }

Finalize is different, in that it doesn’t use a transaction. Could that be the issue? Ha! It is:

class SpaceObjectCollection

    fun removeAndFinalizeAll(moribund: Set<ISpaceObject>) {
        moribund.forEach { spaceObjects += it.subscriptions.finalize() }
        removeAll(moribund)
    }

I add them directly. Bad Ron, no biscuit. Fix:

    fun removeAndFinalizeAll(moribund: Set<ISpaceObject>) {
        moribund.forEach { addAll(it.subscriptions.finalize()) }
        removeAll(moribund)
    }

I think this should work now. Yes even with the filter set back to checking targets, everything seems actually to be working. Too much hackery went on to commit this, however. Since I’ve got a record here, I won’t even stash it or branch it or whatever the youngsters do. I will, however, fix the removal problem and the adding problem in SpaceObjectCollection.

Commit: fix SpaceObjectCollection.clear to clear all sub-collections and fix removeAndFinalizeAll to use addAll to ensure proper classification.

I roll back Game. All the tests are green, and the game works. Let’s sum up. I’m working overtime here.

Summary

I went longer than I like to. With a break of maybe 30-45 minutes, I’ve been at this for five hours. I spent much of that debugging, of course, trying to figure out why my spike didn’t work. And it was a somewhat reasonable investment, because neither of the problems were covered by tests and I’d have been forced into debugging anyway. Sometimes our tests show a weakness like that, and in my opinion, when that happens, we should improve the tests. I’m too tired to do that now, but I’ve made two red sticky notes to remind me to do it.

I believe rather strongly that the spike was working correctly at the end there, but I had made a stronger than usual commitment to myself to delete it, and there had been enough issues, so I’m happy to have rolled it back, except for the two bug fixes.

When we redo this final bit of work, probably tomorrow, it should go smoothly, and we should be able to finish removing WaveMaker.

One note to self: I need to be more careful with the OneShot instance in Game, to be sure to cancel it. Red note for that as well.

Overall, a decent morning, but a bit more work than I like to do in one session. I feel decent, though, and the end wasn’t a debacle, so it must have been OK. Still, I will take a well-deserved break.

Join me next time for the thrilling conclusion …



  1. For some reason, my spell checker does not recognize this word.