In which, our intrepid hero realizes that while thinking is good, clear thinking is better than muddy thinking.

When one’s first solution to a problem doesn’t quite cut it, and one’s second crashes and burns, it should give one to think. As all faithful readers know, I am a proponent of thinking all the time as we develop software. I can think(!) of no time when I’d say “don’t think, just do”. Yes, there are things we should rather automatically do, like write a test, or move the curly bracket to the end of the line, but even then we should be thinking.

It helps a lot, however, if one’s thinking is clear. And mine has not been. Let me see if I can describe the problem, and then, with some combination of luck and skill, try to work up a better solution than what we have now.

The Problem

We have two-word commands in our game and (presently) plan for nothing more elaborate. The commands consist of a verb, like “take”, and a noun, like “axe”. Some few commands are one word only, at the user end, like “xyzzy”, but we convert that to “say xyzzy” in our parsing. So, two-word commands.

There are two places in the game world where the commands might “make sense”, in a specific room, or in the world in general. Now, strictly speaking, all commands take place in a room, but we don’t want to have to define every command in every room: most of them are amenable to being defined at the top level, the world. The process of taking an axe is the same, wherever you are: if there’s an axe there, remove it from there and add it to the player’s inventory.

However. There are some commands that will behave differently depending on what room you’re in. If you “free bird” in most places, the bird leaves the cage and is there in the room, to be taken or not as you may wish. But if you are in the rainbow room, with a scary snake blocking the door, freeing the bird will result in the bird, in an astounding flurry, driving the snake away.

Therefore, it would be nice for the game designer if there could be commands that are defined at the world level, and commands defined at the room level, where the command at the room level would take precedence over the world one, if you’re in that room.

And that’s what I’ve been working on for the past few days, with the SmartMap, which is in use, and yesterday’s PhraseMap, which is not, because it was a very complex idea that was no more powerful than a regular map.

I’ve not managed, yet, to think clearly about just what the problem is, which is perhaps why none of my solutions so far seem quite right. If you don’t understand the problem, it’s really hard to solve it nicely, or at all. So let’s think now about what the game designer may need to do with a command.

The Designer’s Needs

A command is two words, verb/noun. As we specify what is to be done with a given command verb/noun, we might want to do any of these things:

  1. Handle that specific verb/noun;
  2. Handle that verb with any noun, verb/any;
  3. Handle that noun with any verb, any/noun.

The fourth case, handle any/any, is actually interesting: it could be the default behavior if the player says something completely unintelligible.

Now our current scheme is a map from String to Action, and in current practice, the String is a verb. We only match commands on verb and then we deal with either the specific noun case, or the general noun case, inside the command. that tends to create if or when statements in the command Action, which gets messy.

We’re trying to work out here what the game designer would find useful in their work. I think it’s something like this: They should have the ability to:

  1. Define a specific verb/noun action: What to do if he says “throw cow”;
  2. Define a general verb action: What to do if he says “take” anything;
  3. Define a general noun action: What to do if he tries to do anything with the kitten.

Furthermore, I think the game designer would like to be able to define those cases generally in the world, and specifically in a given room. And, I think, if a room command matches the verb/noun the player said, that’s the one that should get the action, even if there is a better match in the world. Offhand, I can’t think of a specific example of a loose room command and a tight world one, but room priority seems right to me.

All world commands presently are verb/any, by the nature of the implementation. Here’s an example of why verb/any in the room should take priority over the standard verb/any.

You are in the grease room.
There is gold here.

> Take gold
The grease in this room is slippery, you fail to get the gold 
and drop something else.

You are in the grease room.
There is gold here.
There is a magic wand here.

Yeah, yeah, so what does the designer need?

Right. I think what we need is a sort of pattern match, where the designer can specify verb/noun, or verb/any, or any/noun, and the best match selects the action. And I think that if anything matches in the room, it will trump even a better match in the world. (That could be wrong, but I can’t think of a case where it would be, and in any case it’ll be consistent.)

Furthermore, I see no value to the current scheme of moving the actions to the SmartMap and then looking them up. Why not just look up the command in the room’s actions directly, and if the room doesn’t want the command, look it up in the world. The fancy object, I suspect, isn’t pulling its weight, and I think we’ll do better to keep the behavior visible at the room and world level, at least until we get it doing what we want.

We’re inching toward a solution. Do we understand the problem well enough?

I propose this as the problem: Game designer can specify, at world or room level, a mapping from input to action in these forms:

  1. verb/noun: specific match;
  2. verb/any: “noun is wild-card”
  3. any/verb: “verb is wild card”
  4. any/any: “wild-wild, matches any input”

This specification can be made at the world level or in an individual room. Any match in the room is preferred over any match in the world, even a more specific world match.

There will probably need to be a way for a room command to give up and ask the world to handle the command after all.

Working Toward a Solution

It seems to me clear that there will be some kind of map from input command to Action, much as we have now, from Verb to Action. I think the key, rather than Verb, should probably be a Phrase. We already have a Phrase class defined:

data class Phrase(val verb: String?=null, val noun: String?=null)

I don’t like the fact that it’s nullable. I think I’d rather have those elements specifically allow a wild card. “*” is an obvious choice, but “:any:” could work. Anything that can’t be a command word, really.

So our map might look vaguely like this:

take/cows -> "Where would you keep them?"
take/:any: -> "Taken"
:any:/cows -> "You seem unduly fascinated by these cows."
:any:/:any: -> "I don't understand."

One thing becomes clear from typing that: typing “:any:” is a pain. We’ll need a much easier way to specify the phrase.

When we submit a command to this map, we want the best match, where the order of goodness is

yes/yes > yes/no > no/yes > no/no

Hm. Binary number score? Interesting … Anyway, that’s the order of preference. In the room, I think we’ll never use any/any, but in principle I guess it could happen.

The Code Wants to Participate

The Code is asking to participate in this design session. I have a p-baked idea for very small p. Let’s write a test.

I start with this much:

    @Test
    fun `priority match`() {
        val map: Map<Phrase,String> = mapOf(
            Phrase("take","cows") to "takecows",
            Phrase("take") to "take",
            Phrase(noun="cows") to "cows",
            Phrase() to "any"
        )
    }

Let’s write some code that finds matches, given an input phrase. I’ll write it in line to begin with.

I think I’m going to need an object soon, but maybe this morning is a good time to learn how to extend classes. Anyway …

        var result:String
        result = find(Phrase("take","cows"))
        assertThat(result).isEqualTo("takecows")

I just posited a function find, which I’ll write right now:

    @Test
    fun `priority match`() {
        val map: Map<Phrase,String> = mapOf(
            Phrase("take","cows") to "takecows",
            Phrase("take") to "take",
            Phrase(noun="cows") to "cows",
            Phrase() to "any"
        )
        var result:String
        result = find(Phrase("take","cows"), map)
        assertThat(result).isEqualTo("takecows")
    }

    fun find(p:Phrase, m:Map<Phrase,String>): String {
        return m[p]!!
    }

That runs, but it won’t do for long. Extend this test, or do another? Let’s do another, see how that feels.

Idea helps me extract the setup, which is nice. My second test:

    @Test
    fun `take non-cows`() {
        val map = makeMap()
        var result = find(Phrase("take", "bird"), map)
        assertThat(result).isEqualTo("take")
    }

    private fun makeMap() = mapOf(
        Phrase("take", "cows") to "takecows",
        Phrase("take") to "take",
        Phrase(noun = "cows") to "cows",
        Phrase() to "any"
    )

This’ll fail nicely. We get a null pointer exception, because there is nothing at that key and our find code is rather dull:

    fun find(p:Phrase, m:Map<Phrase,String>): String {
        return m[p]!!
    }

I think what we need to do is to search for four different keys:

  1. take bird
  2. take null
  3. null bird
  4. null null

And we should accept the first one we find.

I have an idea. This is really poorly formed but let me try it.

    fun find(p:Phrase, m:Map<Phrase,String>): String {
        val p2 = Phrase(p.verb)
        val p3 = Phrase(noun=p.noun)
        val p4 = Phrase()
        var results = listOf(p,p2,p3,p4).map {m.getOrDefault(it,"nope")}
        print("$p->")
        print("$results->")
        results = results.filter { it != "nope" }
        println("$results")
        return results.first()
    }

I make a list of the four possible forms we might match, map them to either what’s in the map or “nope”, filter out the “nope” and return the first. Since any/any is in the list, we should always get something.

Two more tests:

    @Test
    fun `something with cows`() {
        val map = makeMap()
        var result = find(Phrase( noun="cows"), map)
        assertThat(result).isEqualTo("cows")
    }

    @Test
    fun `something different`() {
        val map = makeMap()
        var result = find(Phrase("something", "different"), map)
        assertThat(result).isEqualTo("any")
    }

I expect these to run green, and they do. The printed output shows what’s going on:

Phrase(verb=take, noun=cows)->[takecows, take, cows, any]
	->[takecows, take, cows, any]
Phrase(verb=take, noun=bird)->[nope, take, nope, any]
	->[take, any]
Phrase(verb=null, noun=cows)->[cows, any, cows, any]
	->[cows, any, cows, any]
Phrase(verb=something, noun=different)->[nope, nope, nope, any]
	->[any]

Summary

I think we have the basis for a useful solution to the problem. We’ll surely want the objects to help us more than they are now, but I’m feeling good about this.

Of course, I was feeling good about the other ideas as well. We’ll see what happens when we get the new lookup into the game. It’s well past time to do that, isn’t it?

In addition, I’m definitely feeling more facile with Kotlin. That’s a good thing. But I know that there are many tricks and idioms that I don’t have a handle on yet. But getting better.

See you next time!