GitHub Repository

Let’s look at the Actions in the new Imperative. Advice from my betters suggests I could do, well, better.

I should mention before we begin that I put the starting time at the top of my articles, so that I can check how long I spend on them. This one started at 1337, which, if I’m not mistaken, is a very propitious time1.

Last night, when we were reviewing my code, GeePaw Hill was troubled by the Action lambdas, which look like this:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            "go" to { imp: Imperative -> imp.room.move(imp, imp.world) },
            "say" to { imp: Imperative -> imp.room.castSpell(imp, imp.world) },
            "take" to { imp: Imperative -> imp.room.take(imp, imp.world) },
            "inventory" to { imp: Imperative -> imp.room.inventory(imp, imp.world) },
        ).withDefault {
            { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        }
        )
    }

I’m not entirely sure that I understand his concern, because to tell you the truth, in all the excitement I sort of lost track myself2, but I think he felt that the lambdas should be doing more of the work, not just calling methods on the room. As it happens, if that was his point, I agree with him, and part of this whole Imperative exercise was aimed at that issue.

Remember that this whole thread started with learning to build a DSL, and I picked a text game like ADVENTure or Zork! because it wouldn’t mix in learning graphics, which I’m saving for later. It seems that the more an imaginary game designer could do in the DSL, the better things would be. It would be a shame if, every time they get a neat new game idea, they had to come back to the Kotlin Programming Department to get some new feature added. Right now, they’d have to do that, because almost everything the game can do is a method, typically on Room:

    val castSpell = { imperative: Imperative, world: World ->
        when (imperative.noun) {
            "wd40" -> {
                world.flags.get("unlocked").set(true)
                world.response.say("The magic wd40 works! The padlock is unlocked!")
            }
            "xyzzy" -> { move(imperative, world) }
            else -> { world.response.say("Nothing happens here.") }
        }
    }

    val inventory = { _:Imperative, world:World ->
        world.showInventory()
    }

    val move = { imperative: Imperative, world: World ->
        val (targetName, allowed) = moves.getValue(imperative.noun)
        if (allowed(world)) world.response.nextRoomName = targetName
    }

    val take = { imperative: Imperative, world: World ->
        val done = contents.remove(imperative.noun)
        if ( done ) {
            world.addToInventory(imperative.noun)
            world.response.say("${imperative.noun} taken.")
        } else {
            world.response.say("I see no ${imperative.noun} here!")
        }
    }

    val unknown = { imperative: Imperative, world: World ->
        println("unknown")
        world.response.say("unknown command '${imperative.verb} ${imperative.noun}'")
    }

Right now, all the Imperative’s Action lambdas just dispatch off to one or another of the methods above, and the work gets done. But now, with the Imperative having access to the current world and room, we should be able to, first, write out a command’s meaning in the Action lambda, and, second, provide a way in the DSL to add to the Actions and other elements of the Lexicon, in effect adding new words.

Since the action table is indexed by the command’s verb, it should be the case that we can imagine a new verb, and by adding a new clause to the Actions, cause it to do what it does. I want to start on that today. Let’s first imagine a new command, and see whether we can implement it just by adding something to the Actions.

I have an idea for a command. “Shout”. If you type “Shout” and any subsequent word, the game should say “Your shout of $word echoes through the area.” Maybe it should even upper-case the word.

Let’s try just putting a new action in:

    "shout" to { imp: Imperative -> imp.world.response.say(
        "Your shout of ${imp.noun.uppercase()} echoes through the area.")},

This works well in the game:

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> shout hello
Your shout of HELLO echoes through the area.
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

Sweet. This sort of thing would be easier if the Imperative understood the keywords like say directly, forwarding them on to the world or whoever deals with them. Clearly there will be some set of “primitives” that the code understands, out of which the game designer can assemble their actions.

So let’s put say onto Imperative. It seems to me that we might do this by adding methods inside the class, or via Kotlin’s extension capability. I see no reason to do the more complicated thing, so I’ll just add it to Imperative.

There’s a bit of an issue here. Imperative already has a secret say method, used in testing. Let’s rename that to testingSay. That done, we can build Imperative.say and use it:

data class Imperative ...
    fun say(s: String) = world.say(s)

    "shout" to { imp: Imperative -> imp.say(
        "Your shout of ${imp.noun.uppercase()} echoes through the area.")},

That works fine. Let’s add a test for our new shout command, just to make it official.

    @Test
    fun `shouts echo`() {
        val world = world {
            room("woods") {
                desc("You are in the woods.", "You are in the dark woods.")
            }
        }
        val room = world.unsafeRoomNamed("woods")
        val response = GameResponse()
        val command = Command("shout hello")
        world.command(command, room, response)
        assertThat(response.resultString).contains("Your shout of HELLO echoes through the area.")
    }

Test runs. We are green. Commit: New command “shout” implemented entirely in Action lambda, using new imperative primitive “say”.

Just for the fun of it, let’s see whether we can do another Action all in the lambda. Inventory seems easy and we could then remove the method.

    "inventory" to { imp: Imperative -> imp.world.showInventory() },

That runs green with the room method removed. Sweet. Commit: inventory now done in Action lambda.

It seems to me that these two examples are a good proof of concept for defining Actions directly in the Lexicon’s Action lambdas. I do think that we’ll need to think of something special to do about the castSpell method, which is already pretty complicated. We’ll see. In the next few articles, I propose to get everything the game does defined in lambdas, and in the DSL, unless it turns out to be impossible, which seems quite unlikely.

For now, a nice little improvement. See you next time!



  1. If you know, you know. 

  2. If you know, you know. Again.