GitHub Decentralized Repo
GitHub Centralized Repo
GitHub Flat Repo

I’m not sure what to do this morning. Maybe an asteroid. There are other options. Also: Beck’s Rules of Simple Code.

Other options include:

  • Require an entity’s components to be created once and for all when it is created;
  • Package all the game’s components into a structure rather than a bunch of globals;
  • Simplify the parameters provided to gameCycle;
  • Give up this silly rule about having no methods.

Upon due reflection over the time it took me to type those, I think that none are as interesting as creating asteroids. So be it, we’ll create asteroids.

We need to begin, I guess, with the enum:

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

It already has a value for asteroid and we even have one set of the asteroid shape points in the system, although only the one. Enough for now.

When we create the game, we ask for some number of asteroids, but we don’t create any yet:

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

I think we can see what’s needed here:

fun createGame(missileCount: Int, asteroidCount: Int) {
    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()
}

This demands that we implement newAsteroid. No one could have guessed that.

private fun newAsteroid(): SpaceObject
        = SpaceObject(SpaceObjectType.ASTEROID, 0.0, 0.0, 0.0, 0.0, 0.0, false)

This much will create our 29 asteroids, or however many there are. None of them will display, and if they did, they’ll all be on top of each other in the corner of the screen. When we start the game, we should activate some asteroids, four to be precise. Let’s do that here:

fun startGame(width: Int, height: Int) {
    Ship.active = true
    Ship.x = width/2.0
    Ship.y = height/2.0
}

For now, just this:

fun startGame(width: Int, height: Int) {
    Ship.active = true
    Ship.x = width/2.0
    Ship.y = height/2.0
    activateAsteroids(4)
}

This demands that I implement activateAsteroids. Again no one could have guessed that.

private fun activateAsteroids(asteroidCount: Int) {
    deactivateAsteroids()
    for (i in 1..asteroidCount) {
        activateAsteroid()
    }
}

We need to deactivate all the asteroids first, because we might be in attract mode, or there might be live ones for some other reason. Now we are required to implement two functions. We’re losing ground.

private fun deactivateAsteroids() {
    spaceObjects.filter 
    	{ it.type == SpaceObjectType.ASTEROID }.forEach 
    		{ it.active = false }
}

That seems reasonable. The activate is a bit more tricky. In principle there might not be any available asteroids. Shall we cater to that? Is there a first selector on collections?

First cut is this:

private fun activateAsteroid() {
    val asteroids = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }
    val inactive = asteroids.firstOrNull { ! it.active }
    inactive?.active = true
}

But that won’t do. We need to position it and give it a velocity or it’ll be a pretty boring game.

OK, how about this:

private fun activateAsteroid() {
    val asteroids = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }
    val available = asteroids.firstOrNull { ! it.active }
    if (available != null) {
        available.y = Random.nextDouble(U.ScreenHeight.toDouble())
        setVelocity(available, randomVelocity())
    }
}

Will this never end? Now I have to do randomVelocity.

private fun randomVelocity(): Vector2 {
    val randomAngle = Random.nextDouble(360.0)
    return Vector2(U.AsteroidSpeed, 0.0).rotate(randomAngle)
}

We’re getting there. We need AsteroidSpeed. In the other version, it’s 100.0. We’ll use that.

object U {
    const val AsteroidCount = 26
    const val AsteroidSpeed = 100.0
    const val LightSpeed = 500.0
    const val MissileCount = 6
    const val MissileOffset = 50.0
    const val MissileSpeed = LightSpeed/3.0
    const val MissileTime = 3.0
    const val ScreenHeight = 1024
    const val ScreenWidth = 1024
    const val ShipDeltaV = 120.0
}

I think this will compile and I just have to run it to see what it does.

Well, what it does is not display any asteroids. Curious. Oh. I didn’t set my discovered asteroid to active. There’s yer problem right there.

private fun activateAsteroid() {
    val asteroids = spaceObjects.filter { it.type == SpaceObjectType.ASTEROID }
    val available = asteroids.firstOrNull { ! it.active }
    if (available != null) {
        available.active = true
        available.y = Random.nextDouble(U.ScreenHeight.toDouble())
        setVelocity(available, randomVelocity())
    }
}

Run again expecting asteroids.

four small asteroids and a ship

That, ladies, gentlemen, and others, is what I call success. The asteroids are small, no surprise there, as the drawing is done at the smallest scale and needs to be scaled up for larger asteroids. But there are four of them, as desired, and they start on the sides of the screen and move in random directions. Let’s reflect, and we’ll probably stop right here, on a win. It’s Sunday and our Sunday ritual will soon begin.

Reflection

The big question here should be “where are your tests”, because I certainly didn’t write any. And you know, you get on a roll and things are going well and you say you’ll write them after but then you look at the code and it’s obviously correct and so you see no point to writing any silly tests.

Oh, that’s just me? OK, I’ll write one.

    @Test
    fun `can activate asteroids`() {
        createGame(6, 6)
        val asteroidCount = spaceObjects.count { it.type == SpaceObjectType.ASTEROID}
        assertThat(asteroidCount).isEqualTo(6)
        var inactiveCount = spaceObjects.count { it.type == SpaceObjectType.ASTEROID && ! it.active}
        assertThat(inactiveCount).isEqualTo(6)
        for ( i in 1..4) activateAsteroid()
        inactiveCount = spaceObjects.count { it.type == SpaceObjectType.ASTEROID && ! it.active}
        assertThat(inactiveCount).isEqualTo(2)
    }

So that’s green. Commit: game activates four small asteroids on start.

I’d have to say that so far, this went in easily and isn’t too awful. If we had methods on the objects, and different types, it would be a bit easier, but at the cost of more classes and methods. This puts me in mind of Kent Beck’s “Rules of Simple Design”:

  1. Passes all the tests;
  2. Contains no duplication;
  3. Expresses all our design ideas;
  4. Minimizes number of programming entities.

Programming entities means like modules, classes, methods, functions, variables, and so on.

We have a tension here between expression and number of entities. Our notions of missile vs ship vs asteroid are expressed but not as well as they might be. If we were to express those design ideas with classes and methods, we’d get more code. But the rules are in priority order and would generally call for creating the distinction with something like classes and methods.

We’re operating with a specific constraint, which is not to use methods, only data classes as structs. We’re kind of bending that rule a bit with the components, which are still data classes, but they do implement an interface, albeit a very simple one.

My feeling so far is that this version is actually simpler than either of the OO versions, but it’s far short of the others in terms of features.

We’ll keep pushing until we get nearer to feature parity to decide, but so far, I think this is going well given what it is.

Issues before us include asteroid size, having different asteroid shapes, possibly different asteroid rotations, and, of course the big one, collision handling.

We’ll get there. Inch by inch. I hope you’ll enjoy following along.