GitHub Decentralized Repo
GitHub Centralized Repo

Now on a somewhat better build, I’ll begin by redoing yesterday’s changes, and then continue with the centralizing work. Bison?

Yesterday afternoon, the very kind and wise GeePaw Hill walked with me through the valley of desolation that is the creation of a decent development project in IDEA. We did at least these things:

  • Added the missing package line in the decentralized repo and committed that;
  • Removed some GitHub repos that were attempts at the centralized version;
  • Removed the folders on my machine that represented those attempts;
  • Created a new repo on GitHub for the project;
  • Cloned it to my machine;
  • Copied all the non dotted files plus .gitignore from the decentralized version;
  • Set up a test and a run configuration in IDEA;

We also made some magical changes to files like settings.gradle.kts, where we changed the rootProject.name to “asteroids-centralized”. Even just thinking about and writing about it makes me nervous. It feels to me like walking barefoot in a dark room with bad stuff on the floor. We’ll say no more about it: the repo works on my Mac and Hill’s Windows machine. If you try it and can offer improvements, email ronjeffries at acm dot org.

This morning, I propose to redo the changes to game startup. At this point I’ve not referred back to the article, but of course I have a 24 hour old recollection. Let’s just do it again. I think we’ll try to do it better.

There are two places where we initialize the game:

    fun createInitialContents(controls: Controls) {
        add(Quarter(controls,0))
    }

    fun insertQuarter(controls: Controls) {
        add(Quarter(controls))
    }

The first of these sets up the initial screen, with no ship. That’s what the zero says: make no ships available. That sends the game directly to game over. The second function is called when you type “q”, to “insert a quarter” to play the game. That one defaults the shipCount parameter in Quarter, which looks like this:

class Quarter(
	private val controls: Controls, 
	private val shipCount: Int = 4): ISpaceObject, InteractingSpaceObject {
    override fun update(deltaTime: Double, trans: Transaction) {
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        trans.add(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)
    }

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

Note that we default to a shipCount of 4. That should really be a constant in U, and probably there should be no default here. I usually use those to make testing more convenient and it’s probably not my best habit.

In U:

    const val SHIPS_PER_QUARTER = 4

In the spirit of least possible change:

class Quarter(
    private val controls: Controls,
    private val shipCount: Int = U.SHIPS_PER_QUARTER
): ISpaceObject, InteractingSpaceObject {

Test. Green, of course. Commit: Add SHIPS_PER_QUARTER constant and use in Quarter object.

Now let’s remove the default, to require everyone to give us the parameter. IDEA’s Command-B will help me find those.

class Game ...
    fun insertQuarter(controls: Controls) {
        add(Quarter(controls, U.SHIPS_PER_QUARTER))
    }

class QuarterTest {
    val controls = Controls()
    val quarter = Quarter(controls, U.SHIPS_PER_QUARTER)
    val trans = Transaction()

That’s all there was to change. Test. Green. Commit: Quarter’s shipCount parameter no longer defaults.

Maybe I should have told you why I did that, but it wasn’t entirely clear in my mind. Look what we have now, in Game:

    fun createInitialContents(controls: Controls) {
        add(Quarter(controls,0))
    }

    fun insertQuarter(controls: Controls) {
        add(Quarter(controls, U.SHIPS_PER_QUARTER))
    }

This, folx of all kinds, is duplication. Furthermore, we may vaguely remember from yesterday that we’re going to move the code from what quarter does over here. The change to remove this duplication will seem silly but I think that in a moment that will not be the case.

    fun createInitialContents(controls: Controls) {
        initializeGame(controls, 0)
    }

    fun insertQuarter(controls: Controls) {
        initializeGame(controls, U.SHIPS_PER_QUARTER)
    }
    
    private fun initializeGame(controls: Controls, shipCount: Int) {
        add(Quarter(controls, shipCount))
    }

Now there is only one place in the Game that adds a quarter. There used to be two. One is better, though it’s not glaringly obvious. Test. Green. Commit: refactor to have only one add(Quarter).

Now instead of adding the Quarter, let’s review what it does. As soon as the Quarter is added to the mix, it’ll get a call to update, which does this:

override fun update(deltaTime: Double, trans: Transaction) {
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        trans.add(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)
    }

Given a transaction, it clears, then adds all that stuff. Let's copy that code into our new `initializeGame` function. We'll have to create a transaction and apply it.s>

No. Let’s go smaller. We’ll create a transaction and a Quarter, and just call the update.

    private fun initializeGame(controls: Controls, shipCount: Int) {
        val trans = Transaction()
        val quarter = Quarter(controls, shipCount)
        quarter.update(0.0, trans)
        knownObjects.applyChanges(trans)
    }

Test. Green. Game works. Commit: Game creates and calls Quarter, does not add it to the mix.

Now then. We could retain the Quarter object as a kind of helper, or we could fold its code into Game and remove the class. I think our plan is to push everything into Game so as to provide maximal opportunities for it to become weird and incohesive, so that we can refactor it differently. So let’s see about inlining that call to update. Maybe IDEA can do it.

Yes! Inline Function does this:

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

Outstanding! Well done IDEA! However, I think I chose the wrong option, because IDEA removed the function from Quarter. I should have left it. Do again. Undo undoes it. I choose “Inline this reference and leave the function”. We should be solid.

Test. Oops, the code above won’t quite compile, it’s referencing the quarter. We don’t really need that, so we get this:

    private fun initializeGame(controls: Controls, shipCount: Int) {
        val trans = Transaction()
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        trans.add(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)
        knownObjects.applyChanges(trans)
    }

Test. Green. Game works. Commit: Quarter update function moved to Game initializeGame.

Now the Quarter class should be unused except for its tests. That is the case. Safe delete. Can’t. Have to delete the test first. OK. Safe Delete QuarterTest, then Safe Delete Quarter. Done.

Test. Green. Game works. Commit: Quarter object and tests removed as no longer used.

Time to reflect.

Reflection

Yesterday I did this differently, and, partly because I knew I had a build that didn’t push well, I stopped with Quarter still in existence and being used, and with two methods using it:

From Yesterday:

    fun insertQuarter(controls: Controls) {
        val quarter = Quarter(controls)
        val trans = Transaction()
        quarter.update(0.0, trans)
        knownObjects.applyChanges(trans)
    }

    fun createInitialContents(controls: Controls) {
        val quarter = Quarter(controls, 0)
        val trans = Transaction()
        quarter.update(0.0, trans)
        knownObjects.applyChanges(trans)
    }

Today, we moved the functionality of Quarter into a single method in Game and called it our two different ways:

Today:

    fun createInitialContents(controls: Controls) {
        initializeGame(controls, 0)
    }

    fun insertQuarter(controls: Controls) {
        initializeGame(controls, U.SHIPS_PER_QUARTER)
    }

    private fun initializeGame(controls: Controls, shipCount: Int) {
        val trans = Transaction()
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        trans.add(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)
        knownObjects.applyChanges(trans)
    }

Looking at that last method, it seems to me that it has at least two, maybe three ideas in it:

  1. Make a transaction;
  2. Stuff initial objects into the transaction;
  3. Apply the transaction/

Ideally that would be two ideas, where we’d create a transaction that would automatically apply. Let’s work toward that with an Extract Method:

    private fun initializeGame(controls: Controls, shipCount: Int) {
        val trans = Transaction()
        createInitialObjects(trans, shipCount, controls)
        knownObjects.applyChanges(trans)
    }

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        trans.clear()
        val scoreKeeper = ScoreKeeper(shipCount)
        trans.add(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)
    }

Can we do better? I think we can. I know there is a pattern for this that people use, such as for opening a file, using it, and closing it, but I don’t remember what it’s called. My friends will remind me. For now, I think we can put a method on SpaceObjectCollection to do the job.

class SpaceObjectCollection
    fun performWithTransaction(action: (Transaction) -> Unit ) {
        val trans = Transaction()
        action(trans)
        applyChanges(trans)
    }

And use that:

    private fun initializeGame(controls: Controls, shipCount: Int) {
        knownObjects.performWithTransaction { trans ->
            createInitialObjects(trans,shipCount, controls)
        }
    }

I should have written a test for that perform thing but this will test it. Game works a treat. Green as grass. Commit: Use new performWithTransaction in SpaceObjectCollection in initializeGame.

We were reflecting. We noticed that our code, while nice, wasn’t as nice as it could be. Now, it is a bit nicer, and we have a very convenient function on SpaceObjectCollection that we can use in other situations where we create a transaction, run some code against it, and apply it.

Let’s add a test, though.

    @Test
    fun `performWithTransaction works`() {
        val s = SpaceObjectCollection()
        val saucer = Saucer()
        val ship = Ship(Point.ZERO)
        s.performWithTransaction { trans-> trans.add(saucer); trans.add(ship) }
        assertThat(s.attackers.size).isEqualTo(2)
    }

Test. Green of course. Commit: Added test for performWithTransaction.

So, still reflecting. what have we got here? I think we’ve got enough to sum up and call it a morning.

Summary / Reflection

We’ve redone the conversion of Quarter from a decentralized special object down to a single function in Game, called in two different ways, one for the initial attract screen and one for the actual game play. We promoted a magic number 4 up to its meaning, SHIPS_PER_QUARTER. We have rather clean code for the change, removed two entire classes, and added a very nice new capability to SpaceObjectCollection, which lets us create a transaction, fill it, and apply it, with a very nice notation, performWithTransaction. We even added a test.

This was done with eight commits in less than an hour.

Years ago, when Extreme Programming was new and the bison roamed the West, we had a saying that the thing that took hours yesterday could be redone better today in minutes. Probably the saying was more pithy in the original English, but I only remember the idea, not the words. But it’s true. Yesterday I learned how to do it. Today I was able to do it quickly, with few if any missteps, and better.

Probably if I were to do it again next time, I’d do it even better, but I think instead I’ll try to apply what I’ve learned to another of the special objects. Let’s look back at what we have, and forward to what we might have.

Looking back …

We have moved the functionality of one special object, Quarter, into Game. This removed two classes, and added a couple of functions to Game. Game is frankly a bit less cohesive than it was. It used to know someone to ask to get something done. Now it does it itself. Until this moment, all the Game knew about how to set up the game was to put in a Quarter. Now it actually knows what objects start up the game.

That is not entirely a good thing. Game is now a bit less cohesive. It is more complicated. But that’s OK, in this case, because what we’re doing is intentional. We’re trying to centralize game control, to set up as vivid a comparison as we can between the centralized way and the decentralized. In doing that, it is our specific intention to push things into the Game blob until we see how it should be refactored. We don’t intend to leave it bad, but we do intend to let it get bad enough to tell us what it needs.

Looking forward …

I think we’ll move all the special objects (the objects other than Missile, Ship, Saucer, and Asteroid) into Game much as we did with Quarter. We’ll go one at a time and we’ll try to keep the Game code clean. At some point we might start refactoring to improve it, but I think our primary focus will be on getting the special objects out of the system and into Game.

Then we’ll have a much simplified object hierarchy, with just the colliding objects in it. And we’ll look at our sketched interact function, that mimics the original Asteroids, that is: colliding Missiles, Ship and Saucer against Ship, Saucer, and Asteroids. That code will subsume all the interaction code of those objects, which might leave those objects with nothing to do but draw themselves.

Caveat …

Of course, this is speculation about what will happen. I would meta-speculate that things will go much as I’ve described above, but not exactly like that, and that we’ll discover some interesting issues along the way. We’ll find out. That is in fact our purpose in this sub-series of my Kotlin Asteroids articles. In the parlance of the day:

Join me next time, as we “F[ool] Around and Find Out.”