GitHub Repo

Why am I putting so much wonderful capability into a game-making system that will never be used to make a game people will actually play? To tell the truth, I kinda lost track …

Anyway … my plan is to provide for both the short and long description to be entirely variable if the designer so chooses. The idea is to provide an alternate form of desc that takes two anonymous functions rather than strings. I can put that in and make it work with no new tests.

I implement this:

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

    // DSL Builders

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

    fun desc(short: String, long: String) {
        desc({short}, {long})
    }

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

This is essentially a trivial change: I pass in a function, and evaluate it in description. There’s one test that needs fixing, and then game’s handling of the gate needs to be re-implemented. Here’s the test, with the previous “subs” line removed.

Here’s the old test:

    @Test
    fun `variable description`() {
        var theFacts = Facts()
        val world = world {
            theFacts = facts
            val door = facts["door"]
            room(R.Z_PALACE) {
                desc("Palace", "Grand Palace")
            }
        }
        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")
    }

And here’s the new:

    @Test
    fun `variable description`() {
        var theFacts = Facts()
        val world = world {
            theFacts = facts
            val door = facts["door"]
            room(R.Z_PALACE) {
                fun doorInfo() = when(door.isTrue) {
                    true-> "The treasure room door is open to the west."
                    false -> "There is a locked door to the west."
                }
                desc(
                    { "You're in the Palace. " + doorInfo() },
                    { "You are in a Grand Palace. " + doorInfo() })
            }
        }
        val room = R.Z_PALACE.room
        val command = Command("look around")
        var response = world.command(command,room)
        assertThat(response.resultString).contains("locked door")
        val door = theFacts["door"]
        door.not()
        response = world.command(command,room)
        assertThat(response.resultString).contains("door is open")
    }

That runs green. Commit: New Room.desc allows lambdas for both arguments or neither.

Now let’s beef up the gate room. After some renaming and phrasing:

    room(R.CaveEntrance) {
        val gateIsUnlocked = facts["gateIsUnlocked"]
        fun gateMessage()  = when(gateIsUnlocked.isTrue) {
            true -> "The gate is unlocked."
            false -> "The gate is locked"
        }
        desc(
            { "You are at the cave entrance. " + gateMessage() },
            {
                "To the west, there is a gated entrance to a cave. " +
                        "A cool breeze emanates from the cave. " + gateMessage()
            }
        )
        go(D.East, R.WoodsNearCave)
        action("unlock", "gate") {
            if (inventoryHas("keys")) {
                say("You fumble through the keys and finally unlock the gate!")
                gateIsUnlocked.set(true)
            } else {
                say("The gate is locked.")
            }
        }
        go(D.West, R.LowCave) {
            when (gateIsUnlocked.truth) {
                true -> yes("You swing open the unlocked gate and enter.")
                false -> no("The gate is locked. I believe that I mentioned that earlier.")
            }
        }
    }

And it works as advertised:

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

> w
The gate is locked. I believe that I mentioned that earlier.
You are at the cave entrance. 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.

> w
You swing open the unlocked gate and enter.
You are in a low cave room. The cave continues to the west.

I note and fix the missing period on “The gate is locked”. Commit: Game has nice byplay at locked gate.

This is nearly nice. I don’t entirely love the need to create the little function, but it’s a low price to pay for a variable description. I’d ask the game designers to live with it a bit and see how it goes. At this writing, I don’t have a better idea. Perhaps you have. If so, please do let me know.

For now, let’s sum up.

Summary

This morning’s implementation of a variable message tagged on the end of the standard description has evolved to one that will allow the short and long description to be separate, and to vary in any reasonable way based on the state of a Fact. This capability will not be used often, but in certain rooms, the game will seem much more lively if the messages vary.

The core implementation was simple but powerful: you can substitute anonymous functions for short and long description in the desc command, and those functions can do anything and access anything they can see. (I’m sure there is great chaos possible with misuse of this idea.)

An idea comes to mind. A Room might have an onEntry and onExit function in its DSL, allowing for special actions to take place at those times. Darn, I really want to make a real game with this thing. Will no one rid me of this turbulent desire? Or, better yet, help with it?

Just for fun, I inline the cave room description, to see how an inlined variable description might look:

desc(
    { "You are in a low tunnel." }, {
        "You are in a low low tunnel leading into a cave. " +
                "The cave continues to the west. There is ${
                    when (facts["gateIsUnlocked"].isTrue) {
                        true -> "an unlocked gate"
                        false -> "a locked gate"
                    }
                } to the east."
    })

That’s a bit nasty. Let’s back out part of that.

val gateIsUnlocked = facts["gateIsUnlocked"]
desc(
    { "You are in a low tunnel." }, {
        "You are in a low low tunnel leading into a cave. " +
                "The cave continues to the west. There is ${
                    when (gateIsUnlocked.isTrue) {
                        true -> "an unlocked gate"
                        false -> "a locked gate"
                    }
                } to the east."
    })

Not too bad. We’ll see what option we prefer … some cases might be easier than others. It’s still pretty long. But, again, this will not be used on every room. Which is a good thing, because I don’t have a better idea just now.

In any case, Kotlin’s anonymous functions made this pretty easy to do.

See you next time!