A bit more work with the new Imperative. In reviewing the code, I’m not sure what else it needs to do the job. After a bit of testing and enhancing the tables, I’ll think about whether and how to use it.

What I like about the Imperative and associated classes is that it has turned out to be quite neat and tidy. Nearly every method, other than one or two that are still not table-driven, has come down to one line of code. I find that quite nice.

Early DIgression
I want to address again why I’m doing this. I think it’s fair to say that the main reason is that I can, and I enjoy it. I enjoy learning a new system, at least after I get past those early days when you have no idea what’s going on, I enjoy crafting code neatly, and I enjoy writing about it. That’s why I don’t really care too much whether there’s anyone out there reading it. I certainly hope there is, because I think there’s value in how I think and work, but honestly, whether this is a concert, or just practice, I enjoy playing.

But I do hope people are reading and enjoying, and it’s wonderful when I hear from them.

What’s Right?1

The basic idea behind yesterday’s work is to drive the program’s parsing and execution entirely from tables that can be provided by code written, not in free Kotlin, but in our DSL, our little language for describing a game world. I feel quite confident about that aspect, and it’s because I’ve gained enough experience with what’s done so far to be confident that the rest is possible. Basically, once you can store a function in a table and look it up, you can do anything.

I’m less sanguine2 about whether the current objects in the ImperativeSpike can do everything we need. We have the Command code, a different style of parsing, and it could be made to be table driven. On the gripping hand3, there’s always the likelihood that we can get to our table-driven solution by merging the two ideas, adopting some good from each.

I’m feeling certain that we can handle the synonyms and the operation lookup with what we have, but I’m feeling a bit uncertain about handling certain cases. It may be that the code is right and that I don’t see it, or it may need improvement. And, of course, if it is right and I don’t see it, it needs improvement so that I do see it

We’ll do some tests and see what we need to add to make them work. We’ll use tests that go all the way to our current actions. Here goes.

Here are a couple of tests that I’ll use as templates for new ones.

    @Test
    fun `imperative can act`() {
        val imperatives = ImperativeFactory(VerbTranslator(imperativeTable))
        var imp: Imperative = imperatives.create("east")
        assertThat(imp.act()).isEqualTo("went east")
        imp = imperatives.create("e")
        assertThat(imp.act()).isEqualTo("went east")
        imp = Imperative("forge", "sword")
        assertThat(imp.act()).isEqualTo("I can't forge a sword")
    }

    @Test
    fun `two word legal command`() {
        val imperatives = ImperativeFactory(imperativeTable)
        var imp = imperatives.create("go", "e")
        assertThat(imp.act()).isEqualTo("went east")
    }

I think that the official way to use this thing will be to find your one or two words and send them to the ImperativeFactory’s create method. (We might find it fun and useful to use Kotlin’s varargs capability for that.)

I could try to write down my concerns, but they are vague and ill-formed. The tests that I write will show you, and me, exactly what I’m concerned about.

I’m going to start with this test. I’ll make it longer, later, but this, I think, is enough to cause me to need to fill in the tables a bit:

    @Test
    fun `does west work as well as east`() {
        val imperatives = ImperativeFactory(imperativeTable)
        var imp = imperatives.create("go", "w")
        assertThat(imp.act()).isEqualTo("went west")
    }

Test. Fail as expected:

Expecting:
 <"went w">
to be equal to:
 <"went west">
but was not.

Extend the synonym table:

val synonymTable = mapOf(
    "e" to "east", 
    "n" to "north",
    "w" to "west",
    "s" to "south").withDefault { it }

Test again, expecting success. Joy. Extend test:

        imp = imperatives.create("w")
        assertThat(imp.act()).isEqualTo("went west")

Test, with hope. Fail.

Expecting:
 <"I can't none a none">
to be equal to:
 <"went west">
but was not.

Ah, another table entry, I hope, from here:

val imperativeTable = mapOf(
    "east" to Imperative("go","east"),
    "go" to Imperative("go", "irrelevant")
    ).withDefault { (Imperative("none", "none"))
}

OK, not a great name for this table but we can see what has to happen, probably …

val imperativeTable = mapOf(
    "east" to Imperative("go","east"),
    "west" to Imperative("go","west"),
    "north" to Imperative("go","north"),
    "south" to Imperative("go","south"),
    "go" to Imperative("go", "irrelevant"),
    ).withDefault { (Imperative("none", "none"))
}

If all we have to do in response to today’s tests is add things to the tables, I’ll be happy. If we find something we want to say that can’t be made to work by adding to the tables … well, I’ll be happy in a different way, because I’ll have learned something and will have an opportunity to solve a problem. Win-win, if you hold your mind at just the right angle.

Test. Green! This is looking better. My remaining concerns are about words like “xyzzy” that should turn into “say xyzzy” and words like “inventory” or “look”, which remain as verbs with no noun (or an irrelevant one, which I think is what we’re doing in the table above). First, xyzzy.

        imp = imperatives.create("xyzzy")
        assertThat(imp.act()).isEqualTo("said xyzzy")

This is going to need table entries but also an action improvement. Test.

Expecting:
 <"I can't none a none">
to be equal to:
 <"said xyzzy">
but was not.

Improve this table:

    "say" to Imperative("say", "irrelevant"),
    "xyzzy" to Imperative("say", "xyzzy"),

Kotlin is very civilized, and lets you put a comma after the last element of a list of items. H/T to Kotlin. Test, expecting the verb “say” not to be understood.

Expecting:
 <"I can't say a xyzzy">
to be equal to:
 <"said xyzzy">
but was not.

Here we need to add the action to this code. We don’t have that broken out into a table yet.

data class Imperative(val verb: String, val noun: String) {
    fun act():String {
        val action:(Imperative) -> String = when(verb) {
            "go" -> { i -> "went $noun" }
            else -> { i -> "I can't $verb a $noun"}
        }
        return action(this)
    }
}

Easy addition:

            "say" -> {i -> "said $noun"}

Test expecting green. Yes. Now to test the explicit “say” and then observe that you can say anything.

        imp = imperatives.create("say", "xyzzy")
        assertThat(imp.act()).isEqualTo("said xyzzy")

Expect green. Receive green. Add:

        imp = imperatives.create("say", "unknownWord")
        assertThat(imp.act()).isEqualTo("said unknownWord")

That’s green. Now this should error, but I’m not clear on just how:

        imp = imperatives.create("unknownWord")
        assertThat(imp.act()).isEqualTo("what does it do?")

Testing tells me:

Expecting:
 <"I can't none a none">
to be equal to:
 <"what does it do?">
but was not.

OK, there’s the first thing that I don’t like. When we provide an unknown verb, it comes down to that message. What about an unknown verb and a noun?

        imp = imperatives.create("unknownWord", "unknownNoun")
        assertThat(imp.act()).isEqualTo("what does ?? do?")

Test to find out. This form is a bit better:

Expecting:
 <"I can't none a unknownNoun">
to be equal to:
 <"what does ?? do?">
but was not.

Where is the “none” coming from, and where does it go from there?

This is good: it’s the first thing that isn’t turning out quite like we’d like. It’s an opportunity to improve our scheme. Cool!

val imperativeTable = mapOf(
    "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("none", "none"))
}

I think we can include the actual word that doesn’t match, like this:

    ).withDefault { (Imperative(it, "none"))

Right. The default is a lambda, not just a constant value, so we can refer to it, which is the key we came in with, unknownWord in this case. Now we get this error:

Expecting:
 <"I can't unknownWord a unknownNoun">
to be equal to:
 <"what does ?? do?">
but was not.

Let me improve the test to pass, because we do want it to pass.

        imp = imperatives.create("say", "unknownWord")
        assertThat(imp.act()).isEqualTo("said unknownWord")
        imp = imperatives.create("unknownVerb", "unknownNoun")
        assertThat(imp.act()).isEqualTo("I can't unknownVerb a unknownNoun")
        imp = imperatives.create("unknownWord")
        assertThat(imp.act()).isEqualTo("I can't unknownWord a none")

That’s green. We could improve the default a bit, but I think I’ve covered all but one of my concerns, operational words like “inventory”. I’m sure this will work with an addition to the actions, but let’s do it before committing these tests.

        imp = imperatives.create("inventory")
        assertThat(imp.act()).isEqualTo("You got nuttin")

Test will fail with the can’t:

Expecting:
 <"I can't inventory a none">
to be equal to:
 <"You got nuttin">
but was not.

That’s because we don’t understand it as an action. So:

    fun act():String {
        val action:(Imperative) -> String = when(verb) {
            "go" -> { i -> "went $noun" }
            "say" -> {i -> "said $noun"}
            "inventory" -> {i-> "You got nuttin"}
            else -> { i -> "I can't $verb a $noun"}
        }
        return action(this)
    }

And we are green. And everything we changed was a table entry, except for the action when, and that clearly can be implemented as a table access. And, before we’re finished with all this, it will be a table access.

I’m ready to commit: Expanded tests for syntax variations and tables to match. Good result.

Tidying

I’m just about ready to wrap up this morning’s work, probably be an hour and 45 minutes by the time I’m done here, but I want to do some tidying. IDEA and Kotlin offer lots of useful warnings, and especially when I’m trying to learn, it’s useful to look at them.

Variable is never modified so it can be declared using 'val'
    @Test
    fun `two word legal command`() {
        val imperatives = ImperativeFactory(imperativeTable)
        var imp = imperatives.create("go", "e")
        assertThat(imp.act()).isEqualTo("went east")
    }

Sure, why not. Let it help. It changes it.

Remove redundant backticks

Right, this:

    @Test
    fun `verbTranslator`() {
        val vt = VerbTranslator(imperativeTable)
        val imp = vt.translate("east")
        assertThat(imp).isEqualTo(Imperative("go", "east"))
    }

Sure, go for it. Then four copies of this:

Parameter 'i' is never used, could be renamed to _

I would like to build this habit, so let’s do it. Those are all here, now fixed:

        val action:(Imperative) -> String = when(verb) {
            "go" -> { _ -> "went $noun" }
            "say" -> { _ -> "said $noun"}
            "inventory" -> { _ -> "You got nuttin"}
            else -> { _ -> "I can't $verb a $noun"}
        }

I’m way down into the soft warnings by the way, which are so petty that even IDEA is a bit embarrassed about mentioning them. It even mentions my spelling of “nuttin”. It suggests that a few things could be private, and I’ll do those. I don’t much care about public/private, but in fact I might do better if I did, so we’ll push on that a bit and see how it feels.

Oh and one thing that I noticed. this method:

    fun setNoun(noun: String): Imperative {
        return Imperative(verb,noun)
    }

Can be a one-liner (expression body):

    fun setNoun(noun: String): Imperative = Imperative(verb,noun)

Love it. Green. Commit: tidying.

Summary

I had a vague floating concern that the ImperativeSpike might not handle all the cases it needs to. New tests showed me that adding entries to the tables was sufficient to deal with everything I could think of. It seems clear that the when clause can turn into another table lookup, and we’ll probably do that before changing the game to use the new idea. I think we’ll just make the necessary things public and move them to the production side. We’ll decide that in the moment, but that’s my current expectation.

My concern is allayed. We do have a few more things to deal with, including but not limited to:

  • starting with a raw string and making words of it
  • deciding what object should contain the parsing
  • determining what context object to send to the actions

And the big one

  • enhancing the DSL to allow for the tables to be built and extended entirely in the DSL.

My feeling right now is that these things will all go pretty easily, even the DSL enhancement. I expect that that one is going to require us to build a cute little object that looks things up in more than one table, so that we can take the world’s vocabulary and append a temporary one pertaining to the particular room. I foresee no particular problem with this, and think it’ll be kind of fun.

And, of course we might not really need it. We’ll see.

I hope someone out there is reading and getting value here. I’m certainly getting value doing the work.

See you later!



  1. I mean what’s right locally, with the Imperative stuff. Globally, we’re all screwed. Let’s stick to the code, we can make that better. Do please work and vote, to make the world better too. 

  2. I have started including somewhat obscure words in my articles as a service to a particular couple of readers who have learned new words while reading4. “Sanguine” means confident, optimistic. People ruled by the humor “blood” were thought to tend toward optimism. See also “phlegmatic”, “bilious”, and “melancholic”. 

  3. The Mote in God’s Eye, Niven and Pournelle. 

  4. The real reason is that I like to use obscure words sometimes. I think they spice up the prose. Or, anyway, they spice up my day.