GitHub Repo

I improved the Splats a bit last night. I report on last night, think about design, improve Saucer some more, and feel rather ashamed of myself. But good, too.

Last night, while not writing one of these innumerable articles, I modified the Splat to take a scale value. It looks like this:

class Splat(var position: Point, var scale: Double = 1.0) : ISpaceObject, InteractingSpaceObject {
    
    constructor(ship: Ship) : this(ship.position, 2.0)
    constructor(missile: Missile) : this(missile.position)

I am in the process of changing things so that the Ship has a big splat, the missile has a very small one, and the Saucer now has a big one as well. But I want more. I want them in different colors.

I also modified Missile so that the ship’s missiles are white, and the saucer’s are green:

class Missile(
    shipPosition: Point,
    shipHeading: Double = 0.0,
    shipKillRadius: Double = 100.0,
    shipVelocity: Velocity = Velocity.ZERO,
    val color: ColorRGBa = ColorRGBa.WHITE
): ISpaceObject, InteractingSpaceObject, Collider {
    constructor(ship: Ship): this(ship.position, ship.heading, ship.killRadius, ship.velocity)
    constructor(saucer: Saucer): this(saucer.position, Random.nextDouble(360.0), saucer.killRadius, saucer.velocity, ColorRGBa.GREEN)

So, let’s add color to the Splat, and then sort out the possibilities.

class Splat(
    var position: Point,
    var scale: Double = 1.0,
    var color: ColorRGBa = ColorRGBa.WHITE
) : ISpaceObject, InteractingSpaceObject {
    
    constructor(ship: Ship) : this(ship.position, 2.0, ColorRGBa.WHITE)
    constructor(missile: Missile) : this(missile.position, 0.5, missile.color)
    constructor(saucer: Saucer) : this(saucer.position, 2.0, ColorRGBa.GREEN)

And in the callers:

class HyperspaceOperation
    private fun destroyTheShip(trans: Transaction) {
        trans.add(ShipDestroyer())
        trans.add(Splat(ship))
    }

class Missile
    override fun finalize(): List<ISpaceObject> {
        return listOf(Splat(this))
    }

class Saucer
    private fun checkCollision(asteroid: Collider, trans: Transaction) {
        if (Collision(asteroid).hit(this)) {
            trans.add(Splat(this))
            trans.remove(this)
        }
    }

class Ship
    private fun checkCollision(other: Collider, trans: Transaction) {
        if (weAreCollidingWith(other)) {
            trans.remove(this)
            trans.add(Splat(this))
        }
    }

Finally, in SplatView, we use the color:

    fun draw(splat: Splat, drawer: Drawer) {
        drawer.stroke = splat.color
        drawer.fill = splat.color
        drawer.rotate(rot)
        drawer.scale(splat.scale, splat.scale)
        for (point in points) {
            val size = sizeTween.value(splat.elapsedTime)
            val radius = radiusTween.value(splat.elapsedTime)
            drawer.circle(size*point.x, size*point.y, radius)
        }
    }

It works a treat. Let me commit this and then we’ll get a photo.

I’d like to show you a lot of splats at once. I’ll try to make a short movie. Might have to edit it mercilessly to get it short enough.

splats movie

OK, right. Now I want to think about our Zoom session last night. We looked at what Hill is doing in his part of the forest. He doesn’t really like my distributed action at a distance emergent behavior implementation with all the little objects where you can’t understand what’s going to happen because they all interact in small but all-important ways, leading to your needing to keep a bunch of mental balls juggling in your head and requires editing two or three or more objects to make anything happen which fills up his buffers and leaves him unsatisfied at the end of things.

I can see his point.

He’s working toward a different approach to object interaction that will let him keep the main objects and remove the specials such as the Checkers, Makers, Destroyers and all that there. In aid of that, he has arranged that the knownObjects collection is broken out internally by the type of the object, so that the missiles are separate from the asteroids, which are separate from the ship, and so on. He did this by overloading add and remove to look in various sets inside the knownObjects object. Pretty straightforward, except that he had to break out the Transaction similarly. Straightforward even so.

With that structure in place, he can program interaction more directly, for example by sending all the asteroids against the ship directly, then the missiles, and so on. And he’s planning to do the interactions only one way, so that whichever object has control will destroy itself and the enemy object if they collide.

He thinks this will make the interaction between objects more obvious, by putting it all in one place, and that it will allow him to remove the special objects. I suspect — and he doesn’t disagree — that this may result in some rather monolithic highly coupled objects, such as a ship that knows all about missiles, asteroids, saucers, and what all. He believes — and I agree — that if this does happen, our standard refactoring tools will allow him to break things out in a sensible and pretty standard way.

My take is that it’s going to work and that if he pushes far enough, the comparison of the two approaches will be worth thinking about. The thing will be to get both games approximately equally capable so that the comparison will be mostly apples to apples, without much speculation about things not yet done. I hope we get to do that.

And … he’s making some simplifications that might benefit my version as well, even if I keep my tiny objects approach, which I surely will do. Let’s talk about some possibilities:

Possible Simplifications

We might benefit from folding the views back into the objects. It’s at least worth looking at. That has already been done with finalize in my version.

The Saucer has a SaucerMaker, but not a SaucerChecker. We might be able to eliminate the Ship Checker/Maker or WaveChecker/Maker, combining each pair into just a CheckerMaker. I imagine that there might even be enough common elements in the Checker-Maker family to allow some consolidations across types. Ship and Saucer ought to be similar, except for the hyperspace glitch.

That reminds me of a thing Hill thinks he’ll do. He plans to have the Ship have two modes, active and inactive, where it is visible and moves when active, and not when inactive. That is expected to allow centralizing more knowledge and power in the ship, making things like emergence timing and such easier. I can see the point. I also rather like the fact that our ship doesn’t have much of a master state like that. Instead, it just goes about its business. I’m almost sorry that the current interaction pattern calls for objects removing themselves. It seems rude to make them have to do that.

Something to think about … but it works nicely now and refactoring the collision logic is likely to be tricky.

Aside
The fact that I think that should tell us that there is something iffy about the current scheme. I’ve mentioned it before. The logic of the game being distributed and somewhat “emergent” is interesting, but it does mean that when two objects interact, we have to make changes to each of them. In a more direct design, things would be done to them instead. Maybe better, maybe not, but certainly different.

Back To Work

Let’s take a look at some of the views, to see whether we’d like to fold them into the class they view.

I should mention that in general, it’s thought that objects and their views “should” be separate, because there might be many different views, and the best known patterns for such things keep them separate. Here, in video game, I am inclined to tolerate a closer connection between the thing and how it looks … and we can always factor it back out if need be.

class SplatView(lifetime: Double) {
    private val rot = Random.nextDouble(0.0, 360.0)
    private var sizeTween = Tween(20.0, 100.0, lifetime)
    private var radiusTween = Tween(30.0, 5.0, lifetime)
    private val points = listOf(
        Point(-2.0, 0.0),
        Point(-2.0, -2.0),
        Point(2.0, -2.0),
        Point(3.0, 1.0),
        Point(2.0, -1.0),
        Point(0.0, 2.0),
        Point(1.0, 3.0),
        Point(-1.0, 3.0),
        Point(-4.0, -1.0),
        Point(-3.0, 1.0)
    )

    fun draw(splat: Splat, drawer: Drawer) {
        drawer.stroke = splat.color
        drawer.fill = splat.color
        drawer.rotate(rot)
        drawer.scale(splat.scale, splat.scale)
        for (point in points) {
            val size = sizeTween.value(splat.elapsedTime)
            val radius = radiusTween.value(splat.elapsedTime)
            drawer.circle(size*point.x, size*point.y, radius)
        }
    }
}

We can readily copy this over into Splat. Splat looks like this:

class Splat(
    var position: Point,
    var scale: Double = 1.0,
    var color: ColorRGBa = ColorRGBa.WHITE
) : ISpaceObject, InteractingSpaceObject {
    
    constructor(ship: Ship) : this(ship.position, 2.0, ColorRGBa.WHITE)
    constructor(missile: Missile) : this(missile.position, 0.5, missile.color)
    constructor(saucer: Saucer) : this(saucer.position, 2.0, ColorRGBa.GREEN)

    var elapsedTime = 0.0
    private val lifetime = 2.0
    private var view = SplatView(2.0)

    override fun update(deltaTime: Double, trans: Transaction) {
        elapsedTime += deltaTime
        if (elapsedTime > lifetime) trans.remove(this)
    }

    fun draw(drawer: Drawer) {
        drawer.translate(position)
        view.draw(this, drawer)
    }

    override fun finalize(): List<ISpaceObject> = emptyList()

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

It’s pretty simple, despite the constructors. Should we do it? I decide no, because the SplatView has its own memory of what it’s doing, with its Tweens and such. (I think it is the only user of Tween, by the way. We should probably use them more … or less.) I notice, though, that the Splat should pass lifetime to SplatView, not a raw 2.0:

    private val lifetime = 2.0
    private var view = SplatView(lifetime)

Commit: Tidying splat.

What about ShipView:

class ShipView {
    fun draw(ship: Ship, drawer: Drawer) {
        val points = listOf(
            Point(-3.0, -2.0),
            Point(-3.0, 2.0),
            Point(-5.0, 4.0),
            Point(7.0, 0.0),
            Point(-5.0, -4.0),
            Point(-3.0, -2.0)
        )
        drawer.scale(30.0, 30.0)
        drawer.rotate(ship.heading )
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0
        drawer.lineStrip(points)
    }
}

Right. Now that one I think we will move. We’ll go in two steps, first pasting the code over (and editing as needed), then a promotion of the points to a constant.

class Ship
    fun draw(drawer: Drawer) {
        drawer.translate(position)
//        drawKillRadius(drawer)
        val points = listOf(
            Point(-3.0, -2.0),
            Point(-3.0, 2.0),
            Point(-5.0, 4.0),
            Point(7.0, 0.0),
            Point(-5.0, -4.0),
            Point(-3.0, -2.0)
        )
        drawer.scale(30.0, 30.0)
        drawer.rotate(heading )
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0
        drawer.lineStrip(points)
    }

And then let’s move the points outside as a constant.

private val points = listOf(
    Point(-3.0, -2.0),
    Point(-3.0, 2.0),
    Point(-5.0, 4.0),
    Point(7.0, 0.0),
    Point(-5.0, -4.0),
    Point(-3.0, -2.0)
)

    fun draw(drawer: Drawer) {
        drawer.translate(position)
//        drawKillRadius(drawer)
        drawer.scale(30.0, 30.0)
        drawer.rotate(heading )
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0
        drawer.lineStrip(points)
    }

Yes that seems fine. Remove ShipView. Commit: inline ShipView into Ship.

The AsteroidView is fairly complex. I clean it up a bit and get this:

private val rocks = listOf(
    listOf(
        Point(4.0, 2.0), Point(3.0, 0.0), Point(4.0, -2.0),
        Point(1.0, -4.0), Point(-2.0, -4.0), Point(-4.0, -2.0),
        Point(-4.0, 2.0), Point(-2.0, 4.0), Point(0.0, 2.0),
        Point(2.0, 4.0), Point(4.0, 2.0),
    ),
    listOf(
        Point(2.0, 1.0), Point(4.0, 2.0), Point(2.0, 4.0),
        Point(0.0, 3.0), Point(-2.0, 4.0), Point(-4.0, 2.0),
        Point(-3.0, 0.0), Point(-4.0, -2.0), Point(-2.0, -4.0),
        Point(-1.0, -3.0), Point(2.0, -4.0), Point(4.0, -1.0),
        Point(2.0, 1.0)
    ),
    listOf(
        Point(-2.0, 0.0), Point(-4.0, -1.0), Point(-2.0, -4.0),
        Point(0.0, -1.0), Point(0.0, -4.0), Point(2.0, -4.0),
        Point(4.0, -1.0), Point(4.0, 1.0), Point(2.0, 4.0),
        Point(-1.0, 4.0), Point(-4.0, 1.0), Point(-2.0, 0.0)
    ),
    listOf(
        Point(1.0, 0.0), Point(4.0, 1.0), Point(4.0, 2.0),
        Point(1.0, 4.0), Point(-2.0, 4.0), Point(-1.0, 2.0),
        Point(-4.0, 2.0), Point(-4.0, -1.0), Point(-2.0, -4.0),
        Point(1.0, -3.0), Point(2.0, -4.0), Point(4.0, -2.0),
        Point(1.0, 0.0)
    )
)

class AsteroidView {
    private val rock = rocks.random()

    fun draw(asteroid: Asteroid, drawer: Drawer) {
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 16.0
        drawer.fill = null
        val sizer = 30.0
        drawer.scale(sizer, sizer)
        val sc = asteroid.scale()
        drawer.scale(sc,sc)
        drawer.rotate(asteroid.heading)
        drawer.stroke = ColorRGBa.WHITE
        drawer.strokeWeight = 8.0/30.0/sc
        drawer.scale(1.0, -1.0)
        drawer.lineStrip(rock)
    }

}

I think we’ll let that ride as well. Keep the mess to itself. Commit: Tidy AsteroidView.

Now What?

Back to our list of things for the Saucer:

  1. √ Creation of missiles by other than a ship.
  2. √ Possibly a different color or look to them?
  3. X Missiles should be able to shoot down missiles.
  4. √ Missiles should be able to destroy the ship (presently cannot).

We can check off the color one:

  1. √ Creation of missiles by other than a ship.
  2. √ Possibly a different color or look to them?
  3. X Missiles should be able to shoot down missiles.
  4. √ Missiles should be able to destroy the ship (presently cannot).

Let’s allow missiles to kill missiles.

class Missile
    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans ->
            if (checkCollision(asteroid)) { trans.remove(this) }
        },
        interactWithSaucer = { saucer, trans ->
            if (checkCollision(saucer)) { trans.remove(this) }
        },
        interactWithShip = { ship, trans ->
            if (checkCollision(ship)) { trans.remove(this) }
        },
        draw = this::draw
    )

This should do the job:

        interactWithMissile = { missile, trans ->
            if (checkCollision(missile)) { trans.remove(this) }
        },

However, I think the odds are not good, because the radius of a missile is only 10, so it’ll take some precision shooting to hit one. Still, there it is. Commit it.

We can check that one off, but there is another issue: we really don’t want the score going up when a saucer or saucer missile destroys an asteroid. So I’ll add two more lines:

  1. √ Creation of missiles by other than a ship.
  2. √ Possibly a different color or look to them?
  3. √ Missiles should be able to shoot down missiles.
  4. √ Missiles should be able to destroy the ship.
  5. X Saucer missile collision with asteroids should not score.
  6. X Saucer collision with asteroids should not score.

OK, who’s creating the Score? That’s done in Asteroid now, as part of finalize:

    override fun finalize(): List<ISpaceObject> {
        val objectsToAdd: MutableList<ISpaceObject> = mutableListOf()
        val score = getScore()
        objectsToAdd.add(score)
        if (splitCount >= 1) {
            objectsToAdd.add(asSplit(this))
            objectsToAdd.add(asSplit(this))
        }
        return objectsToAdd
    }

Let’s remove that and make the missile do the scoring.

    override fun finalize(): List<ISpaceObject> {
        val objectsToAdd: MutableList<ISpaceObject> = mutableListOf()
        if (splitCount >= 1) {
            objectsToAdd.add(asSplit(this))
            objectsToAdd.add(asSplit(this))
        }
        return objectsToAdd
    }

And in Missile …

        interactWithAsteroid = { asteroid, trans ->
            if (checkCollision(asteroid)) { trans.remove(this) }
        },

As a first step, let’s always score:

        interactWithAsteroid = { asteroid, trans ->
            if (checkCollision(asteroid)) {
                trans.remove(this)
                trans.add(asteroid.getScore())
            }
        },

Game should still score as before. That seems to be the case. But what we want is something like this:

    override val subscriptions = Subscriptions(
        interactWithAsteroid = { asteroid, trans ->
            if (checkCollision(asteroid)) {
                trans.remove(this)
                if (missileIsFromShip trans.add(asteroid.getScore())
            }
        },

We could have two different classes for Missile, but that seems too much. Let’s construct them with a flag.

class Missile(
    shipPosition: Point,
    shipHeading: Double = 0.0,
    shipKillRadius: Double = 100.0,
    shipVelocity: Velocity = Velocity.ZERO,
    val color: ColorRGBa = ColorRGBa.WHITE,
    val missileIsFromShip: Boolean = false
): ISpaceObject, InteractingSpaceObject, Collider {
    constructor(ship: Ship): this(ship.position, ship.heading, ship.killRadius, ship.velocity, ColorRGBa.WHITE, true)
    constructor(saucer: Saucer): this(saucer.position, Random.nextDouble(360.0), saucer.killRadius, saucer.velocity, ColorRGBa.GREEN)

That should do it. Works as advertised. Additional effect: now if the ship collides with an asteroid, it doesn’t get the points. Probably makes sense. Commit: only ship missiles score points on asteroids.

Again I get to check some items off, but I have to add a new pair:

  1. √ Creation of missiles by other than a ship.
  2. √ Possibly a different color or look to them?
  3. √ Missiles should be able to shoot down missiles.
  4. √ Missiles should be able to destroy the ship.
  5. √ Saucer missile collision with asteroids should not score.
  6. √ Saucer collision with asteroids should not score.
  7. X Ship missile killing saucer scores 200 for large saucer.
  8. X Ship missile killing saucer scores 1000 for large saucer.

That should be easy enough … should we do it in missile? Might as well, we know the facts there.

class Missile
        interactWithSaucer = { saucer, trans ->
            if (checkCollision(saucer)) {
                trans.remove(this)
                if (missileIsFromShip) trans.add(saucer.getScore())
            }
        },

And, for now,

class Saucer
    fun getScore() = Score(200)

That’s good too. Commit: hitting large saucer with ship missile scores 200. Another check mark.

  1. √ Creation of missiles by other than a ship.
  2. √ Possibly a different color or look to them?
  3. √ Missiles should be able to shoot down missiles.
  4. √ Missiles should be able to destroy the ship.
  5. √ Saucer missile collision with asteroids should not score.
  6. √ Saucer collision with asteroids should not score.
  7. √ Ship missile killing saucer scores 200 for large saucer.
  8. X Ship missile killing saucer scores 1000 for large saucer.

Time to sum up and score myself.

Summary

I got a lot done, and I am confident in all of it. The simplicity of my objects helps with that. Would I bet $100 that this is all good … maybe not. Why?

I wrote no tests for any of this, and I’m sure that the existing tests don’t cover the cases we dealt with. We have no tests for non-ship missiles, colliding or anything else. I think we have no tests of anyone destroying a Saucer, certainly none addressing scoring.

And … shame of shame … I haven’t run the test in a while … and when I do three fail!!

Let’s see what we’ve got. This one is expecting the score that we used to return:

    @Test
    fun `asteroid splits on finalize`() {
        val full = Asteroid(
            position = Point.ZERO,
            velocity = Velocity.ZERO
        )
        val radius = full.killRadius
        val halfSize= full.finalize()
        assertThat(halfSize.size).isEqualTo(3) // two asteroids and a score
        val half = halfSize.last()
        assertThat((half as Asteroid).killRadius).describedAs("half").isEqualTo(radius/2.0)
        val quarterSize = half.finalize()
        assertThat(quarterSize.size).isEqualTo(3)
        val quarter = quarterSize.last()
        assertThat((quarter as Asteroid).killRadius).describedAs("quarter").isEqualTo(radius/4.0)
        val eighthSize = quarter.finalize()
        assertThat(eighthSize.size).describedAs("should not split third time").isEqualTo(1)
    }

OK, no harm done, change the 3s to 2s, and the 1 to 0.

    @Test
    fun `asteroid finalizer`() {
        val asteroid = Asteroid(Point.ZERO)
        val splits = asteroid.finalize()
        assertThat(splits.size).isEqualTo(3) // split guys and a score
    }

Same issue, same fix.

    @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.add(asteroid)
        game.add(ship)
        assertThat(game.knownObjects.size).isEqualTo(2)
        assertThat(ship).isIn(game.knownObjects.spaceObjects)
        game.processInteractions()
        assertThat(game.knownObjects.size).isEqualTo(4) // new ship (hack) Splat, and a Score
        assertThat(ship).isNotIn(game.knownObjects.spaceObjects) // but a new one is
    }

Same issue, same fix. Green. Commit: fix tests due to scoring change.

Now, as I was saying, I haven’t been keeping the tests up to snuff. For me, this is a perennial problem in screen-oriented game. To my small credit, I do have more tests than in some previous games, where I had almost none. But I could certainly do better than I’m doing.

This slows me down, and I know it. Cases where I could just run the tests and be confident turn into running the game, waiting until I’m sure I know what the score is before shooting the saucer, only to have it ram an asteroid before I can shoot it. I have unquestionably wasted many minutes in manually testing the game, not even counting the time that I then waste actually playing a bit before I turn back to my work.

In a “real” program the effect is larger, especially as manual testing gets more and more time-consuming. And we get more defects as well, for at least two reasons. First, sometimes our manual test just isn’t sufficient to trigger a bug: we tend to test what we know works and not to think of things that might be wrong. Second, we get tired of testing and skip over a test that would have broken had we only done it.

Even though I’m just a guy sitting in his living room at his zebrawood desk programming for fun and writing for a few folks, I feel badly about this. I “should” be setting a better example. YKW would take away my craftsman badge had that not already happened. And think of the children who will say “if Ron doesn’t have to test, I don’t have to either”.

Put them to bed without supper. Two wrongs don’t make a right, kids, but three lefts sometimes do.

And … when we get behind on tests, it is truly tedious and boring to write them after the fact. Writing them first provides benefits: it helps us focus on the say the objects will interact and behave, and it lets us then get a little jolt of happiness when things work as intended. tests after the fact don’t include as much learning, they don’t impact the design as favorably, the news they bring is always bad, and mostly they’re just boring.

Don’t do like my brother’s brother. Keep your tests first and frequent.

I’ll try to do better, and to add some back in. Maybe I’ll let you watch, maybe I’ll just grind them out and report later.

For now … we have some nice new features including colored missiles, better scoring, and more beautiful splats in multiple colors and sizes. And that’s good!

See you next time!