GitHub Decentralized Repo
GitHub Centralized Repo

Let’s move some more code over into GameCycler. We complete the Strangler!

I think we’d better start by passing the Game to the Cycler on creation. We expect to be able to remove the link when the strangling is done.

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        cancelAllOneShots()
        trans.clear()
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, ship, saucer)
    }

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        cycler.cycle(deltaTime, drawer)
    }

class GameCycler(private val game: Game, private val knownObjects: SpaceObjectCollection, numberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {
    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        game.stranglerCycle(deltaTime, drawer)
    }

I expect green. Yes. Commit: instantiate GameCycler with link to Game (temporary for strangler).

Now let’s move the next item:

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

That’ll be processInteractions:

    fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())

We’ll go in tiny steps:

class Game
    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

class GameCycler
    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        game.processInteractions()
        game.stranglerCycle(deltaTime, drawer)
    }

Should be green. Yep. Commit: begin moving processInteractions. still calls back.

Now move the method. Grr, there’s a test that calls it. Move it anyway, see what we can do to the test:

class GameCycler
    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        game.stranglerCycle(deltaTime, drawer)
    }

    private fun processInteractions() = 
    	knownObjects.applyChanges(game.changesDueToInteractions())

Test will break.

    @Test
    fun `colliding ship and asteroid splits asteroid, loses ship`() {
        val game = Game()
        val asteroid = Asteroid(Vector2(1000.0, 1000.0))
        val ship = Ship(
            position = Vector2(1000.0, 1000.0)
        )
        game.knownObjects.add(asteroid)
        game.knownObjects.add(ship)
        assertThat(game.knownObjects.spaceObjects().size).isEqualTo(2)
        assertThat(ship).isIn(game.knownObjects.spaceObjects())
        game.processInteractions()
        assertThat(ship).isNotIn(game.knownObjects.spaceObjects())
        assertThat(game.knownObjects.asteroidCount()).isEqualTo(2)
    }

Let’s break out the creation of GameCycler that we use in createInitialObjects:

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        cancelAllOneShots()
        trans.clear()
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, ship, saucer)
    }

Extract method:

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        cancelAllOneShots()
        trans.clear()
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = makeCycler()
    }

    fun makeCycler() = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, ship, saucer)

Now I think we can change the test:

    @Test
    fun `colliding ship and asteroid splits asteroid, loses ship`() {
        val game = Game()
        val asteroid = Asteroid(Vector2(1000.0, 1000.0))
        val ship = Ship(
            position = Vector2(1000.0, 1000.0)
        )
        game.ship = ship // crock
        game.knownObjects.add(asteroid)
        game.knownObjects.add(ship)
        assertThat(game.knownObjects.spaceObjects().size).isEqualTo(2)
        assertThat(ship).isIn(game.knownObjects.spaceObjects())
        game.makeCycler().processInteractions()
        assertThat(ship).isNotIn(game.knownObjects.spaceObjects())
        assertThat(game.knownObjects.asteroidCount()).isEqualTo(2)
    }

I had to tuck the ship into game to get the test to run but it is green. Commit: move processInteractions, calling back to changesDueToInteractions.

Now for that method:

    fun processInteractions() = knownObjects.applyChanges(changesDueToInteractions())

    fun changesDueToInteractions(): Transaction {
        val trans = Transaction()
        knownObjects.pairsToCheck().forEach {
            it.first.interactWith(it.second, trans)
            it.second.interactWith(it.first, trans)
        }
        return trans
    }

Again there will be test problems. I’ll try that gameCycler() trick again. I also had to patch in the ship in a couple of places. These tests are too tightly tied to the exact setup. I’ll make a Jira note. But we are green, legitimately. The tests are clunky but actually testing things.

Commit: move changesDueToInteractions.

What’s next? Asteroid tally:

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

I’ll just cut and paste that right over.

    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        game.stranglerCycle(deltaTime, drawer)
    }

Should be green. Yes. Commit: move AsteroidTally.

Next will be the waves, which will require us to move over the OneShots. Should be no biggie, but let’s pause and reflect.

Reflection

The “strangler” pattern consists of creating a place for the code to go and then moving it over bit by bit, slowly strangling out the implementation from the donor class. As we’ve seen, it can typically be done in incredibly small steps, which could be spread over a long period if need be.

We have a few tests that are overly specific, testing methods from the middle of Game, like tick or changesDueToInteractions, that are relying on Game to be properly set up, at least when we go to create the Cycler. I think we’ll find that we can fix that soon.

I think I’ll try slightly larger steps for these next bits. We’ll see whether that bites me or not.

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        createNewWaveIfNeeded()
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }


    private fun createNewWaveIfNeeded() {
        if ( U.AsteroidTally == 0 ) {
            knownObjects.performWithTransaction { waveOneShot.execute(it) }
        }
    }

I’ll move over the method and the oneShot.

I had to go pretty deep, but IDEA and Kotlin led the way by coloring things red:

class GameCycler(private val game: Game, private val knownObjects: SpaceObjectCollection, val initialNumberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {

    var numberOfAsteroidsToCreate = initialNumberOfAsteroidsToCreate
    private val waveOneShot = OneShot(4.0) { makeWave(it) }

    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        game.stranglerCycle(deltaTime, drawer)
    }

    private fun createNewWaveIfNeeded() {
        if ( U.AsteroidTally == 0 ) {
            knownObjects.performWithTransaction { waveOneShot.execute(it) }
        }
    }

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

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

I probably could have made the number in the constructor var, but that seems wrong. The compiler thinks this will be OK. Again I have to futz with some tests. At least one turned out better:

    fun `how many asteroids per wave`() {
        val game = Game()
        game.insertQuarter(Controls())
        val cycler = game.makeCycler()
        assertThat(cycler.howMany()).isEqualTo(4)
        assertThat(cycler.howMany()).isEqualTo(6)
        assertThat(cycler.howMany()).isEqualTo(8)
        assertThat(cycler.howMany()).isEqualTo(10)
        assertThat(cycler.howMany()).isEqualTo(11)
        assertThat(cycler.howMany()).isEqualTo(11)
    }

Well, at least not worse.

We are green. I really want to try the game now. Works well. Commit: move createNewWaveIfNeeded complete.

I noticed, however, that I had to change this in Game:

    // all OneShot instances go here:
    private val allOneShots = listOf(saucerOneShot, shipOneShot)

That’s used here:

    private fun cancelAllOneShots() {
        val ignored = Transaction()
        for (oneShot in allOneShots) {
            oneShot.cancel(ignored)
        }
    }

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        cancelAllOneShots()
        trans.clear()
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = makeCycler()
    }

We’d better create the cycler sooner and tell it to cancel also, then we’ll move them over and all will be good. Right now, there is a possible bug if we failed to cancel the wave OneShot.

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = makeCycler()
        cycler.cancelAllOneShots()
        cancelAllOneShots()
        trans.clear()
    }

That should get us close.

class GameCycler(private val game: Game, private val knownObjects: SpaceObjectCollection, val initialNumberOfAsteroidsToCreate: Int, ship: Ship, saucer: Saucer) {

    var numberOfAsteroidsToCreate = initialNumberOfAsteroidsToCreate
    private val waveOneShot = OneShot(4.0) { makeWave(it) }
    private val allOneShots = listOf(waveOneShot)

    fun cycle(deltaTime: Double, drawer: Drawer?) {
        tick(deltaTime)
        beforeInteractions()
        processInteractions()
        U.AsteroidTally = knownObjects.asteroidCount()
        createNewWaveIfNeeded()
        game.stranglerCycle(deltaTime, drawer)
    }

    fun cancelAllOneShots() {
        val ignored = Transaction()
        for (oneShot in allOneShots) {
            oneShot.cancel(ignored)
        }
    }

Lets see if that flies. Yes. Commit: cancelOneShots now done in both Game and Cycler.

Now to move whatever’s next:

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        createSaucerIfNeeded()
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

Again I’ll just paste it over and follow my nose and the red words.

    private fun createSaucerIfNeeded() {
        if ( knownObjects.saucerIsMissing() ) {
            knownObjects.performWithTransaction { saucerOneShot.execute(it) }
        }
    }

This demands the oneShot:

class GameCycler(
    private val game: Game,
    private val knownObjects: SpaceObjectCollection,
    val initialNumberOfAsteroidsToCreate: Int,
    val ship: Ship,
    val saucer: Saucer
) {
    var numberOfAsteroidsToCreate = initialNumberOfAsteroidsToCreate
    
    private val waveOneShot = OneShot(4.0) { makeWave(it) }
    private val saucerOneShot = OneShot( 7.0) { startSaucer(it) }
    private val allOneShots = listOf(waveOneShot, saucerOneShot)

Let’s see what the tests think. They think green. Commit: move createSaucerIfNeeded.

Let’s see if we can move the last creating one and then see where we stand.

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        createShipIfNeeded()
        drawer?.let { draw(drawer) }
    }

Same process.

    private fun createShipIfNeeded() {
        if ( knownObjects.shipIsMissing() ) {
            knownObjects.performWithTransaction { shipOneShot.execute(it) }
        }
    }

Now the OneShot:

    fun canShipEmerge(): Boolean {
        if (knownObjects.saucerIsPresent()) return false
        if (knownObjects.missiles().isNotEmpty()) return false
        for ( asteroid in knownObjects.asteroids() ) {
            val distance = asteroid.position.distanceTo(U.CENTER_OF_UNIVERSE)
            if ( distance < U.SAFE_SHIP_DISTANCE ) return false
        }
        return true
    }

And then …

    private fun startShipAtHome(trans: Transaction) {
        ship.setToHome()
        trans.add(ship)
    }

Missing scoreKeeper. Can get it from knownObjects I think.

    private val shipOneShot = OneShot(U.SHIP_MAKER_DELAY, { canShipEmerge() }) {
        if ( knownObjects.scoreKeeper.takeShip() ) {
            startShipAtHome(it)
        }
    }

Test. A ton of tests of canShipEmerge.

I have to do my makeCycler() trick and I have to inject ship because makeCycler needs it at present. We are green. Commit: createShipIfNeeded moved over.

Now there’s just drawing to move:

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
        drawer?.let { draw(drawer) }
    }

Cut and paste. IDEA demands:

    private fun draw(drawer: Drawer) {
        knownObjects.forEachInteracting { drawer.isolated { it.draw(drawer) } }
        knownObjects.scoreKeeper.draw(drawer)
    }

Green. Test the game, we just moved drawing. Works. I am not surprised. Commit: move drawing to GameCycler. All code is now moved.

I’ll do a bare minimum of clean up today and then look again with fresh eyes tomorrow.

Remove this:

    fun stranglerCycle(deltaTime: Double, drawer: Drawer?) {
    }

Rip out the unneeded cancellation of OneShots:

    private fun cancelAllOneShots() {
        val ignored = Transaction()
        for (oneShot in allOneShots) {
            oneShot.cancel(ignored)
        }
    }

    // all OneShot instances go here:
    private val allOneShots = emptyList<OneShot>()

Game is now down to a very reasonable size, and we’re not done yet:

class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
    private var lastTime = 0.0
    private var numberOfAsteroidsToCreate = 0
    private var saucer = Saucer()
    lateinit var ship: Ship
    private var cycler: GameCycler = GameCycler(this, knownObjects, 0, Ship(U.CENTER_OF_UNIVERSE), saucer)
    private var scoreKeeper: ScoreKeeper = ScoreKeeper(-1)

    fun createInitialContents(controls: Controls) {
        initializeGame(controls, -1)
    }

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

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

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        scoreKeeper = ScoreKeeper(shipCount)
        knownObjects.scoreKeeper = scoreKeeper
        val shipPosition = U.CENTER_OF_UNIVERSE
        ship = Ship(shipPosition, controls)
        saucer = Saucer()
        cycler = makeCycler()
        cycler.cancelAllOneShots()
        trans.clear()
    }

    fun makeCycler() = GameCycler(this, knownObjects, numberOfAsteroidsToCreate, ship, saucer)

    fun cycle(elapsedSeconds: Double, drawer: Drawer? = null) {
        val deltaTime = elapsedSeconds - lastTime
        lastTime = elapsedSeconds
        cycler.cycle(deltaTime, drawer)
    }
}

Over in GameCycler things are nearly OK. I’ll spare you the printout: we’ll look in a little more detail tomorrow but basically all we’ve done is copy things over.

Summary

The transition from Game to GameCycler is complete except for polishing. It has taken just a couple of hours so far and I expect a couple more or less should have the separation done quite nicely.

The whole thing was done in a series of very small steps, with a total of 15 individual commits, after each one of which the game was running correctly. It’s amazing what we can accomplish without breaking any code or breaking a sweat, with small enough steps.

I’m starting to wonder whether we would benefit from breaking GameCycler up a bit as well. We’ll think about that next time. Please join me then for the next thrilling episode of … The Strangler!