This time around, I’ll plug the Imperative into the game, or know the reason why I couldn’t. I’m hoping for the former.

Well actually … plugging it in should be easy. Making to take over action is where I expect trouble, though I do have a possible hack in mind, which I’ll mention if and when I come to it. The initial installation will go here:

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        command.validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

We come in there with a Command. The command has an input string. The world has a Lexicon. We have a place to stand. What I don’t have is a sense of how to test this. Let me first sketch some code in. I’m at green with no changes. Perfect for rolling back if I need to.

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        command.validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

That’ll probably be harmless, but I’ll run the tests to be sure. Right, we’re good. Let’s try saving a couple of the elements of the command in member variables in the world:

        world.testVerb = imperative.verb
        world.testNoun = imperative.noun

IDEA would just love to create those for me. I’ll allow it:

class World {
    var testNoun: String
    var testVerb: String
    val lexicon = makeLexicon()

Now I can write a test to see how things are going, or even enhance existing World tests (or Room tests). Here’s an existing one:

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val player = Player(world, "clearing")
        player.command("go n")
        assertThat(world.response.nextRoomName).isEqualTo("woods")
    }

I can check the test noun and verb after the call to command.

        player.command("go n")
        assertThat(world.testVerb).isEqualTo("move")
        assertThat(world.testNoun).isEqualTo("north")
        assertThat(world.response.nextRoomName).isEqualTo("woods")

I don’t expect this to run correctly, because I suspect the test lexicon that I passed in isn’t up to snuff. But the test will tell me.

Urrk. First the test tells me to init the testVerb and testNoun. I init them to “”. Test again.

Expecting:
 <"go">
to be equal to:
 <"move">
but was not.

Perfect. Bug in the Verbs.

    private fun makeVerbs(): Verbs {
        return Verbs(mutableMapOf(
            "go" to Imperative("go", "irrelevant"),
            "east" to Imperative("go", "east"),
            "west" to Imperative("go", "west"),
            "north" to Imperative("go", "north"),
            "south" to Imperative("go", "south"),
            "say" to Imperative("say", "irrelevant"),
            "xyzzy" to Imperative("say", "xyzzy"),
        ).withDefault { (Imperative(it, "none"))})
    }

We want “move” for the verb in all of those.

            "go" to Imperative("move", "irrelevant"),
            "east" to Imperative("move", "east"),
            "west" to Imperative("move", "west"),
            "north" to Imperative("move", "north"),
            "south" to Imperative("move", "south"),

Test again. That test ran, but I had an explicit test for the lexicon that expected “go”. Changed that.

This test being green tells me that the Imperative is in place and doing its job. We could almost certainly make the existing code play the game. Let’s try it and see what words we are missing. We are starting here:

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        world.testVerb = imperative.verb
        world.testNoun = imperative.noun
        command.validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

We could almost plug the Imperative in. We can certainly pull out its verb and dispatch on it (even though when we’re done we intend just to execute the Imperative’s action lambda.) We’d have to change action to expect the Imperative rather than the command, but that’s probably easy. Let’s. I’ll change the code here and let IDEA help sort it. As soon as I do this, if the tests are good, some of the commands, like “inventory”, should trigger test fails, until we wire them into the Lexicon.

So this:

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        world.testVerb = imperative.verb
        world.testNoun = imperative.noun
        val action: (Command, World)->Unit = when(imperative.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(imperative, world)
    }

IDEA rightly informs me that I need to fix action:

        val action: (Imperative, World)->Unit = when(imperative.verb) {

Now all the methods mentioned, inventory and so on, are unhappy, because they expect a command, like this:

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world)) world.response.nextRoomName = targetName
    }
Aside
I expect that, proceeding this way, I may have to change all these methods more than once, but maybe not. We’ll see. Either way, I think this is a short path to everything working again, with the Lexicon’s Actions not yet in the loop. I’m fearful about writing those, so I’m putting it off until it’s the last remaining issue.

The idea is to do all the relatively easy bits, so that what’s left is isolated and, with luck, easy to deal with. Sometimes one does the opposite, dealing with the hard thing first. Matter of experienced choice, I guess. Anyway move gets an Imperative now:

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

This one seems easy, all we are looking for in Imperative, for now, is the noun. I’ll let IDEA show me the others that need changing. They all seem to go in fairly readily. Almost too readily.

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

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

    private 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!")
        }
    }

    private 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.")
            }
        }
    }

Test, expecting some problems. Lots of tests break. I’ll try to find an easy one, in hopes that there is a general mistake that I can correct o good general effect. Here’s one that looks easy, related to “go”:

Expecting:
 <"clearing">
to be equal to:
 <"woods">
but was not.

The test is the one we enhanced:

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val player = Player(world, "clearing")
        player.command("go n")
        assertThat(world.testVerb).isEqualTo("move")
        assertThat(world.testNoun).isEqualTo("north")
        assertThat(world.response.nextRoomName).isEqualTo("woods")
    }

I wonder what went wrong. But first a comment:

Comment
IDEA and Kotlin, in their zeal to get all my types right, required me to change a lot of methods. I guess I could have commented out the dispatches instead, but as it stands, I’ve made a number of changes and am therefore feeling a bit edgy. In addition, I forgot to commit back when I could have so my save point is further away than I’d like. No real harm done, a revert is always less hassle than one thinks.

Anyway, what’s up with move?

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

I bet I know. We’re using “north” as the primary noun for moving north, but in our moves, we refer, for convenience, to “n”:

        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }

We can fix that in Lexicon, making the synonyms go the other way. We have this:

    private fun makeSynonyms(): Synonyms {
        return Synonyms( mutableMapOf(
            "e" to "east",
            "n" to "north",
            "w" to "west",
            "s" to "south").withDefault { it }
        )
    }

We want this, to make the long terms imply the short:

    private fun makeSynonyms(): Synonyms {
        return Synonyms( mutableMapOf(
            "east" to "e",
            "north" to "n",
            "west" to "w",
            "south" to "s").withDefault { it }
        )
    }

I expect that one test, RoomsWithGo, to start working, and a few others as well. Test.

LOL. Rooms with go needed to be fixed because now I should expect noun to be “n”. Silly rabbit. However, it still fails:

Expecting:
 <"clearing">
to be equal to:
 <"woods">
but was not.

I’ll focus on this one test and get me some information:

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

I’m worrying about allowed but let’s go one step at a time.

Hm, when I try to run just that one test, ideA demurs, saying

> No tests found for given includes: [
com.ronjeffries.adventureFour.WorldTest.roomsWithGo$Adventure4_test](--tests filter)

I don’t want to look for that, I’ll just run them all, I guess. I don’t see my print. Is the dispatch broken somehow? I’m close to calling for a rollback. Just a bit more looking, we should be close.

Wait! Look at this:

        val action: (Imperative, World)->Unit = when(imperative.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }

Why did I think I needed to return “move” for the verb? Put that back? Or change the when? First, another print. Yes, we are coming in with “move” and the dispatch doesn’t have that. Impedance mismatch. Fix the verbs back as they were.

OK after again changing my expectations in the test, the move worked, Some changes of expecting “move” back to “go”. Run all the tests.

imperative verb = s

Expecting:
 <"unknown command 's none'
You find yourself in the fascinating first room.
">
to be equal to:
 <"The grate is closed!
You find yourself in the fascinating first room.
">
but was not.

I think the verbs do not know “s”, they are expecting the long words for directions.

    private fun makeVerbs(): Verbs {
        return Verbs(mutableMapOf(
            "go" to Imperative("go", "irrelevant"),
            "east" to Imperative("go", "east"),
            "west" to Imperative("go", "west"),
            "north" to Imperative("go", "north"),
            "south" to Imperative("go", "south"),
            "say" to Imperative("say", "irrelevant"),
            "xyzzy" to Imperative("say", "xyzzy"),
        ).withDefault { (Imperative(it, "none"))})
    }

Fix. I think I’ll first try just providing the short ones, so that synonyms carry the load but we also want the short words in the Imperative.

    private fun makeVerbs(): Verbs {
        return Verbs(mutableMapOf(
            "go" to Imperative("go", "irrelevant"),
            "e" to Imperative("go", "e"),
            "w" to Imperative("go", "w"),
            "n" to Imperative("go", "n"),
            "s" to Imperative("go", "s"),
            "say" to Imperative("say", "irrelevant"),
            "xyzzy" to Imperative("say", "xyzzy"),
        ).withDefault { (Imperative(it, "none"))})
    }

Test.

Expecting:
 <"unknown command 'wd40 none'
You are in an empty room in the palace. There is a padlocked door to the east.
">
to contain:
 <"unlocked"> 

We need for wd40 to be translated like “xyzzy”.

    "wd40" to Imperative("say","wd40"),

Test. This error is in my lexicon test:

Expecting:
 <"east">
to be equal to:
 <"go">
but was not.

That reads:

    @Test
    fun `world has lexicon`() {
        val world = world {}
        val lex = world.lexicon
        assertThat(lex.synonym("east")).isEqualTo("e")
        val imp = lex.translate("east")
        assertThat(imp.verb).isEqualTo("go")
        assertThat(imp.noun).isEqualTo("east")
    }

Let’s cause this to be actually correct:

    @Test
    fun `world has lexicon`() {
        val world = world {}
        val lex = world.lexicon
        assertThat(lex.synonym("east")).isEqualTo("e")
        val imp = lex.translate("e")
        assertThat(imp.verb).isEqualTo("go")
        assertThat(imp.noun).isEqualTo("e")
    }

The tests are green! I want to try the game.

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

> take keys
keys taken.
You're in a charming wellhouse
You find bottle.
You find water.

> inventory
You have keys.

You're in a charming wellhouse
You find bottle.
You find water.

> take bottle
bottle taken.
You're in a charming wellhouse
You find water.

> take water
water taken.
You're in a charming wellhouse

> s
You're in a charming clearing. There is a fence to the east.
You find cows.

> take cows
cows taken.
You're in a charming clearing. There is a fence to the east.

> inventory
You have keys, bottle, water, cows.

You're in a charming clearing. There is a fence to the east.

> e
You can't climb the fence!
You're in a charming clearing. There is a fence to the east.

The game basically works. I’ll rerun the tests and then commit: “Imperative is used to parse input commands, Actions not yet used, old dispatch still in use.”

A lovely place to stop for the afternoon.

Summary

The first two steps of plugging the Imperative in went smoothly. Running it to get the verb and noun worked fine, but, unsurprisingly, the synonyms and verbs from the tests didn’t line up with the expectations of the when dispatch that was already there. A bunch of tests broke, but were all fixed by adjusting the words in lexicon.synonyms and lexicon.verbs. That got slightly fiddly and a bit confusing, because I had written some trivial checks for what was there, and changing the lexicon to match the game’s expectations broke those checks. But all the changes were to the lexicon, which is about what we could expect.

It remains to use the Actions from the lexicon, which will have to be adapted to the game’s needs, and, I think, we’ll want to fold a pointer to the world into the Imperative, since the commands do need the world. We’ll see what the best way to do that is once we get them hooked together.

All in all, a quick change and now the new Imperative is in use in the Game. We can’t quite remove the Command, but if we are willing to pass the input string directly through, which might not be unreasonable (but a bit tacky), we can remove all the Command stuff. We re no longer using its ability to validate / parse input.

A nice result, smoothly reached … with a moment of concern where I was afraid I’d have to roll back.

See you next time!