GitHub Decentralized Repo
GitHub Centralized Repo

I have a little time. Let’s see if we can take a few small steps toward wave-making. Should be possible, I had it working yesterday.

We’re on number four from this list:

  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.

4. In Game, implement a makeWave function

Let’s write a little test for the actual wave-making in Game. Here’s what it looks like now, in WaveMaker:

    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()))
        }
    }

That’s the code that actually runs when the DeferredAction finally happens. So we should be able to test that directly on Game.

    @Test
    fun `game can create asteroids directly`() {
        val game = Game()
        val transForFour = Transaction()
        game.makeWave(transForFour)
        assertThat(transForFour.adds.size).isEqualTo(4)
        val transForSix = Transaction()
        game.makeWave(transForSix)
        assertThat(transForSix.adds.size).isEqualTo(6)
    }

This demands the makeWave function on Game. I wonder if I should call the createInitialContents function or whatever it is. Let’s find out.

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

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

I need the member variable.

class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
    private var lastTime = 0.0
    private var numberOfAsteroidsToCreate = 0

I chose zero because now the test will fail, and I’ll need to do the contents creation.

It does fail, seeing zero, not four. I change the test:

    @Test
    fun `game can create asteroids directly`() {
        val game = Game()
        game.createInitialContents(Controls())
        val transForFour = Transaction()
        game.makeWave(transForFour)
        assertThat(transForFour.adds.size).isEqualTo(4)
        val transForSix = Transaction()
        game.makeWave(transForSix)
        assertThat(transForSix.adds.size).isEqualTo(6)
    }

I implement:

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

This should run green. (And I think we should fix the magic number 4.) Test is green. Do we have a constant for that 4 yet? We do not. Create one. Change the code to use it:

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

All tests are green. Commit: Game now includes unused code to create waves of asteroids.

Now let’s see about the next step:

5. Make the waves in Game, not WaveMaker.

I think we have tests that will fail if I remove the WaveMaker:

    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)
    }

Yes, those nice new ones about creating asteroids break. Perfect. Let’s just push on. Where shall we put the check? I think the end of cycle is right:

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

Probably right before the draw:

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

And then …

    private fun createNewWaveIfNeeded() {
        if ( knownObjects.asteroidCount() == 0 ) {
            val trans = Transaction()
            makeWave(trans)
            knownObjects.applyChanges(trans)
        }
    }

This will create the wave with no delay. Seems not unreasonable, let’s try it. My two tests fail, but I can’t resist trying the game. I get four asteroids immediately, and if I kill them all, I get six new ones, again immediately. Perfect.

Now to do the time delay. We can copy the OneShot from WaveMaker:

class Game(val knownObjects:SpaceObjectCollection = SpaceObjectCollection()) {
    private var lastTime = 0.0
    private var numberOfAsteroidsToCreate = 0
    private val oneShot = OneShot(4.0) { makeWave(it) }

And we use it just as in WaveMaker, I think:

    private fun createNewWaveIfNeeded() {
        if ( knownObjects.asteroidCount() == 0 ) {
            val trans = Transaction()
            oneShot.execute(trans)
            knownObjects.applyChanges(trans)
        }
    }

I think one of my tests should run and one not. Let’s see.

I’m right but I’m wrong. One of my wave making tests still fails:

    fun `game creates asteroids even when quarter comes rapidly`() {

I expected exactly that, because I’m not cancelling the oneShot. Remember that red note in Jira?

But two others fail, and I’m supposing that’s because they no longer apply.

    @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)
    }

Says expected 1 but was two. I don’t know which 1 it is.

        assertThat(mix.size).describedAs("mix size").isEqualTo(1)
        assertThat(mix.deferredActions.size).describedAs("deferred size").isEqualTo(1)

It’s deferred size that’s 2. I wonder what’s in there and suppose it must be my wave maker deferred object. Calling cycle will add it now, unconditionally. I’ll patch in a print to be sure.

        for ( da in mix.deferredActions) {
            println("deferred ${(da as DeferredAction).delay}")
        }

I expect to see one with 4 as delay, one with 7. That is what I see. The new correct answer is 2. Maybe I can make the test a bit stronger.

        assertThat(mix.deferredActions.size).describedAs("deferred size").isEqualTo(2)
        val found = mix.deferredActions.find { (it as DeferredAction).delay == 7.0 }
        assertThat(found).isNotNull

I expect this to run green. It does. What else broke? It’s one of the WaveMaker tests:

    @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())
    }

I bet if I’d just remove the creation of the WaveMaker, this test might run. (How do you win a bet that includes the word “might”?) Anyway, no, but since we’re removing WaveMaker, these tests can go. I comment out the broken one in anticipation of deleting the whole batch. Now we just have the one expected test failing, the one that inserts a quarter before the OneShot has a chance to fire:

    @Test
    fun `game creates asteroids even when quarter comes rapidly`() {
        val game = Game()
        val controls = Controls()
        game.createInitialContents(controls)
        assertThat(game.knownObjects.asteroidCount()).isEqualTo(0)
        game.cycle(0.2)
        game.cycle(0.3)
        game.insertQuarter(controls)
        game.cycle(0.2)
        game.cycle(0.3)
        assertThat(game.knownObjects.asteroidCount()).isEqualTo(0)
        game.cycle(4.2)
        assertThat(game.knownObjects.asteroidCount()).isEqualTo(4)
    }

That requires this fix:

    private fun createInitialObjects(
        trans: Transaction,
        shipCount: Int,
        controls: Controls
    ) {
        oneShot.cancel(Transaction()) // <===
        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)
    }

Green. Remove the commented line. Still green. Commit: Asteroid waves created by Game, WaveMaker no longer used.

Now to safe delete the wavemaker tests and class. Safe delete finds one usage: I think I just did this this morning:

    @Test
    fun `clear clears all sub-collections`() {
        val s = SpaceObjectCollection()
        s.add(Missile(Ship(U.CENTER_OF_UNIVERSE)))
        s.add(Asteroid(Point.ZERO))
        s.add(WaveMaker())
        val deferredAction = DeferredAction(3.0, Transaction()) {}
        s.add(deferredAction)
        s.clear()
        for ( coll in s.allCollections()) {
            assertThat(coll).isEmpty()
        }
    }

Change that to a Score:

    @Test
    fun `clear clears all sub-collections`() {
        val s = SpaceObjectCollection()
        s.add(Missile(Ship(U.CENTER_OF_UNIVERSE)))
        s.add(Asteroid(Point.ZERO))
        s.add(Score(666))
        val deferredAction = DeferredAction(3.0, Transaction()) {}
        s.add(deferredAction)
        s.clear()
        for ( coll in s.allCollections()) {
            assertThat(coll).isEmpty()
        }
    }

Test. Still green. Safe delete WaveMaker. Still green. Commit: Remove unused WaveMaker class and tests.

So that’s nice. Let’s sum up.

Summary

One thing worth noting is that this went much more smoothly than it did when I tried it yesterday. I spent lots of time debugging yesterday. Of course that investment paid off with at least one key bit of info, the need to cancel the OneShot when starting a new game, but there was a lot more fumbling around.

One way that a Spike pays off is by building familiarity with the problem and solution. When we are wise enough to throw it away, it’s always much faster to do it the second time than the first, and we generally do it a bit better, both in the code itself and just in the style and grace with which we do it.

Taking the larger view, we just removed over 80 lines of tests and code (2/3 tests), while adding about a dozen lines of code and some much more robust tests. A net gain in lines, but mostly in tests, which were definitely needed.

I think it’s fair to say that Game is getting a bit busy, but it’s not too awful yet. Well … maybe it is pretty awful, it’s over 130 lines and around 20 functions. Getting suspiciously large but still manageable. Our purpose here is to move everything more or less into a blob and then refactor, but we could certainly take a look now and see what we see.

There’s game creation, game cycling, and now the wave making rigmarole. We could have some factory stuff for the creation and a helper of some kind for wave making. We’ll wait and see.

We’re making good progress centralizing and the game still works. Such fun! See you next time!