Let’s do some very simple parsing.

I wrote this trivial test to learn just enough pattern matching:

class ParsingTest {
    @Test
    fun `pattern splitter`() {
        val regex = Regex("\\W+")
        val words = "take axe".split(regex)
        assertThat(words.size).isEqualTo(2)
        assertThat(words[0]).isEqualTo("take")
        assertThat(words[1]).isEqualTo("axe")
    }
}

That works, and I think it’s nearly enough to do the job. If you type weird stuff, it just won’t work.

Now what to actually do. I figure that we’ll always have the command word first, and the object, if any, last. And with a two-word sentence, there’s only a first and last. We’ll probably have synonyms like east for e (or vice versa), and maybe we’ll do something for “go” to allow “go home” or something. That can come in due time.

Oh, and “say xyzzy” for “xyzzy”. Easy enough, I think.

I’m planning for a truly simple approach, no lexing and grammaring here. I’ll write some more tests to get a feeling for it. Should we have some kind of command object? Or just dispatch off to do things based on the words? The latter seems like a good place to start.

I decide to extend String as my next clever move:

    @Test
    fun `identify a few commands`() {
        val words = "take axe".parse()
        assertThat(words.size).isEqualTo(2)
    }

private fun String.parse(): List<String> {
    return this.split(Regex("\\W+"))
}

Test runs. So that’s interesting. What does our current command stuff look like?

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

I think I’ll just create a little function command in my test file and see how to shape it to do good things. Sort of a parsing Spike, which is pretty much what I’ve already been doing.

    @Test
    fun `some commands`() {
        var result:String
        result = command("e")
        assertThat(result).isEqualTo("move e")
    }

That demands command, of course. I’ll try this:

private fun command(cmd:String): String {
    val words = cmd.parse()
    if (words.size == 0) return "no command found"
    val verb = words[0]
    var noun = ""
    println("Size ${words.size}")
    if (words.size > 1)
        noun = words[1]
    return when(verb) {
        "e" -> move("e")
        else -> "I don't understand $cmd.\n"
    }
}

private fun move(dir: String): String = "move $dir"

So that works. Let’s extend the test a bit.

    @Test
    fun `some commands`() {
        var result:String
        result = command("e")
        assertThat(result).isEqualTo("move e")
        result = command("sw")
        assertThat(result).isEqualTo("move sw")
    }

And excuse me for this:

private fun command(cmd:String): String {
    val words = cmd.parse()
    if (words.size == 0) return "no command found"
    val verb = words[0]
    var noun = ""
    println("Size ${words.size}")
    if (words.size > 1)
        noun = words[1]
    return when(verb) {
        "n","s","e","w" -> move(verb)
        "nw","sw","ne","se" -> move(verb)
        "up","dn","down" -> move(verb)
        else -> "I don't understand $cmd.\n"
    }
}

Now it would be nice if instead of, when you type “ns” meaning “nw”, you got better than “I don’t understand ns.” Do you get that? Let’s make sure:

    @Test
    fun `some commands`() {
        var result:String
        result = command("e")
        assertThat(result).isEqualTo("move e")
        result = command("sw")
        assertThat(result).isEqualTo("move sw")
        result = command("ns")
        assertThat(result).isEqualTo("I don't understand ns.\n")
    }

Still green. Let’s work on take:

I think we should break out size 1 and size 2, so I’ll include that in my change here.

        result = command("take axe")
        assertThat(result).isEqualTo("Taken: axe")

And I code:

private fun command(cmd:String): String {
    val words = cmd.parse()
    if (words.size == 0) return "no command found"
    val verb = words[0]
    var noun = ""
    return if (words.size == 1)
        oneWord(words[0])
    else if (words.size == 2)
        twoWords(words[0], words[1])
    else
        "Too many words!"
}

fun oneWord(verb: String): String {
    return when(verb) {
        "n","s","e","w" -> move(verb)
        "nw","sw","ne","se" -> move(verb)
        "up","dn","down" -> move(verb)
        else -> "I don't understand $verb.\n"
    }
}

fun twoWords(verb: String, noun: String): String {
    return when(verb) {
        "take","get" -> take(noun)
        else -> "I don't understand $verb $noun.\n"
    }
}

private fun move(dir: String): String = "move $dir"

fun take(noun:String): String = "Taken: $noun"

I hope it’s obvious here that what I’m doing is basically Spiking a method of interpreting simple commands of verb, or verb-noun form. I’m just moving the code around until it seems like something I could live with.

And we’re getting there. Let’s Reflect and Sum Up, because I’m tired of fiddling.

Reflect / Sum Up

Well, this approach to parsing is extremely ad-hoc, but it seems promising. I can sort of see how and where we can look up the parsed word in a synonym map, to get the definitive word, rather than typing “take”,”get” in the when statement, so that shouldn’t be difficult.

What I don’t see, yet, is how to deal with this thing that irritates me a bit: as implemented now, each command function has to receive not just the command word, but a world instance. I just find that tedious to type, so I wish there were some clever way to do that just once. Perhaps there’s a way to return the name of the function? Hmm, I think there is. Something like World::fun? I’ll look it up.

And I think that the rule should be that the commands say things, and if they want to change the room, they tell world to do it rather than the current scam of always needing to return the room name, which I generally forget.

And obviously I need to get more clever about periods and newlines. It’s too fiddly to think about those while thinking about larger matters. My tiny brain just can’t manage all that.

Next time, I’ll see about enhancing this Spike to show me how to better do those things. I hope you’ll join me!