GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

I think it’s time to start on the saucer. What do you think?

Story

The saucer appears every seven seconds and runs for seven seconds. It starts at a random y coordinate, at the edge of the screen. It alternates traveling left to right and right to left. It changes direction every so often: I’ll have to look up the interval. It can fire only two missiles at a time, and I think it fires once per second. I’ll have to look that up. It has some rule about sometimes firing accurately and sometimes not. I’ll have to look up the odds. None those values should change the basic logic.

Plan

I figure it’ll work much the same as the Ship. If there is no saucer, start a countdown and activate it. Keep a direction value, plus or minus, probably plus or minus its basic speed. Keep three running timers, one for firing, one for stopping, and one for turning. Probably the stopping one and starring one can be the same.

The saucer’s point list is available from the other versions, and drawing it should be the same as everything else.

Tests

I think I’d be wise to start with tests and let them pull out the functionality we need.

Let’s start with something simple, starting it and letting it motor across.

Get To It

    @Test
    fun `start saucer after seven seconds`() {
        createGame(U.MissileCount, U.AsteroidCount)
        startGame(U.ScreenWidth, U.ScreenHeight)
        assertThat(Saucer.active).isEqualTo(false)
    }

That’s more than enough to drive out a Saucer.

fun createGame(missileCount: Int, asteroidCount: Int) {
    Score = 0
    val objects = mutableListOf<SpaceObject>()
    for (i in 1..missileCount) objects.add(newMissile())
    Ship = newShip()
    objects.add(Ship)
    for (i in 1..asteroidCount) objects.add(newAsteroid())
    SpaceObjects = objects.toTypedArray()
}

We’ll add it here. I need a new global of course.

var Score: Int = 0
lateinit var SpaceObjects: Array<SpaceObject>
lateinit var Ship: SpaceObject
lateinit var Saucer: SpaceObject

fun createGame(missileCount: Int, asteroidCount: Int) {
    Score = 0
    val objects = mutableListOf<SpaceObject>()
    for (i in 1..missileCount) objects.add(newMissile())
    Ship = newShip()
    objects.add(Ship)
    Saucer = newSaucer()
    objects.add(Saucer)
    for (i in 1..asteroidCount) objects.add(newAsteroid())
    SpaceObjects = objects.toTypedArray()
}

Now …

private fun newSaucer(): SpaceObject = SpaceObject(SpaceObjectType.SAUCER, 0.0, 0.0, 0.0, 0.0, 0.0, false)

Nasty but they all are and we only do them once. Now we need the enum, which will in turn drive out the points.

private val saucerPoints = listOf(
    Vector2(-2.0, 1.0), Vector2(2.0, 1.0), Vector2(5.0, -1.0),
    Vector2(-5.0, -1.0), Vector2(-2.0, -3.0), Vector2(2.0, -3.0),
    Vector2(5.0, -1.0), Vector2(2.0, 1.0), Vector2(1.0, 3.0),
    Vector2(-1.0, 3.0), Vector2(-2.0, 1.0), Vector2(-5.0, -1.0),
    Vector2(-2.0, 1.0)
)

enum class SpaceObjectType(val points: List<Vector2>) {
    ASTEROID(asteroidPoints),
    SHIP(shipPoints),
    SAUCER(saucerPoints),
    MISSILE(missilePoints)
}

I think this test might run now. If not, it’ll tell me what to do. Yes. Commit: initial creation of saucer object, points, enum, etc.

Now enhance the test to demand the little bugger into existence.

    @Test
    fun `start saucer after seven seconds`() {
        createGame(U.MissileCount, U.AsteroidCount)
        startGame(U.ScreenWidth, U.ScreenHeight)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(0.1)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(true)
    }

Now we’d best implement that check. I’ll do the minimum:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    if ( ! Saucer.active ) {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay) {
            Saucer.active = true
            saucerGoneFor = 0.0
        }
    }
}

I expect the test to run. It does. I can’t resist running the program to see if the saucer displays. I expect it to be too small. I see a corner of it in the top left corner. Let’s enhance the init.

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    if ( ! Saucer.active ) {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
            saucerGoneFor = 0.0
        }
    }
}

That demands the speed. I have no idea but the other version has, 150.0.

One minor detail, it’s upside down.

saucer upside down

We can fix the data, or fix the code. I change the sign of all the y coordinates in the saucer and it is just fine. And the scale looks OK, but I think the scale between this version and the others is different for the ship and saucer both. Anyway it looks good enough for now.

Let’s make the saucer go away again. I’ll enhance the test further:

    @Test
    fun `start saucer after seven seconds`() {
        createGame(U.MissileCount, U.AsteroidCount)
        startGame(U.ScreenWidth, U.ScreenHeight)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(0.1)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(true)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(false)
    }

That’ll fail on the last assert. It does. Fix:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    if ( ! Saucer.active ) {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
            saucerGoneFor = 0.0
        }
    } else {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay ) {
            Saucer.active = false
        }
    }
}

I expect that to run green. It does. I want to watch it in the game.

Bug! I forgot to zero the time. Enhance the test:

    @Test
    fun `start saucer after seven seconds`() {
        createGame(U.MissileCount, U.AsteroidCount)
        startGame(U.ScreenWidth, U.ScreenHeight)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(0.1)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(true)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded( 0.1)
        assertThat(Saucer.active).describedAs("stays gone").isEqualTo(false) 
    }

Should fail. Does. Fix:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    if ( ! Saucer.active ) {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay) {
            saucerGoneFor = 0.0
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
        }
    } else {
        saucerGoneFor += deltaTime
        if (saucerGoneFor > U.SaucerDelay ) {
            saucerGoneFor = 0.0
            Saucer.active = false
        }
    }
}

Should be green. Game shows no saucer, then saucer, then no saucer, then saucer again. Always left to right, of course. Let’s refactor that code a bit:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) { saucerGoneFor = 0.0
    if ( ! Saucer.active ) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
        }
    } else {
        Saucer.active = false
    }
}

I moved things around, had to do it by hand. Should still pass. Doesn’t. Undo it. Do this much:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if ( ! Saucer.active ) {
        if (saucerGoneFor > U.SaucerDelay) {
            saucerGoneFor = 0.0
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
        }
    } else {
        if (saucerGoneFor > U.SaucerDelay ) {
            saucerGoneFor = 0.0
            Saucer.active = false
        }
    }
}

OK how can we do this without error? Let’s insert a redundant check:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        if (!Saucer.active) {
            if (saucerGoneFor > U.SaucerDelay) {
                saucerGoneFor = 0.0
                Saucer.active = true
                Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
                Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
            }
        } else {
            if (saucerGoneFor > U.SaucerDelay) {
                saucerGoneFor = 0.0
                Saucer.active = false
            }
        }
    }
}

Test. Green. Remove redundant check:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        if (!Saucer.active) {
            if (saucerGoneFor > U.SaucerDelay) {
                saucerGoneFor = 0.0
                Saucer.active = true
                Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
                Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
            }
        } else {
            saucerGoneFor = 0.0
            Saucer.active = false
        }
    }
}

Test. Green. Remove the other.

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        if (!Saucer.active) {
            saucerGoneFor = 0.0
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
        } else {
            saucerGoneFor = 0.0
            Saucer.active = false
        }
    }
}

Test. Green. One more little change:

private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        saucerGoneFor = 0.0
        if (!Saucer.active) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(U.SaucerSpeed, 0.0)
        } else {
            Saucer.active = false
        }
    }
}

Test. Still green. I’m glad I wrote that nice test. Commit: ship starts after seven seconds, stops, starts, etc.

I should have committed when it first worked, before undertaking the refactoring. Anyway, we have a scary saucer flying. Should we make it change direction before we close? Let’s do.

I’ll write another test.

    @Test
    fun `saucer switches direction`() {
        createGame(U.MissileCount, U.AsteroidCount)
        startGame(U.ScreenWidth, U.ScreenHeight)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(true)
        assertThat(Saucer.dx).isGreaterThan(0.0)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(false)
        checkIfSaucerNeeded(U.SaucerDelay + 0.1)
        assertThat(Saucer.active).isEqualTo(true)
        assertThat(Saucer.dx).describedAs("reverse").isLessThan(0.0)
    }

Should fail on reverse. Does:

java.lang.AssertionError: [reverse] 
Expecting actual:
  150.0
to be less than

Now to make it happen.

private var saucerSpeed = U.SaucerSpeed
private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        saucerGoneFor = 0.0
        if (!Saucer.active) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(saucerSpeed, 0.0)
            saucerSpeed *= -1.0
        } else {
            Saucer.active = false
        }
    }
}

I expect green. I get green. Commit: Saucer alternates direction. Does not turn.

I think that’ll do for this afternoon. Let’s sum up.

Summary

Creating tests for the Saucer made it easy. It went in just fine until I did something wrong with that refactoring. Reverting and going in smaller steps worked fine.

I think I could have committed more frequently, and had I committed before the refactoring, reversion would have been easier. As it was, I had to manually undo, typing all those tedious Command+Zs. I am exhausted.

It went well. And there’s now a bit of duplication that we could think about removing:

var dropScale = U.ShipDropInScale
private var shipGoneFor = 0.0
fun checkIfShipNeeded(deltaTime: Double) {
    if ( ! Ship.active ) {
        shipGoneFor += deltaTime
        if (shipGoneFor > U.ShipDelay) {
            dropScale = U.ShipDropInScale
            Ship.position = Vector2(U.ScreenWidth/2.0, U.ScreenHeight/2.0)
            Ship.velocity = Vector2(0.0,0.0)
            Ship.angle = 0.0
            Ship.active = true
            shipGoneFor = 0.0
        }
    } else {
        dropScale = max(dropScale - U.ShipDropInScale*deltaTime, 1.0)
    }
}

private var saucerSpeed = U.SaucerSpeed
private var saucerGoneFor = 0.0
fun checkIfSaucerNeeded(deltaTime: Double) {
    saucerGoneFor += deltaTime
    if (saucerGoneFor > U.SaucerDelay) {
        saucerGoneFor = 0.0
        if (!Saucer.active) {
            Saucer.active = true
            Saucer.position = Vector2(0.0, Random.nextDouble(U.ScreenHeight.toDouble()))
            Saucer.velocity = Vector2(saucerSpeed, 0.0)
            saucerSpeed *= -1.0
        } else {
            Saucer.active = false
        }
    }
}

I’m not at all sure that I’ll go after that, especially since the saucer one is going to get more tricky. Although … what if we had a new function, checkIfSaucerTurns?

Anyway in this version we can’t readily extract an object. We’ll probably let the duplication be.

Anyway, a nice new feature and we have a saucer.

saucer right side up

See you next time!