Let’s see about cleaning up that expand method.

The method in question looks like this:

    fun expandIfNeeded(): Command {
        if (words.size == 2) return this
        val directions = listOf(
            "n","e","s","w","north","east","south","west",
            "nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
            "up","dn","down")
        val magicWords = listOf("xyzzy", "plugh")
        val word = words[0]
        if (word in directions) {
            words.add(0,"go")
        } else if (word in magicWords) {
            words.add(0,"say")
        } else {
            words[0] = "verbError"
            words.add(word)
        }
        return this
    }

It seems to me that we can do that operation with a series. Our convention is that we check for one-word possibilities when there is only one word. (We’re not dealing with more than two just now.) This suggests to me that we should be able to do a series of calls, goWords, magicWords, errorIfOnlyOneWord or something like that. If we prefix each of those with a check for number of words, the second one and third will skip if a prior one has expanded the command to two words.

Let’s try it. I’ll start by adding those two calls to the list and making them just return this:

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

And …

    fun goWords(): Command {
        return this
    }

    fun magicWords(): Command {
        return this
    }

Tests remain green. Now let’s move the go words into the goWords method:

    fun goWords(): Command {
        if (words.size == 2) return this
        val directions = listOf(
            "n","e","s","w","north","east","south","west",
            "nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
            "up","dn","down")
        if (words[0] in directions) {
            words.add(0,"go")
        return this
    }

And trim the expand method:

    fun expandIfNeeded(): Command {
        if (words.size == 2) return this
        val magicWords = listOf("xyzzy", "plugh")
        val word = words[0]
        if (word in magicWords) {
            words.add(0,"say")
        } else {
            words[0] = "verbError"
            words.add(word)
        }
        return this
    }

Green. Now let’s move the magic words to that method:

    fun magicWords(): Command {
        if (words.size == 2) return this
        val magicWords = listOf("xyzzy", "plugh")
        val word = words[0]
        if (word in magicWords) {
            words.add(0,"say")
        }
        return this
    }

And eviscerate the expand:

    fun expandIfNeeded(): Command {
        if (words.size == 2) return this
        val word = words[0]
        words[0] = "verbError"
        words.add(word)
        return this
    }

I expect green. I am dismayed:

Expecting:
 <"verbError">
to be equal to:
 <"say">
but was not.

I do ish I had committed a while back. Remind me to commit every time I’m green, OK?

Ah, the test is wrong:

    @Test
    fun `expand magic`() {
        val command = Command("xyzzy")
        command.makeWords()
        assertThat(command.words.size).isEqualTo(1)
        command.expandIfNeeded()
        assertThat(command.words.size).isEqualTo(2)
        assertThat(command.words[0]).isEqualTo("say")
        assertThat(command.words[1]).isEqualTo("xyzzy")
        command.makeVerbNoun()
        assertThat(command.verb).isEqualTo("say")
        assertThat(command.noun).isEqualTo("xyzzy")
    }

Should use our new method:

    @Test
    fun `expand magic`() {
        val command = Command("xyzzy")
        command.makeWords()
        assertThat(command.words.size).isEqualTo(1)
        command.magicWords()
        assertThat(command.words.size).isEqualTo(2)
        assertThat(command.words[0]).isEqualTo("say")
        assertThat(command.words[1]).isEqualTo("xyzzy")
        command.makeVerbNoun()
        assertThat(command.verb).isEqualTo("say")
        assertThat(command.noun).isEqualTo("xyzzy")
    }

Yes. That’s better. Let’s smooth these methods out and rename the last one.

    fun expandIfNeeded(): Command {
        if (words.size == 2) return this
        val word = words[0]
        words[0] = "verbError"
        words.add(word)
        return this
    }

That would be better this way:

    fun expandIfNeeded(): Command {
        if (words.size == 2) return this
        words.add(0,"verbError")
        return this
    }

And the rename:

    fun errorIfOnlyOneWord(): Command {
        if (words.size == 2) return this
        words.add(0,"verbError")
        return this
    }

And this:

    fun magicWords(): Command {
        if (words.size == 2) return this
        val magicWords = listOf("xyzzy", "plugh")
        val word = words[0]
        if (word in magicWords) {
            words.add(0,"say")
        }
        return this
    }

Can become this:

    fun magicWords(): Command {
        if (words.size == 2) return this
        val magicWords = listOf("xyzzy", "plugh")
        if (words[0] in magicWords) {
            words.add(0,"say")
        }
        return this
    }

I tried to induce IDEA to inline that temp variable but nowhere that I clicked would do it. Disappointing. We are green. Commit: improve CommandExperiment parsing.

I notice some duplication:

    fun goWords(): Command {
        if (words.size == 2) return this
        val directions = listOf(
            "n","e","s","w","north","east","south","west",
            "nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
            "up","dn","down")
        if (words[0] in directions) {
            words.add(0, "go")
        }
        return this
    }

    fun magicWords(): Command {
        if (words.size == 2) return this
        val magicWords = listOf("xyzzy", "plugh")
        if (words[0] in magicWords) {
            words.add(0,"say")
        }
        return this
    }

We ought to be able to extract a method for this, passing in the lists. I bet I can’t convince IDEA to do it for me but I’ll try.

I can see no way to do this. I’ll just code it. New method substituteSingle.

    fun substituteSingle(sub: String, singles: List<String>): Command {
        if (words.size == 2) return this
        if (words[0] in singles) words.add(0, sub)
        return this
    }

And use it:

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

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

That works a treat and gets rid of that duplication. Commit: refactor to reduce duplication.

What else? This looks kind of marginal:

    fun makeVerbNoun(): Command {
        verb = words[0]
        noun = words[1]
        return this
    }

It always happens, and we always have two words by the end. So let’s remove that method and make verb and noun into properties.

private 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]

Green. Commit: removed unneeded makeVerbNoun method.

So, we have a nice little parsing thing that seems to do most of what I need. It looks like this:

private 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]

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

    fun makeWords(): Command {
        words += input.split(" ")
        return this
    }

    fun errorIfOnlyOneWord(): Command {
        if (words.size == 2) return this
        words.add(0,"verbError")
        return this
    }

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

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

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

    fun substituteSingle(sub: String, singles: List<String>): Command {
        if (words.size == 2) return this
        if (words[0] in singles) words.add(0, sub)
        return this
    }

    // execution

    fun execute(): String {
        result = operation(noun)
        return result
    }

    fun commandError(noun: String) : String = "command error '$input'."
    fun go(noun: String): String = "went $noun."
    fun say(noun:String): String = "said $noun."
    fun take(noun:String): String = "$noun taken."
    fun verbError(noun: String): String = "I don't understand $noun."
}

Nearly good, not bad for an experiment. Probably could be better, and certainly could be more general. But it’s pretty solid for what we need, with at least one exception: it doesn’t deal with words table length other than 1 and 2. We can deal with that next time. Oh, and the only operations it really knows are take, say, and go. So it would need extension to all the words the game knows. And that probably doesn’t want to be coded like this:

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

Well, we might do it that way. There’s a design decision to be made, whether we have a method per command, or a table of functions per command, or what.

Oh and another thing, we may need to add new words in the room definitions. I won’t really want to have to program what “plugh” does. Instead the DSL for a room should specify the word and what it does, where it does it, and what to say when it isn’t going to work.

That’ll be interesting …

Anyway, a nice experiment and a decent-looking bit of code. I’ll call it a day. See you tomorrow!