Today’s plan: plug in the new parser. We’ll need to refine it just a bit, but I’d like to get it in play this morning. Best laid plans …

Here I am, ensconced at my custom-made zebrawood desk, overlooking the lake and the cat’s deck cage, surrounded by the chaos that is my desk, sustaining banana and granola bars by my side, iced chai at the ready, Beatles channel playing softly. I must be ready to get started.

I want to get the new parser installed into the game, and then I’d like to create a more comprehensive puzzle. But first, what did I mention yesterday that needed dealing with, and what can I see today that should be dealt with before plugging it in?

  • Command lengths less than one or greater than two.
  • Full game vocabulary.
  • Connection to game via methods or lambdas?
  • Support for words created by game designer.

Of those, I think only the first one needs solving outside the game.

Given the new “verbError” message, if we check length at the beginning of the command list and file a message at that point, it should drop through to the bottom. Let’s write a test. Probably two tests.

Post-Edit
This isn’t quite right. The “verbError” command issues “I don’t understand”. I add a new error, “countError”, as you’ll see shortly.
    @Test
    fun `too many words`() {
        val command = Command("too many words")
        val result = command
            .validate()
            .execute()
        assertThat(result).isEqualTo("I understand only one- and two-word commands, not 'too many words'.")
    }

That should fail with some other message, probably the command error one:


    @Test
    fun `too many words`() {
        val command = Command("too many words")
        val result = command
            .validate()
            .execute()
        assertThat(result).isEqualTo("I understand only one- and two-word commands, not 'too many words'.")
    }

Wrong enough. I’ll start by intention, changing this:

    fun validate(): Command{
        return this
            .makeWords()
            .goWords()
            .magicWords()
            .errorIfOnlyOneWord()
            .findOperation()
    }

To this:

    fun validate(): Command{
        return this
            .makeWords()
            .oneOrTwoWords()
            .goWords()
            .magicWords()
            .errorIfOnlyOneWord()
            .findOperation()
    }

And I’ll code oneOrTwoWords:

    fun oneOrTwoWords(): Command {
        if (words.size < 1 || words.size > 2 ){
            words.clear()
            words.add("countError")
            words.add(input)
        }
        return this
    }

I need to interpret the new command, which comes down to these:

    fun findOperation(): Command {
        val test = ::take
        operation = when (verb) {
            "take" -> ::take
            "go" -> ::go
            "say" -> ::say
            "verbError" -> ::verbError
            "countError" -> ::countError
            else -> ::commandError
        }
        return this
    }

    fun countError(noun: String): String = "I understand only one- and two-word commands, not '$noun'."

And we’re green. Commit: CommandExperiment now checks for only one- or two-word commands.

So that’s nice. Now I’d like to plug this new parsing capability into the actual game. However, I wrote it as a class, instantiated on an input string, with two main methods, validate and execute. I think it’ll be OK to use the class to produce a validated command, and then provide our own way of executing the command in the game. This is almost certainly more than we needed. Our real purpose with Command was to experiment with this way of parsing, which, honestly, I rather like.

Here’s how we do input in the game now:

    fun someoneTyped() {
        val cmd = myCommand.text
        game.command(cmd)
        myText.appendText("\n> " + cmd)
        myText.appendText("\n"+game.resultString)
        myCommand.text = ""
        myCommand.appendText("")
    }
    // Game
    fun command(cmd: String) {
        currentRoom = world.command(cmd, currentRoom)
    }
    // Room
    fun command(cmd: String, world: World) {
        val action: (String, String, World)->String = when(cmd) {
            "take axe" -> ::take
            "take bottle" -> ::take
            "take cows" -> ::take
            "inventory" -> ::inventory
            "s","e","w","n" -> ::move
            "xyzzy" -> ::move
            "cast wd40" -> ::castSpell
            else -> ::unknown
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

So … if we were to accept our cmd input and give it to an instance of the new Command class, and tell it to validate, what comes out should have verb and noun strings suitable for us to process, working out some kind of dispatch like we have here, or like we have in Command’s execute method.

Can I do a when on two items at once? Probably no need, we’ll leave it up to the verb implementations to deal with the nouns.

This is a refactoring, but it is a bit complicated. I’m on a fresh commit, so lets see how much we can do in tiny steps before we break anything. I’ll commit at each stage, except when I get all excited and forget.

First, let’s move Command over into the main side of things and make it public.

Ah! IDEA helps me with this one. Cursor on the class, Refactor / Move, select the directory and it does it. Test. Green. Commit: Move command, public, to main folder.

So that’s nice. Now that we can, in Room.command, let’s create a Command and validate, to see what happens.

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        val action: (String, String, World)->String = when(cmd) {
            "take axe" -> ::take
            "take bottle" -> ::take
            "take cows" -> ::take
            "inventory" -> ::inventory
            "s","e","w","n" -> ::move
            "xyzzy" -> ::move
            "cast wd40" -> ::castSpell
            else -> ::unknown
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

That’s easy enough. Let’s try moving dispatches out of the when(cmd), one or a few at a time. After looking for a moment, I think what I’ll do is make the else clause on the new dispatch be the existing when, if I can make that compile.

Oh nice!

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
            else -> when(cmd) {
                "take axe" -> ::take
                "take bottle" -> ::take
                "take cows" -> ::take
                "s", "e", "w", "n" -> ::move
                "xyzzy" -> ::move
                "cast wd40" -> ::castSpell
                else -> ::unknown
            }
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

This passes all the tests and plays the game. Not too surprising, unless you’re new to Kotlin and generally inept. Now I can move one thing at a time up into the first bit, make it work, then move on. I’ll try inventory:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            else -> when(cmd) {
                "take axe" -> ::take
                "take bottle" -> ::take
                "take cows" -> ::take
                "s", "e", "w", "n" -> ::move
                "xyzzy" -> ::move
                "cast wd40" -> ::castSpell
                else -> ::unknown
            }
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

Test. Tests run OK but the inventory command doesn’t show the bottle when I take it in the game. After some judicious printing, it seems that my top-level call does not recognize inventory. The bug is this:

    fun validate(): Command {
        return this
            .makeWords()
            .oneOrTwoWords()
            .goWords()
            .magicWords()
            .errorIfOnlyOneWord()
            .findOperation()
    }

The findOperation shouldn’t really be called in our new location, but it shouldn’t interfere with verb and noun. A bit more searching …

Hm. When I print c.verb and c.noun, it says:

found com.ronjeffries.adventureFour.Command@4b364ae4.verb, com.ronjeffries.adventureFour.Command@4b364ae4.noun
class Command(val input: String) {
    val words = mutableListOf<String>()
    var operation = this::commandError
    var result: String = ""
    val verb get() = words[0]
    val noun get() = words[1]

Go I need parens? I’ve not used them before.

Arrgh! The bug is that the parser doesn’t understand “inventory” and therefore does not expand it into anything. If I say “inventory xxx” it might work. And it does.

Mini-Reflection

It’s interesting how, when we’re changing things our mind gets set toward certain errors, and it takes a while to get reset to see other errors. Anyway, now I’m on the right track … except that I didn’t commit when I could have and so now I have a bit more work to do to get green. I’ll move the inventory trigger back down into the old when, and remove all my prints. That should be green and I’ll commit this time.

Back to the grind

Now I think the first thing is to put inventory into the parser. Let’s enhance its tests, do the thing right.

    fun validate(): Command {
        return this
            .makeWords()
            .oneOrTwoWords()
            .goWords()
            .magicWords()
            .singleWordCommands()
            .errorIfOnlyOneWord()
            .findOperation()
    }

    fun singleWordCommands(): Command {
        if (words.size == 2) return this
        val ignoredNounWords = listOf("inventory", "look")
        if (words[0] in ignoredNounWords) words.add("ignored")
        return this
    }

    fun inventory(noun: String): String = "Did inventory with '$noun'."

Tests run. But I’ve made kind of a rookie mistake here. In my learning project with Command, I was trying to build up a sense of how a command understanding object could be built with that series of steps passing the command along. But in so doing, and this may or may not have been appropriate, I included semantic knowledge of specific words that needed to be transformed, like “east” into “go east” and so on. The result is that if the parser doesn’t understand a particular word, it doesn’t pass it on through. A parser shouldn’t do that sort of thing. It should ensure correct grammar (but what’s that to us, one or two words in a row), and then the next phase should deal with meaning.

So my object has meaning entangled with parsing. That’s going to lead to more confusion.

Right now, I don’t want to address that. I’m in the mode of making my new parser fit into the game and by god I’ll have that or know the reason why. This is a poor decision, but with luck, not a terrible one. At least the problem is isolated to a couple of classes, and there is at least some sense to the notion that the Command thing rewrites part of the input to a preferred form.

We’ll see. I put us all on notice that what I’m going is probably not ideal.

Be that as it may, I think I can probably move inventory up again. But first, I’m green, let’s commit: Handle single word commands by appending noun “ignored”.

Now let me try moving inventory up again. But let’s ask ourselves, selves, why did no test fail when I did it last time?

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            else -> when(cmd) {
                "take axe" -> ::take
                "take bottle" -> ::take
                "take cows" -> ::take
                "s", "e", "w", "n" -> ::move
                "xyzzy" -> ::move
                "cast wd40" -> ::castSpell
                else -> ::unknown
            }
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

This works. But I think I want a room test for it.

    @Test
    fun `can take inventory`() {
        val world = world {
            room("storage") {
                desc("storage room", "large storage room")
                item("broom")
            }
        }
        val game = Game(world, "storage")
        game.command("take broom")
        assertThat(game.resultString).isEqualTo("broom taken.\nlarge storage room\n")
        game.command("inventory")
        assertThat(game.resultString).isEqualTo("You have broom.\n\nlarge storage room\n")
    }

Test runs. Commit: Added test for inventory to RoomTests.

Lot of distractions this morning. Not really helpful. Throws me off my game, if I was ever on it. Let’s see if there is something else I can move up to the new parsing area:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            else -> when(cmd) {
                "take axe" -> ::take
                "take bottle" -> ::take
                "take cows" -> ::take
                "take broom" -> ::take
                "s", "e", "w", "n" -> ::move
                "xyzzy" -> ::move
                "cast wd40" -> ::castSpell
                else -> ::unknown
            }
        }
        val name = action(cmd, cmd, world)
        world.response.nextRoomName = name
    }

Most of these will require small changes, I think. One issue is that in the old scheme we pass the cmd to both parameters of the method and in the new scheme we want to pass noun and verb. Another issue is that we have a go command and say command and such. I think I should have returned a “failed” flag from the top when, and invoked the bottom one if it failed. Maybe I can set a flag. It’ll be grotesque but I should be able to get rid of it.

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            else -> {
                firstOK = false
                when (cmd) {
                    "take axe" -> ::take
                    "take bottle" -> ::take
                    "take cows" -> ::take
                    "take broom" -> ::take
                    "s", "e", "w", "n" -> ::move
                    "xyzzy" -> ::move
                    "cast wd40" -> ::castSpell
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

I’m doing all this because I want to take tiny steps, never breaking anything, changing only a few lines at a time, then back to green. When I take larger steps, I wind up with more debugging and I don’t like that. Things go more smoothly, and ultimately faster, this way, and I feel more confident and less tense.

I’m green: Commit: hack to pass verb/noun to functions recognized by new command.

Now I can move something up. I think I’ll try take. They’re all going to be a bit weird.

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            else -> {
                firstOK = false
                when (cmd) {
                    "s", "e", "w", "n" -> ::move
                    "xyzzy" -> ::move
                    "cast wd40" -> ::castSpell
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

Here’s the current take:

    private fun take(verb: String, noun: String, world: World): String {
        // interim hackery waiting for new parser
        val words = noun.split(" ")
        val realNoun = words.last()
        val done = contents.remove(realNoun)
        if ( done ) {
            world.addToInventory(realNoun)
            world.response.say("$realNoun taken.")
        } else {
            world.response.say("I see no $realNoun here!")
        }
        return roomName
    }

Now I can unwind my hack:

    private fun take(verb: String, noun: String, world: World): String {
        val done = contents.remove(noun)
        if ( done ) {
            world.addToInventory(noun)
            world.response.say("$noun taken.")
        } else {
            world.response.say("I see no $noun here!")
        }
        return roomName
    }

I have high hopes for this. Tests run, game runs. Commit: take done via new command.

Now, from this:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            else -> {
                firstOK = false
                when (cmd) {
                    "s", "e", "w", "n" -> ::move
                    "xyzzy" -> ::move
                    "cast wd40" -> ::castSpell
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

To this:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            "go" -> ::move
            else -> {
                firstOK = false
                when (cmd) {
                    "xyzzy" -> ::move
                    "cast wd40" -> ::castSpell
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

I don’t mind telling you that I know that in years gone by, I’d have set out to do this thing all at once, and I can assure you that I’d have made it work. this slow and steady way is less exciting, but it’s also less frustrating, very little can go wrong, when something does go wrong it’s almost always just one thing, and I’m sure that I am faster from beginning to end.

Where was I? Oh, right, fixing move.

    fun move(verb: String, noun: String, world: World): String {
        val (targetName, allowed) = moves.getValue(verb)
        return if (allowed(world))
            targetName
        else
            roomName
    }

Since we’re now coming in on “go” “e” and the like, we can probably use the noun instead of the verb here.

    fun move(verb: String, noun: String, world: World): String {
        val (targetName, allowed) = moves.getValue(noun)
        return if (allowed(world))
            targetName
        else
            roomName
    }

We’re green. Let’s rename that method to go from move. We’ll be glad of that some day soon.

The game runs, but a test fails.

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

The test is:

        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val clearing:Room = world.unsafeRoomNamed("clearing")
        val newLocName:String = clearing.go("n", "", world)
        assertThat(newLocName).isEqualTo("woods")

Right, that’s going thru the back door. Change it:

        val newLocName:String = clearing.go("go", "n", world)

Works. Commit: go done with new command. What’s next?

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            "go" -> this::go
            else -> {
                firstOK = false
                when (cmd) {
                    "xyzzy" -> this::go
                    "cast wd40" -> ::castSpell
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

OK, these remaining two are odd. The new command converts these to “say”, not to “cast”, and it thinks that xyzzy is a command, not a direction. Let’s change “xyzzy” to be accepted as a direction for now:

    fun goWords(): Command {
        val directions = listOf(
            "xyzzy",
            "n","e","s","w","north","east","south","west",
            "nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
            "up","dn","down")
        return substituteSingle("go", directions)
    }

I think that should still pass all the tests. No, not quite.

Expecting:
 <"went xyzzy.">
to be equal to:
 <"said xyzzy.">
but was not.

Let’s put that back and go the other way, converting xyzzy to a magic word.

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            "go" -> this::go
            "say" -> ::castSpell
            else -> {
                firstOK = false
                when (cmd) {
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

Now I’ll have to do some work in castSpell:

    private fun castSpell(noun: String, verb: String, world: World): String {
        world.flags.get("unlocked").set(true)
        world.response.say("The magic wd40 works! The padlock is unlocked!")
        return roomName
    }

OK, clearly we have to break out the various words here. OK … This change is a bit large, and as I might have predicted, I’m having a bit of trouble with it. Here’s what I’ve got:

    private fun castSpell(verb: String, noun: String, world: World): String {
        val returnRoom = when (noun) {
            "wd40" -> {
                world.flags.get("unlocked").set(true)
                world.response.say("The magic wd40 works! The padlock is unlocked!")
                roomName
            }
            "xyzzy" -> {
                val (targetName, allowed) = moves.getValue(noun)
                return if (allowed(world))
                    targetName
                else
                    roomName
            }
            else -> {
                world.response.say("Nothing happens here.")
                roomName
            }
        }
        return returnRoom
    }

My error (and I think I know what it is) is:

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

Interesting that “unoknown”. We’ll have to see who said that. But the bug is that wd40 is not a known magic word in the parser. Add it:

    fun magicWords(): Command {
        val magicWords = listOf("xyzzy", "plugh", "wd40")
        return substituteSingle("say", magicWords)
    }

Expect success. Same error. Interesting. Let’s find out who said “unoknown”, just to start.

That’s in room. Oh, I bet I know what happened. Where do we say “wd40”? Ah.

        val game = Game(world,"palace")
        game.command("e")
        assertThat(game.resultString).isEqualTo("The room is locked by a glowing lock!\n" +
                "You are in an empty room in the palace. There is a padlocked door to the east.\n")
        game.command("cast wd40")
        assertThat(game.resultString).contains("unlocked")
        game.command("e")
        assertThat(game.resultString).contains("rich with gold")

We need to change that, for now, to “say”. And we need to deal with synonyms in our verbs. First fix this. I can just say wd40 now that it is a known magic word.

        game.command("wd40")

Green. Commit: All commands now going through new Command object.

I have 8 warnings, and I’m not sure how best to get rid of them. We’ll talk about that in a moment. Now we can remove our else thingie and the flag hack, from command:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        var firstOK = true
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            "go" -> this::go
            "say" -> ::castSpell
            else -> {
                firstOK = false
                when (cmd) {
                    else -> ::unknown
                }
            }
        }
        val name:String = when (firstOK) {
            true-> action(c.verb, c.noun, world)
            false->action(cmd, cmd, world)
        }
        world.response.nextRoomName = name
    }

That becomes:

    fun command(cmd: String, world: World) {
        val c = Command(cmd).validate()
        val action: (String, String, World)->String = when(c.verb) {
            "inventory" -> ::inventory
            "take" -> ::take
            "go" -> this::go
            "say" -> ::castSpell
            else -> ::unknown
        }
        val name:String = action(c.verb, c.noun, world)
        world.response.nextRoomName = name
    }

Green, game runs. Commit: Old command logic removed. Here’s a sample run of the game:

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

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

> 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.

> n
You're in a charming wellhouse
You find keys.
You find water.

> inventory
You have bottle, cows.

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

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

> inventory
You have bottle, cows, keys.

You're in a charming wellhouse
You find water.

I call that good. time for a break. Let’s sum up.

Summary

This has gone quite nicely. There were a couple of points of brief confusion, but in each case, either the problem had to be in the code I’d just written, or a disagreement between the old parsing and the new parsing. In most cases, I quickly found the difficulty. Had I tried two changes at once, there would have been more places to look, more possible causes of problems, and more mental balls in my head to juggle.

For me, these tiny steps are a solid strategy, and result in lots of happy green bars.

I need to be better about committing on each and every green bar. I wonder whether there’s a way to do that automatically …

I do have a lot of warnings. Most of them are in the command methods, saying that noun or verb is never used. Kotlin doesn’t allow me to cal parameters _ in methods, so other than a @suppress or whatever it is, i can’t really make those go away easily. There are a couple on lambdas, that I will allow it to rename to _:

    val moves = mutableMapOf<String,GoTarget>().withDefault { Pair(roomName, { _:World->true}) }

    fun go(direction: String, roomName: String, allowed: (World)->Boolean = { _:World -> true}) {
        moves += direction to Pair(roomName, allowed)
    }

The others, I guess I’m stuck with them. Weird.

Let’s see if I have the brain power left to remember some issues:

  • Need better handling of vocabulary vs parsing.
  • Command is doing too much, most likely.
  • Need to deal better with synonyms, “say” vs “case”, “e” vs “east”.
  • Decide whether I want to jump through hoops get get rid of those warnings.

That’s all, my brain is empty now. Anyway … time to rest. I can’t even predict what I’ll do next time. I look forward to finding out, and I hope you’ll join me!