GitHub Repo

Sometimes something really simple is not obvious. This may be a case for pair and mob and ensemble programming.

I’ve been struggling with the somewhat awkward syntax of the “facts” that the game keeps, so that distant rooms can know things that are not part of them, such as whether there is a gate open, or whether the player has as yet encountered the Persistently Passionate Princess. Today, the syntax looks like this:

    action("unlock", "gate") {
        if (inventoryHas("keys")) {
            say("You fumble through the keys and finally unlock the gate!")
            facts["openGate"].set(true)
        } else {
            say("The gate is locked.")
        }
    }
    go(D.West, R.LowCave) {
        when (facts["openGate"].isTrue) {
            true -> yes("You open the gate and enter.")
            false -> no("The gate is locked.")
        }
    }

I spent a pleasant hour or so last night, trying various syntax and helper methods. The objects supporting the feature look like this:

class Facts {
    private val map = mutableMapOf<String, Fact>()
    operator fun get(name: String) = map.getOrPut(name) { Fact() }

    fun increment(name: String): Unit           = this[name].increment()
    fun isFalse(name: String): Boolean          = this[name].isFalse
    fun isTrue(name: String): Boolean           = this[name].isTrue
    fun decrement(name: String): Unit           = this[name].decrement()
    fun not(name: String): Unit                 = this[name].not()
    fun set(name: String, truth: Boolean): Unit = this[name].set(truth)
    fun set(name: String, amount: Int): Unit    = this[name].set(amount)
    fun truth(name: String): Boolean            = this[name].isTrue
    fun value(name: String): Int                = this[name].value
}

class Fact(var value: Int = 0) {
    val isTrue: Boolean get()  = value != 0
    val isFalse: Boolean get() = value == 0
    fun decrement()            { value -= 1 }
    fun increment()            { value += 1 }
    fun not()                  { value = if (isTrue) 0 else 1 }
    fun set(amount:Int)        { value = amount }
    fun set(truth: Boolean)    { value = if (truth) 1 else 0 }
}

There’s a trick here which I need to admit, because it might be better to have done this differently. In my thinking, a Fact can represent a boolean condition, true/false, or it can represent an integer value. So the methods in Fact translate zero/non-zero to false/true, and can set and return either integer or boolean values, and can increment the value and decrement it. (I am not using most of this capability, and implemented it anyway. At least it’s all tested.)

The helper methods in Facts class would allow me to say

    facts.set("openGate", true) // for
    facts["openGate"].set(true)

    when (facts.isTrue(openGate)) // for
    when (facts["openGate"].isTrue)

I built the various ways to refer to facts, not because I want them all available, but so that I can gain experience using them and then decide on a “standard” way to do things. If this were a real program, we’d publish that way to the game designers and forget about the others.

Aaaanyway …

I was asking on Hill’s Slack yesterday afternoon and evening, to see if anyone had ideas for a better way to do this. Hill finally said something that made the following code obvious.

    val gate = facts["openGate"] // <--- declare a variable!
    action("unlock", "gate") {
        if (inventoryHas("keys")) {
            say("You fumble through the keys and finally unlock the gate!")
            gate.set(true)
        } else {
            say("The gate is locked.")
        }
    }
    go(D.West, R.LowCave) {
        when (gate.isTrue) {
            true -> yes("You open the gate and enter.")
            false -> no("The gate is locked.")
        }
    }

And just to see if I prefer it, I just implemented the val truth:

    val truth: Boolean get()   = value != 0

That lets me say:

    go(D.West, R.LowCave) {
        when (gate.truth) {
            true -> yes("You open the gate and enter.")
            false -> no("The gate is locked.")
        }
    }

I’m not sure, but I might like that better.

Simple, Yet Not Obvious

I think that simple move, declaring val openGate = facts["openGate"] is better. It’s a blissfully simple idea, but one that after hours of thinking, was not obvious to me until whatever Hill said. I was looking deep in the bag of Kotlin tricks to find something to improve the code. The idea “use a temp variable” just never occurred to me. Very likely, if I had been pairing with almost anyone better than a cat, we’d have had the idea sooner.

The good news is that we have it now. I wonder whether we could use this feature to improve other things. First, let’s test and commit. “used val gate = facts[“openGate”] to streamline a room.”

A challenge

The description for the gate room looks like this:

    desc(
        "You are at the cave entrance.",
        "You are at an entrance to a cave. " +
                "A cool breeze emanates from the cave." +
                "There is a locked gate blocking your way west."
    )

It would be a good thing if we could make that message dynamic, so that it changes depending on whether the gate is open or closed. Right now, the descriptions are just strings, and they are not reinterpreted on each usage.

    fun desc(short: String, long: String) {
        shortDesc = short
        longDesc = long
        theDesc = long
    }

    fun description(): String {
        return theDesc.also { setShortDesc() }
    }

The description function could certainly do substitution if we can figure out a way to pass in variable information.

I have an idea that might work. If it works, maybe we can make it simpler.

I do make it work. Here’s the start of the room code:

    room(R.CaveEntrance) {
        val gate = facts["openGate"]
        desc(
            "You are at the cave entrance.",
            "To the west, there is a gated entrance to a cave. " +
                    "A cool breeze emanates from the cave. "
        ) { if (gate.isTrue) "The gate is unlocked." else "The gate is locked."}

I’ve added an optional anonymous function to the desc. Like any such function, it has access to things in its lexical scope, in this case, to gate. So the game works like this now:

> w
To the west, there is a gated entrance to a cave. A cool breeze emanates from the cave. The gate is locked.

> unlock gate
You fumble through the keys and finally unlock the gate!
You are at the cave entrance.The gate is unlocked.

> look
To the west, there is a gated entrance to a cave. A cool breeze emanates from the cave. The gate is unlocked.

This is nice. It limits us to a variable string at the end of the description.

A more capable solution might be to allow for a different version of desc that takes one or two anonymous functions which are always called. Then the description could vary substantially.

I think I’ll experiment with that: it might be a better idea. For now, this will do. Here’s the relevant code from Room:

class Room(val roomName: R, private val actions: IActions = Actions()) : IActions by actions {
    ...
    var shortDesc = ""
    var longDesc = ""
    var theDesc = ""
    var theSubs: () -> String = {""}

    // DSL Builders

    fun desc(short: String, long: String, subs: () -> String = {""}) {
        shortDesc = short
        longDesc = long
        theDesc = long
        theSubs = subs
    }

    fun description(): String {
        return theDesc+theSubs().also { setShortDesc() }
    }

I’d like to add a test for this feature: so far, I’ve been testing by running the game. Bad Ron, no biscuit!

    @Test
    fun `variable description`() {
        var theFacts: Facts = Facts()
        val world = world {
            theFacts = facts
            val door = facts["door"]
            room(R.Z_PALACE) {
                desc("Palace", "Grand Palace") {
                    if (door.isTrue) "A treasure room is off to the east."
                    else "I smell treasure here."
                }
            }
        }
        val room = R.Z_PALACE.room
        val command = Command("look around")
        var response = world.command(command,room)
        assertThat(response.resultString).contains("smell")
        theFacts["door"].not()
        response = world.command(command,room)
        assertThat(response.resultString).contains("treasure room")
    }

That’s a bit messy, since I needed access to facts outside the DSL, but it runs green. Commit: Room.desc now allows optional anonymous function returning a String to be appended to long and short descriptions.

Let’s sum up.

Summary

We were given a simple idea to make handling facts easier: declare a variable. I think this is the “Introduce Variable” refactoring in IDEA/Kotlin. Whatever it is, it made dealing with facts a bit easier and more readable.

That simplicity, and just thinking about what was going on, suggested to me that we could make room descriptions vary based on the status of facts. A simple change, adding an optional anonymous function to the desc, has shown the feasibility of this. A future change will, I think, allow us to make the short and long room descriptions optionally be anonymous functions, providing more capability and removing the need for the trailing lambda. (I can imagine that we might keep it as a convenience, but I’m not sure yet.)

The “big” lesson, I think, is to work with others as much as we can. Even the smartest of us, whoever that may be, can benefit from the ideas of others. And the rest of us need all the help we can get.

That’ll do for this morning. See you next time!