GitHub Repo

Still thinking about the essence of this design and what it implies. This time for the game, not the world. I am fascinated and love what it is … and isn’t. In a real sense … it isn’t a game at all.

Earlier today I wrote Implications, about some probably ill-drawn notions of reality based on how this game works. Here, I want to stay a bit closer to earth, but still in outer space, and think about the design as it works for the game.

I said in that article that the game’s little objects work according to some very simple rules:

  1. Any object can remove itself from the mix.
  2. Any object can add any other object to the mix.
  3. Every object can receive an interactWith... message from another in the mix. That message includes the object interacted with, and a transaction, and the name of the method tells the receiver what kind of object they’re interacting with.
  4. Every object can have a chance to update, and a chance to draw itself.

I’ve written a lot of these games. This is my second Asteroids, maybe third, and I’ve done Spacewar! at least three times. This is the first time that I’ve done it in this exceedingly non-procedural way. There is no central object deciding things. In fact, the only decision that any object can make about any other is to create it. No object is allowed to destroy another. (There is an exception in there now, which I intend to fix shortly.)

It is possible for an object to get information from another object … and use that information only to decide whether to destroy itself, or to create something new.

It’s rather a fascinating set of rules, and what I find most fascinating is that it creates, not just an enjoyable game but a game that is indistinguishable from the original, which was definitely not written in this style.

My game is hard to understand, in the sense that while you can readily see what each object does, it can be difficult to see how it all adds up to a reasonable game. I think life is like that as well: it is hard to see how people doing such mundane things can add up to such a complex world. It’s harder still to see how atoms, doing the simple things they do, can add up to people doing the things that people do.

It’s all just in the mix. Even this game is the result of whatever particles we want to settle on, protons and neutrons, whatever, just whirling about … and next thing you know, a few billion years go by and there’s an asteroids game.

I mentioned that there’s an object in the system that destroys another. I’m tempted to make that impossible … but I don’t see how. I can certainly make this case not happen. ScoreKeeper destroys the Score.

class ScoreKeeper: ISpaceObject, InteractingSpaceObject {
    var totalScore = 0

    override val subscriptions = Subscriptions(
        interactWithScore = { score, trans ->
            totalScore += score.score
            trans.remove(score)
        },
        draw = this::draw
    )

    override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
        // try not doing anything
    }

    override fun update(deltaTime: Double, trans: Transaction) {}

    override fun finalize(): List<ISpaceObject> { return emptyList() }

    fun draw(drawer: Drawer) {
        drawer.translate(100.0, 500.0)
        drawer.stroke = ColorRGBa.GREEN
        drawer.fill = ColorRGBa.GREEN
        drawer.text(formatted(), Point(0.0, 0.0))
    }

    fun formatted(): String {
        return ("00000" + totalScore.toShort()).takeLast(5)
    }
}

You see there in interactWithScore, the Keeper destroys the Score. The Score must be destroyed: if it were not, the score would increase without bound, very rapidly, as the same Score got counted over and over. But we don’t want the ScoreKeeper destroying other than itself (which it will never do). So we remove that line:

    override val subscriptions = Subscriptions(
        interactWithScore = { score, trans -> totalScore += score.score },
        draw = this::draw
    )

If I were to run now, the score would increase without bound as soon as I shoot one asteroid. And it does: I couldn’t resist trying.

Now how can we get rid of the Score? Simple! As soon as it interacts with the ScoreKeeper, it can destroy itself. But ScoreKeeper currently doesn’t interact: that code and comment are there because I consciously thought about whether it was OK for an object not to interact. Turns out, in this case, it’s not OK.

There is no interactWithScoreKeeper in Subscriptions, yet, but we need one. In ScoreKeeper, we implement the standard boilerplate:

    override fun callOther(other: InteractingSpaceObject, trans: Transaction) {
        other.subscriptions.interactWithScoreKeeper(this, trans)
    }

In subscriptions, we provide the default.

class Subscriptions(
    val beforeInteractions: () -> Unit = {},

    val interactWithMissile: (missile: Missile, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithScore: (score: Score, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithScoreKeeper: (keeper: ScoreKeeper, trans: Transaction) -> Unit = { _, _, -> },
    val interactWithShip: (ship: Ship, trans: Transaction) -> Unit = { _, _, -> },
    ...

And in Score, we subscribe to the event:

class Score(val score: Int): ISpaceObject, InteractingSpaceObject {

    override val subscriptions = Subscriptions(
        interactWithScoreKeeper = {_, trans -> trans.remove(this) }
    )

That should do the job just fine. Tests are green, game scores correctly. Commit: ScoreKeeper no longer kills Score object. Score object cleans itself up.

Now we have to ask ourselves whether that was a simple change or not. I think the short answer is that while it only changed three lines, the three lines were in three different objects. We had to know which three to change, and we had to think about how they interact. When there is a Score in the mix, the ScoreKeeper will interact with it, and the Score will interact with the Scorekeeper. The ScoreKeeper extracts the score value and posts it. The Score, because it is interacting with the ScoreKeeper, can remove itself because we know, what it does not, that its score value has been recorded.

None of the game objects really knows what is going on. The Game lets everyone interact and knows nothing about what they do. The ScoreKeeper knows that there is a score to tally, but doesn’t know where the Score came from or where it went. The Score basically knows nothing. Its sole job is to remove itself when it interacts with a ScoreKeeper. Where is the knowledge of the rather intricate relationship between these cooperating objects?

I assert that that knowledge is not in the program anywhere. We normally try to write code that states clearly what is known and what happens. In this program, no part of it knows what is going on, and no part of it understands more than its own trivial role in the scheme of things.

It reminds me of life. It reminds me of a mute, unknowing universe of whirling atoms, or maybe even just immutable spacetime, that somehow includes patterns such that some of those patterns (us) see the patterns of us and others, and make a kind of sense out of it all.

I am amazed, and delighted. We see a game when we run this program: and yet, in a very real sense, there is no game in the program. There’s no “place” where it all happens, no central organization that makes it happen. It just emerges from the interactions.

I love it.