Kotlin 37
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!