GitHub Repo

We have an interesting architecture going here, unlike any I’ve used in these games before. Let’s see if it can go all the way.

In previous versions of Asteroids and similar games, it seems that I’ve always had a kind of “universe” object that makes the big decisions and orchestrates the overall game. In this version, so far, we don’t have that. We have a central game loop, but all it does is tell all the objects to update (their position, velocity, whatever), then it offers them all a chance to interact (called “colliding” at the moment) and then it gives them each a chance to draw itself on the screen:

    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        update(deltaTime)
        processCollisions()
        draw(drawer)
    }

For a while, there was only one kind of “Flyer” in the game, which took on the guise of a ship, an asteroid, or a missile. Everything that the game did, from the viewpoint of a viewer outside, like thee and me, came about through the flyers interacting.

In update, we just do this:

    fun update(deltaTime: Double) {
        val adds = mutableListOf<IFlyer>()
        flyers.forEach {
            adds.addAll(it.update(deltaTime))
        }
        flyers.addAll(adds)
    }

When we update an object, it might return additional objects to be added to the flyers. So we add them. We don’t know what they are: we just add them.

In processCollisions, the result of the objects interacting could include existing objects disappearing, such as a missile hitting an asteroid), so we unconditionally remove colliding objects. But collisions can produce new objects coming, such as smaller asteroid splits, so we add the detailed result of split to the Flyers collection:

    fun processCollisions() {
        val colliding = colliders()
        flyers.removeAll(colliding)
        for (collider in colliding) {
            val splitOnes = collider.split()
            flyers.addAll(splitOnes)
        }
    }
Aside
I am tempted to refactor some of this, at least to the extent of renaming, as I introduce my thinking. I think we’ll hold off on that for just a bit, but let me foreshadow here by saying that the word “interacting” might be better here than “colliding”, and there might be a better term here than “split”, which tells the object what to do rather than informing it that it was involved in an interaction.

So when flyers interact, they are assumed to be going to disappear, and when an object that was in an interaction (collision) is told to split, it can return any new (or old) flyer objects that it thinks should exist. Note that none of this code knows what kinds of flyers there are: it just manipulates them.

Then we draw all the objects. A flyer has its own draw method, and the main objects, ship, asteroid, and missile all have a “view” that does the actual drawing:

    override fun draw(drawer: Drawer) {
        val center = Vector2(drawer.width/2.0, drawer.height/2.0)
        drawer.fill = ColorRGBa.MEDIUM_SLATE_BLUE
        drawer.translate(position)
        view.draw(this, drawer)
    }

We probably don’t need that drawer.fill there. If a view wants a fill, it can ask for it.

So there we are. There is no central all-knowing brain that controls the universe. There’s just the chance for every pair of objects to interact. Some of them interact by killing each other: ships, asteroids, and missiles are mutually destructive.

I’m not sure why I didn’t build in a brain, and I’m not sure why I resisted making ships and asteroids and missiles unique classes. I just wanted to keep things simple. But then something interesting happened.

A Very Different “Flyer”

Over the past couple of days, we created the ShipMonitor. We had a particular need: when a ship is destroyed, we need to get a new one. And, when the need arose, I wanted it to come back immediately, just as it was, although in future stories, we’ll have time delays and new positions and such.

There were a few possibilities for how to manage the ship’s reappearance, including:

  1. The game could search the Flyers for the ship. This would be a bit tricky, because even the ship doesn’t know it’s a ship. It happens to have a particular radius and view, but it has no special type.

  2. The ship, upon dying, could send a message to Game (or return a result, perhaps) telling the game that there was no ship. This, too, would be tricky, because if the ship did this, so would missiles and asteroids, and the game wouldn’t know what to do.

  3. What we actually did, which was the ShipMonitor.

Rather than take some global action, it came to me that there could be an object somewhere in space, interacting with all the other flyers. This one would note whether or not it interacted with the ship (which it would be privately acquainted with) and if it found no ship, it would take the appropriate action, creating a new one. (Frills like when and where to be determined later.)

So that’s what we did. It was a bit tricky to think about but now we have the ShipMonitor1 object, Its inner behavior is roughly as follows. The object starts having seen the ship.

Update
If we have not seen the ship, go “active”, in initiating the behavior that creates a new ship. Otherwise, mark that we are looking for the ship.
Collision
If we are looking for the ship and see it, mark that we have seen it. If we are “active”, however, no matter what we see, return the ship, so that it is recreated. Enter a state that causes us not to consider any more collisions until next time around. Return ourselves so that we’ll get a call to split.
Split
When we are offered the opportunity to split, we know that we’ve been removed. We return ourselves and the ship. Now there’s a ship and a monitor and we’re back to steady state.
Aside
As I describe this, I realize that what I’ve done (I never blame “us” for mistakes) may be more complicated than it needs to be. We’ll revisit that before we elaborate the scheme.

The “insight”

The insight, if I may give it such a self-important name, is that a simple object in the mix can deal with global state of the mix, because it sees all the other objects during the interaction (collision) phase, and can take actions that are significant to the global state.

If that only worked for the ShipMonitor, that would already be just fine. There’s some value, I think, to avoiding centralizing something that can be done locally. But the idea be used for more things than that. For example: the game is supposed to display the score on the screen. Instead of having special code in game to do that, we could have a flyer (that never moves) whose view is the current score. How would it get the values? Through some interaction, yet to be defined, with all the objects in the universe. Those that wanted to score would pass the information to the Score object, which would display it.

Honestly, I think this is rather nice

To me, there’s something elegant about doing global things through the interaction of objects acting locally, rather than view some god object that oversees everything and makes presumably beneficial moves based on global knowledge. Instead, the objects interact and the right things happen.

I plan to continue to push this architecture to see whether it can do everything we need. Before we begin to push further, we need to discuss some intricacy, and to see whether we can improve what we have.

The Intricacy

The collision code is careful to interact every pair of objects just once a::b or b::a, rather than twice, both a::b and b::a. Since objects do not know each others’ type, and perhaps just due to how the code evolved, the original collision logic returned all the colliding objects to the game, which then removed them all, sent split to each one, and added back in anything that returned from split. That allowed an asteroid to split if it was large enough, returning two smaller ones, but a missile, which just terminates, would return nothing.

At that point, from the viewpoint of the game, everything was alike. If it collided, it got to split. The name was chosen at a time when all they did was split (or get so small as not to split). And, when this was first done, there were always two objects involved in the collision and both were destroyed and both subsequently got the chance to split. It didn’t matter whether the collision was missile::asteroid or asteroid::missile. Either case was dealt with the same: both are removed, each gets a chance to put things back into the flyers. It was elegant for its time, or seemed so to me.

However, with the ShipMonitor, things had to be different. If the proposed interaction was monitor::ship, the monitor would see the ship and all would be well. But if it happened to be ship::monitor, the ship would have no idea that there was anything special going on … and the monitor wouldn’t reliably know whether there was a ship in the mix or not.

That led to the Double Dispatch. In essence, it allows the basic Flyers (missile, ship, asteroid, …) to interact in any order, but it ensures that our special objects (only ShipMonitor so far) always get to run their code on every collision. It goes like this:

Double Dispatch
When a basic flyer sees alice.collisionDamageWith(bob), it always does bob.collisionDamageWithOther(alice). The latter method does the work. We just bounce back and do the work except now it’s bob running the code rather than alice. Either way, they both get returned as having been involved, both get removed, and both get a chance to split. The second dispatch has no noticeable effect. But suppose the interacting pair are alice and monitor.

In that case, we have alice.collisionDamageWith(monitor) and she does monitor.collisionDamageWithOther(alice) … and now we are in the monitor’s code, not the basic flyer code!

On the other hand, had the pairing already been monitor first, we’d have monitor.collisionDamageWith(alice) and again the monitor gets the ball.

So the Double Dispatch allows any IFlyer who isn’t basic to be sure that it will get to make all the decisions about what its interactions are, while the regular basic flyers just carry on. It’s a bit tricksy to think about, for me at least, but it handles cases like this very cleanly.

Double Dispatch, in my experience, is most commonly used when something needs to be done based on the type of an object. It avoids actually checking for the type, instead typically informing one of the recipients about the type of the other.

An arithmetic hierarchy will commonly include lots of this sort of thing. aFloat+anInteger might lead to aFloat.plus(anInteger), which would double dispatch: anInteger.plusWithFloat(afloat) and now we are in code that knows the type of both variables without anyone ever checking type. It’s handy. And tricksy to get right, especially as the complexity grows. We’ll try to avoid that problem.

Refactoring Opportunities

The evolution of the code has offered us some opportunities for improvement. I think that the ShipMonitor’s behavior, while correct, can be simpler. And the names of the methods are on the one hand awkward and on the other hand still don’t communicate well.

Finally, I think that possibly the practice of getting the colliders back (in basic Flyers) and then telling them all to split could perhaps be simpler. Of that, I’m less sure than I am about the monitor.

Let’s first review the adding and removing that we now support.

  1. On update, an object returns a collection, generally empty, of objects to be added. Ship uses this to return a missile. As far as I know, no one else is using this ability … yet.
  2. In the collision logic, we interact all pairs, and each such interaction returns a collection, possibly empty, of objects. These are unconditionally removed from Flyers, and are subsequently unconditionally sent a message, currently split. (Perhaps youAreDead or removed would be better. How about finalize?)
  3. The split (finalize) call can return a collection of objects that are unconditionally added to the Flyers.

A question …

This list triggers a question from the back of the room. Suppose that when the ship is killed, we want to create an explosion. We could do that by returning from split (finalize) a list of fragments that fly about for a while. The question is … How do these objects cease to exist?

They could certainly have a timer and when the timer runs down, they’d know their time was up. (We have the same issue with a missile, which should fly for N seconds and then fizzle.) When do they get a chance to be removed from Flyers? Update returns new objects. Collision returns objects to be removed, but who are these people colliding with? Perhaps no one. And split adds, it doesn’t remove. It would seem that for an object to time out, it must collide with something.

Oh. How about a clock? They could collide with a clock, which could return them to be finalized.

OK, thanks for that question. I think that with something like a clock, our scheme can handle objects self-terminating. We’ll try it soon. Let’s move on.

Let’s do some renaming. First, split. Let’s rename that to finalize. The rule is, when an object is sent finalize it is because it has already been removed from the Flyers. This is its chance to scream in agony or to go out in a blaze of glory, by returning some objects to be added. An undying object could return itself.

Do the rename …

interface IFlyer {
    abstract val killRadius: Double
    abstract val position: Vector2
    abstract val ignoreCollisions: Boolean
    fun collisionDamageWith(other: IFlyer): List<IFlyer>
    fun collisionDamageWithOther(other: IFlyer): List<IFlyer>
    fun draw(drawer: Drawer)
    fun move(deltaTime: Double)
    fun finalize(): List<IFlyer>
    fun update(deltaTime: Double): List<IFlyer>
}

IDEA seems to think that it has renamed it everywhere.

I should have run the tests first. I think I have a test or two failing because of a tweak made to the world parameters. Roll that back and test.

Telling On Myself
A handful of tests needed updating. Clearly I hadn’t run the tests since I changed the universal values of acceleration and such. And it even looked as if I changed the implicit return from ship update somewhere and didn’t fix some tests.

This, I think, is the pernicious problem of relying on running the game and looking at the screen. We rely less on out tests than we should, and they can drift out of date. I need to be more careful with this.

Where were we? Oh, right, renaming split to, what was it, finalize. IDEA does the rename, tests run green. That’s how it’s supposed to work. There are comments and other methods with split in them. Let’s find and fix. We’ll see some of those as we go forward. I renamed some tests to refer to finalize, and left the split notion where it makes sense, like in Flyers, which do have a splitCount, for example.

Test, to be sure. Commit: Rename split to finalize.

Now let’s look at the colliding-related names.

In Game:

class Game ...
    fun cycle(drawer: Drawer, seconds: Double) {
        val deltaTime = seconds - lastTime
        lastTime = seconds
        update(deltaTime)
        processCollisions()
        draw(drawer)
    }

Let’s rename that to processInteractions. That leads to this method:

    fun processInteractions() {
        val colliding = colliders()
        flyers.removeAll(colliding)
        for (collider in colliding) {
            val addedByFinalize = collider.finalize()
            flyers.addAll(addedByFinalize)
        }
    }

These objects are not colliding, they are objects that have passed on, that are no more, that have ceased to be. They’ve expired, are off the twig, have shuffled off their mortal coil. What shall we cal the result? I’m not sure so let’s rename colliders instead. What have we here?

    fun colliders() = flyers.collectFromPairs { f1, f2 -> f1.collisionDamageWith(f2) }

They’re objects that are to be finalized. Let’s think about that other method collisionDamageWith. We’ll even look at it.

What are we doing?
What we’re doing is sometimes called System of Names. We’re looking at a series of related operations and their names, and seeing how to revise them to better tell the story of what’s going on.

Back in processInteractions, let’s say this:

    fun processInteractions() {
        val toBeRemoved = colliders()
        flyers.removeAll(toBeRemoved)
        for (removedObject in toBeRemoved) {
            val addedByFinalize = removedObject.finalize()
            flyers.addAll(addedByFinalize)
        }
    }

The names toBeRemoved and removedObject seem to me to make more sense.

What about colliders? The method really just performs interactions, pairwise. But it happens to return objects to be removed. (At one point I had thought that it might want to return objects to be added as well. My main reasons for not providing for that were, first, I didn’t need it at the time, and second, returning two things from a function isn’t commonly done.)

The method is something like pairwise interactions returning objects to be removed. That’s rather a longer name than we have time for.

I wouldn’t often recommend what I’m about to do. Well, part of it I would. I don’t see better names further down, so I’m going to commit these and move on. That’s sensible: sitting here making up long names, less so. So let’s test. Green. Commit: Renaming collision-related notions to better communicate.

ShipMonitor

Recall that I thought ShipMonitor could be improved. Its improvement may help us think about the interactions and their changes. Here’s ShipMonitor in toto[smalldog], except for methods that do nothing. So maybe not so much “toto”.

class ShipMonitor(val ship: Flyer) : IFlyer {
    override val ignoreCollisions = false
    override val killRadius: Double = -Double.MAX_VALUE
    override val position: Vector2 = Vector2.ZERO
    var state: ShipMonitorState = ShipMonitorState.HaveSeenShip

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        var result:List<IFlyer> = emptyList()
        state = when {
            state == ShipMonitorState.Active -> {
                result = listOf(this)
                ShipMonitorState.DoneReporting
            }
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        return result
    }

    override fun collisionDamageWithOther(other: IFlyer): List<IFlyer> {
        return collisionDamageWith(other)
    }

    override fun finalize(): List<IFlyer> {
        state = ShipMonitorState.HaveSeenShip
        return listOf(this,ship)
    }

    override fun update(deltaTime: Double): List<IFlyer> {
        state = when (state) {
            ShipMonitorState.LookingForShip -> ShipMonitorState.Active
            else -> ShipMonitorState.LookingForShip
        }
        return emptyList()
    }
}

We could get rid of the ignoreCollisions flag here but it would require some nasty casting that I am not willing to do, over in Flyer. In ShipMonitor it’s not used.

Let me tag the returns with what they are. I think it’ll help us recognize possibilities for improvement.

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        var toBeRemoved:List<IFlyer> = emptyList()
        state = when {
            state == ShipMonitorState.Active -> {
                toBeRemoved = listOf(this)
                ShipMonitorState.DoneReporting
            }
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        return toBeRemoved
    }

Where possible, I’ll comment by changing the name rather than appending a comment like this:

// var results // these guys get removed

Moving on we change to this:

    override fun finalize(): List<IFlyer> {
        state = ShipMonitorState.HaveSeenShip
        val toBeAdded = listOf(this,ship)
        return toBeAdded
    }

Here, the var is able to be inlined and IDEA is nagging me about it. I’d like to keep the name, though. I think we’ll leave it this way for clarity, though a comment would be OK if you prefer.

    override fun update(deltaTime: Double): List<IFlyer> {
        state = when (state) {
            ShipMonitorState.LookingForShip -> ShipMonitorState.Active
            else -> ShipMonitorState.LookingForShip
        }
        val toBeAdded:List<IFlyer> = emptyList()
        return toBeAdded
    }

I did this renaming because I wanted to underline a possibility. When we do the interaction (currently called various forms of collision) we add the ShipMonitor to be removed, which will get us a callback on finalize, which we use to recreate ourselves, and the ship. We can instead not return ourselves to be removed and not add ourselves back in to be recreated. Like this:

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        state = when {
            state == ShipMonitorState.Active -> {
                ShipMonitorState.DoneReporting
            }
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        val toBeRemoved:List<IFlyer> = emptyList()
        return toBeRemoved
    }

    override fun finalize(): List<IFlyer> {
        state = ShipMonitorState.HaveSeenShip
        val toBeAdded = listOf(ship)
        return toBeAdded
    }

We’ve not removed ourselves in the collision logic, nor added ourselves back in finalize. Should be no effect. Test. A test demurs: it’s checking the first return. Fix the test. Ah, no. We will get no call to our finalize, because we didn’t schedule ourselves for destruction. Belay that idea.

But I still think we can do the job more simply. Remember a day or so ago when I simplified the ShipMonitor test after making the monitor work (at all)? I had thought there would be one more pass through update.

Suppose that we want ShipMonitor not to remove itself, so that it won’t have to deal with finalize and will therefore become simpler. Presently, when it becomes active, it does two things:

In the collision code, it returns itself so as to get finalize. In finalize it returns itself and the ship, thus reviving the ship and itself. In update, we just do this:

What if, instead, we were to return the “new” ship from there when active? Then couldn’t we remove all the tricksy bits with returning ourselves and recreating us later? Let’s try that. I do think that the long scenario test may take another pass to work. We’ll see.

Remake the changes from the previous attempt:

I think we can remove the entire state==Active from the collision …

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        state = when {
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        var noOneDamaged:List<IFlyer> = emptyList()
        return noOneDamaged
    }

Now we will not see a call to finalize. We can do this. I believe it is never called.

    override fun finalize(): List<IFlyer> {
        val neverCalled = emptyList()
        return neverCalled
    }

Now the only thing that happens so far is that if we see a ship, we set state to HaveSeenShip. Now in update:

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.LookingForShip -> {
                toBeCreated = listOf(ship)
                state
            }
            else -> ShipMonitorState.LookingForShip
        }
        return toBeCreated
    }

There’s more in here than we need but what we have here is that if we ever get to update while looking for a ship, we know that last time through there was none, so we recreate the ship. Otherwise state must be HaveSeenShip (I think) so we set to looking and carry on.

I think this ought to play correctly and may or may not pass the detailed tests, some of which check the details of the returns, not whether the ship is around at the right time.

As I thought, the game plays correctly. Let’s see what the tests are whining about.

The failing one is the big story test, which looks like this. The only interesting part is the very last section:

    @Test
    fun `ship monitor correctly adds a new ship`() {
        val sixtieth = 1.0/60.0
        val ship = Flyer.ship(Vector2(1000.0, 1000.0))
        val asteroid = Flyer.asteroid(Vector2.ZERO, Vector2.ZERO)
        val monitor = ShipMonitor(ship)
        val game = Game()
        game.add(ship)
        game.add(asteroid)
        game.add(monitor)
        assertThat(game.flyers.size).isEqualTo(3)
        assertThat(game.flyers.flyers).contains(ship)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.HaveSeenShip)

        // nothing colliding
        game.update(sixtieth)
        game.processInteractions()
        assertThat(game.flyers.size).isEqualTo(3)
        assertThat(game.flyers.flyers).contains(ship)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.HaveSeenShip)

        // ship colliding, make two asteroids and lose ship
        ship.position = Vector2.ZERO
        game.update(sixtieth)
        game.processInteractions()
        assertThat(game.flyers.size).isEqualTo(3)
        assertThat(game.flyers.flyers).doesNotContain(ship)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.HaveSeenShip)

        // now we discover the missing ship
        game.update(sixtieth)
        game.processInteractions()
        assertThat(game.flyers.size).isEqualTo(3)
        assertThat(game.flyers.flyers).doesNotContain(ship)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.LookingForShip)

        // now we go active, return monitor damaged, get split,
        // thus adding in ship and monitor.
        game.update(sixtieth)
        assertThat(monitor.state).describedAs("just switched").isEqualTo(ShipMonitorState.Active)
        game.processInteractions()
        assertThat(game.flyers.flyers).contains(ship)
        assertThat(game.flyers.flyers).contains(monitor)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.HaveSeenShip)
    }

When we get to the “now we go active” bit, we are in lookingForShipState. It follows that after we call update, the ship will already be there, and the state never goes to Active. Recast:

        // There has been no ship. Update should add it.
        // thus adding in ship and monitor.
        game.update(sixtieth)
        assertThat(game.flyers.flyers).contains(ship)
        assertThat(monitor.state).describedAs("just switched").isEqualTo(ShipMonitorState.HaveSeenShip)
        game.processInteractions()
        assertThat(game.flyers.flyers).describedAs("after interactions after adding").contains(ship)
        assertThat(game.flyers.flyers).contains(monitor)
        assertThat(monitor.state).isEqualTo(ShipMonitorState.HaveSeenShip)

I think the state check (just switched) fails now, and that we should fix it. Test.

[just switched] 
expected: HaveSeenShip
 but was: LookingForShip

Right. Fix:

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.LookingForShip -> {
                toBeCreated = listOf(ship)
                ShipMonitorState.HaveSeenShip
            }
            else -> ShipMonitorState.LookingForShip
        }
        return toBeCreated
    }

I expect green. I do not get it.

[after interactions after adding] 
Expecting ArrayList:
  [com.ronjeffries.ship.ShipMonitor@5bd82fed,
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0)]
to contain:
  [Flyer Vector2(x=0.0, y=0.0) (150.0)]
but could not find the following element(s):
  [Flyer Vector2(x=0.0, y=0.0) (150.0)]

The ship has vanished during the processInteractons. Why? Those four objects there might be a clue. When an asteroid splits once, its killRadius goes from 500 to 250. When it splits again, the radius goes from 250 to 125. We have four split asteroids here, all at zero zero, where the ship was as well. We’ve destroyed it the instant it came back, because it rezzed on top of the new spit-off asteroids.

This is more interesting than it seems … because it is very likely that in the real game, we’re rezzing a lot more ships than I realize. I have to find out. Add a couple of prints.

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.LookingForShip -> {
                println("reviving ship")
                toBeCreated = listOf(ship)
                ShipMonitorState.HaveSeenShip
            }
            else -> ShipMonitorState.LookingForShip
        }
        return toBeCreated
    }

    override fun finalize(): List<Flyer> {
        if (splitCount < 1) return listOf()
        println("splitting")
        val meSplit = asSplit()
        val newGuy = meSplit.asTwin()
        return listOf(meSplit, newGuy)
    }

Sure enough, the test says this:

splitting
reviving ship
splitting
splitting

[after interactions after adding] 
Expecting ArrayList:
  [com.ronjeffries.ship.ShipMonitor@5bd82fed,
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0),
    Flyer Vector2(x=0.0, y=0.0) (125.0)]
to contain:
  [Flyer Vector2(x=0.0, y=0.0) (150.0)]
but could not find the following element(s):
  [Flyer Vector2(x=0.0, y=0.0) (150.0)]

Now in game play I’m not seeing the same effect. It’s harder to tell, and of course new asteroids in the real game do move. Maybe in the game they’re just getting out of the way in time. I’d really like to move these out of the way so that the scenario test makes more sense, but it’s already pretty intricate. I add this after discovering that the asteroid has split and destroyed the ship:

        // remove asteroids to avoid multiple collisions (hack)
        game.flyers.forEach { it.move(1.0)}

I just motor them away for a full second. They’ll be well away after that, their velocity is 1000, so they’ll be a tenth of the screen away. The test passes.

Have we discovered a fatal flaw? I argue now, because I have knowledge of the future.

In the future, we won’t instantly bring the ship back to life after it passes, all untimely, from mortal existence. Instead, we will wait a respectful interval and then replace it, and we’ll replace it in a relatively safe position, with no asteroids near by. So in the fullness of time, even if the game is fixing ships a few times per collision now, they won’t be doing it any more.

So this change is working and it is good. Let’s get our stuff together, commit this and clean it up. Commit: simplify ShipMonitor logic not to destroy itself just to get a wake-up call.

Now I think we are only using a couple of states in ShipMonitor. Would you like to take another look?

enum class ShipMonitorState {
    HaveSeenShip, LookingForShip, Active, DoneReporting
}

class ShipMonitor(val ship: Flyer) : IFlyer {
    override val ignoreCollisions = false
    override val killRadius: Double = -Double.MAX_VALUE
    override val position: Vector2 = Vector2.ZERO
    var state: ShipMonitorState = ShipMonitorState.HaveSeenShip

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        state = when {
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        var noOneDamaged:List<IFlyer> = emptyList()
        return noOneDamaged
    }

    override fun collisionDamageWithOther(other: IFlyer): List<IFlyer> {
        return collisionDamageWith(other)
    }

    override fun draw(drawer: Drawer) {
    }

    override fun move(deltaTime: Double) {
    }

    override fun finalize(): List<IFlyer> {
        val neverCalled: List<IFlyer> = emptyList()
        return neverCalled
    }

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.LookingForShip -> {
                toBeCreated = listOf(ship)
                ShipMonitorState.HaveSeenShip
            }
            else -> ShipMonitorState.LookingForShip
        }
        return toBeCreated
    }
}

Looks to me as if we only use HaveSeenShip and LookingForShip. Remove the other two from the enum. See if Kotlin agrees. It was way ahead of me. Now clean up the state code a bit.

I don’t even like the look of this one now:

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        state = when {
            state == ShipMonitorState.LookingForShip && other === ship -> {
                ShipMonitorState.HaveSeenShip
            }
            else -> state
        }
        var noOneDamaged:List<IFlyer> = emptyList()
        return noOneDamaged
    }

I think it had more in it before. Now Let’s do this:

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        if (state == ShipMonitorState.LookingForShip) {
            if (other == ship)
                state = ShipMonitorState.HaveSeenShip
        }
        var noOneDamaged:List<IFlyer> = emptyList()
        return noOneDamaged
    }

We’re green. Let’s do this:

    private val noDamagedObjects = mutableListOf<IFlyer>()

    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        if (state == ShipMonitorState.LookingForShip) {
            if (other == ship)
                state = ShipMonitorState.HaveSeenShip
        }
        return noDamagedObjects
    }

I think I like that. Elsewhere in the forest:

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.LookingForShip -> {
                toBeCreated = listOf(ship)
                ShipMonitorState.HaveSeenShip
            }
            else -> ShipMonitorState.LookingForShip
        }
        return toBeCreated
    }

We can remove the else with explicit mention of the state:

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            ShipMonitorState.HaveSeenShip -> ShipMonitorState.LookingForShip
            ShipMonitorState.LookingForShip -> {
                toBeCreated = listOf(ship)
                ShipMonitorState.HaveSeenShip
            }
        }
        return toBeCreated
    }

I don’t love this but it is a bit better. Could make the toBeCreated mutable and add to it but that seems off.

I was trying to get rid of having to say ShipMonitorState all the time. It turns out that you can put the enum in a separate file, then import it, and use the short names. So now we have this:

import com.ronjeffries.ship.ShipMonitorState.*

class ShipMonitor ...
    override fun collisionDamageWith(other: IFlyer): List<IFlyer> {
        if (state == LookingForShip) {
            if (other == ship)
                state = HaveSeenShip
        }
        return noDamagedObjects
    }

Apparently you can’t import it in the file it’s defined in. Weird. Anyway I like that notation better. The other method looks like this:

    override fun update(deltaTime: Double): List<IFlyer> {
        var toBeCreated: List<IFlyer> = emptyList()
        state = when (state) {
            HaveSeenShip -> LookingForShip
            LookingForShip -> {
                toBeCreated = listOf(ship)
                HaveSeenShip
            }
        }
        return toBeCreated
    }

Yes, I like that and the cost of having the enum in a separate file is minimal, you can command B back and forth if you need to.

Commit: Move enum to own file, allow short names.

It might be well past time to wrap up, we’re almost 800 lines in. I sure would like to be able to remove finalize from the IFlyer protocol, but maybe that’s premature. Oh, wait? I think I can have concrete implementations in the interface. Let’s try one.

interface IFlyer {
    abstract val killRadius: Double
    abstract val position: Vector2
    abstract val ignoreCollisions: Boolean
    fun collisionDamageWith(other: IFlyer): List<IFlyer>
    fun collisionDamageWithOther(other: IFlyer): List<IFlyer>
    fun draw(drawer: Drawer) {}
    fun move(deltaTime: Double)
    fun finalize(): List<IFlyer>
    fun update(deltaTime: Double): List<IFlyer>
}

I added the {} to draw. Can I now remove draw from ShipMonitor? Yes! Here we go for move and finalize.

interface IFlyer {
    abstract val killRadius: Double
    abstract val position: Vector2
    abstract val ignoreCollisions: Boolean
    fun collisionDamageWith(other: IFlyer): List<IFlyer>
    fun collisionDamageWithOther(other: IFlyer): List<IFlyer>
    fun draw(drawer: Drawer) {}
    fun move(deltaTime: Double) {}
    fun finalize(): List<IFlyer> { return emptyList() }
    fun update(deltaTime: Double): List<IFlyer>
}

Finalize needs to return the default empty list of course. Now I can remove two more methods from ShipMonitor. Test, commit: modify IFlyer to make draw, move, and finalize optional.

Let’s wrap up.

Wrapping

I ran into trouble with missing commits and perhaps even a commit with tests broken. That’s not good. Other than try harder, I’m not sure what to do. I’ll try harder.

We improved a few names, referring to objects “interacting” rather than specifically colliding. There are more opportunities in pushing that notion down. We reduced the complexity of ShipMonitor substantially, including removing two states, removing one round trip interaction with the Game (the kill-me finalize to bring me back cycle). We moved the revivification of the ship to update, since finalize was never being called.

I would argue, were it not that there is no one here who disagrees with me, that we’ve shown the power of the dark side objects in the mix to handle formerly “global” ideas. My feeling is that we can do things like the score display and the various attract and insert coin modes with similar objects … those will override draw, I imagine.

So … an odd exercise but I think we’re in a better place, and with a more solid appreciation for what we can do. And a need for better names.

See you next time!



  1. Not to be confused with the ship, Monitor. You know, the Monitor and the Merrimack née Virginia? The Battle of Hampton Roads? 1862? March? First battle of ironclad ships? Never mind, it doesn’t matter here.