Let’s make the Actions in the Lexicon do their job. There are some issues … do we resolve them? We do and it’s great! A big lesson is relearned.

Yesterday, we plugged the Imperative in where the Command had been. We used it to parse the command (which is nigh on to trivial, but rather neatly done if I do say so myself) but we are dispatching the action in the Room’s command method:

// class Room ...
    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        world.testVerb = imperative.verb
        world.testNoun = imperative.noun
        val action: (Imperative, World)->Unit = when(imperative.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(imperative, world)
    }

The actions, inventory, take, and so on, are methods in Room. They all accept parameters of the imperative and the world. So those method have access to all three, and they sometimes need them:

    val move = {imperative: Imperative, world: World ->
        val (targetName, allowed) = moves.getValue(imperative.noun)
        if (allowed(world)) world.response.nextRoomName = targetName
    }

The noun comes from the imperative, moves are a collection in Room, and we refer to the world to check whether the move is allowed, and to set the next room into the response.

However, so far, the Actions in the Lexicon expect to pass in only the imperative:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            "go" to { imp: Imperative -> imp.say("went ${imp.noun}")},
            "say" to { imp: Imperative -> imp.say("said ${imp.noun}")},
            "inventory" to { imp: Imperative -> imp.say("You got nothing")}
        ).withDefault { { imp: Imperative -> imp.say("I can't ${imp.verb} a ${imp.noun}") }})
    }

Each action can only send messages to the imperative, imp. It could, of course, send messages to anything that the imperative happened to know. If the imperative knew the room and the world, then we could send messages (call methods) on those, as needed. As written and tested, the Imperative does not know those things. It only knows the noun and the verb, and is passed the lexicon in its act method:

data class Imperative(val verb: String, val noun: String) {
    fun act(lexicon: Lexicon):String {
        lexicon.act(this)
        return said
    }


class Lexicon(val synonyms: Synonyms, val verbs: Verbs, val actions: Actions) {
    fun act(imperative: Imperative) = actions.act(imperative)
Reflection
What I’m struggling with, I think, is a difference between my speculative design for Imperative and Lexicon, and what I actually need. My focus should be on what I need, and I should be perfectly willing to change those supporting classes to make it so. However, I don’t know quite what I need, and I have those nice tests for the Imperative. I think I’m overly concerned about the “sunk cost” in what I have.

That’s natural, I think, but mistaken. Another thing holding me back is the fact that I don’t really know what I need in the game. I need to focus on the game and let the old tests fall where they may. Let’s think about what we need.

Right now, each action resolves to a method on Room. However, I have in mind the idea that “game designers” should be able to define new actions using the DSL somehow. That’s causing me to try to think about all the things at once. Let’s just make it work. I see two ways to go. Let me list them, in hope that I think of more along the way:

  1. Pass world and room to act, and passed to the Action. That will require the signature of the Action to change.
  2. Plug world and room into the Imperative, which is already passed to the Action, and which will allow an Action to access them at will.
  3. Same idea but with some kind of single context object given to Imperative, possibly allowing greater future flexibility.
  4. Create a new object to contain pointers to world, room, and imperative, and send the act message to that. That would avoid mutating the Imperative, but at the cost of changing the Action signature.

See how that goes? We try to list our only two ideas and suddenly we have more. That they aren’t very good doesn’t matter: they might be good, and even if not they help us choose the best one we have.

I’m going to stuff world and room into the Imperative.

Oh! And let’s do it with a second, overloaded act method. That should keep existing tests running.

I’ll do this by intention, which just means that I’ll write the code that I wish worked, and then I’ll make it work. This is a bit different from writing a test for what I wish worked, because I write the new code right in my real objects. We start here:

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        world.testVerb = imperative.verb
        world.testNoun = imperative.noun
        val action: (Imperative, World)->Unit = when(imperative.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(imperative, world)
    }

We want to replace all that action bit with a call to act:

// class Room ...
    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        val factory = ImperativeFactory(world.lexicon)
        val imperative = factory.fromString(command.input)
        world.testVerb = imperative.verb
        world.testNoun = imperative.noun
        imperative.act(world, this) -- this = room
    }

I’m not passing the lexicon, because world has it. Now we write the new method in Imperative. IDEA offers me nothing useful. An extension function, perhaps, but why not build it right in. OK, I’ll just do it:

data class Imperative(val verb: String, val noun: String) {
    lateinit var world: World
    lateinit var room: Room

    fun act(world: World, room: Room) {
        this.world = world
        this.room = room
        act(world.lexicon)
    }

I think this is kind of tacky. There may be something better to do, but let’s see if we can make it work. Our actions are now all wrong but let’s run to see what happens. I expect chaos.

In fact, I get so much chaos that I decide to fix up the actions in our World Lexicon before I even pay attention to the many broken tests. No, that’s a bad idea! Let me find one easy one and fix that.

I choose roomsWithGo which looks like this but is actually rather simple:

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val player = Player(world, "clearing")
        player.command("go n")
        assertThat(world.testVerb).isEqualTo("go")
        assertThat(world.testNoun).isEqualTo("n")
        assertThat(world.response.nextRoomName).isEqualTo("woods")
    }

I think that since this just has one go command, I can just fix up the go command in my Actions to do the right thing:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            "go" to { imp: Imperative -> imp.say("went ${imp.noun}")},
            "say" to { imp: Imperative -> imp.say("said ${imp.noun}")},
            "inventory" to { imp: Imperative -> imp.say("You got nothing")}
        ).withDefault { { imp: Imperative -> imp.say("I can't ${imp.verb} a ${imp.noun}") }})
    }

The right thing to do in go is to call the move method on Room, passing the imperative and the world. So, presumably:

    "go" to { imp: Imperative -> imp.room.move(imp, imp.world)},

IDEA prompted me through that, so I think it understands me. Let’s test.

That fixed four out of the nine failing tests. Woot! Now I am inspired to change the others. Here’s one to fix:

    @Test
    fun `can take inventory`() {
        val world = world {
            room("storage") {
                desc("storage room", "large storage room")
                item("broom")
            }
        }
        val player = Player(world, "storage")
        player.command("take broom")
        assertThat(player.resultString).isEqualTo("broom taken.\nlarge storage room\n")
        player.command("inventory")
        assertThat(player.resultString).isEqualTo("You have broom.\n\nlarge storage room\n")
    }

I’ll have to add take and fix inventory, I think. We have this to start:

    "go" to { imp: Imperative -> imp.room.move(imp, imp.world)},
    "say" to { imp: Imperative -> imp.say("said ${imp.noun}")},
    "inventory" to { imp: Imperative -> imp.say("You got nothing")}

Change to:

    "take" to { imp: Imperative -> imp.room.take(imp, imp.world)},
    "inventory" to { imp: Imperative -> imp.room.inventory(imp, imp.world)},

I expect this to improve things. Yes. Previously failed 5, now only 2. Both will be magic, I think. Yes, one uses xyzzy and one uses wd40. We need to fix say to do castSpell, I think.

    "say" to { imp: Imperative -> imp.room.castSpell(imp, imp.world)},

Test with high hopes.

Gentlebeings, we are green! Commit: Commands use Imperative fully, including Actions.

Let’s reflect.

Reflection

The very good news is that with a few simple steps, we have installed the new Imperative fully, completing our conversion to the new technology. We now only use the Command object to pass a string from the player to the world to the game. We could eviscerate1 Command, or even just pass the string. But that can be deferred.

(My friend GeePaw Hill would at least use typealias to call the string a Command, which at least makes the code make a little more sense. And both he and I might finally build a tiny class2 to do the job.)

Our actions don’t look unduly complex:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            "go" to { imp: Imperative -> imp.room.move(imp, imp.world)},
            "say" to { imp: Imperative -> imp.room.castSpell(imp, imp.world)},
            "take" to { imp: Imperative -> imp.room.take(imp, imp.world)},
            "inventory" to { imp: Imperative -> imp.room.inventory(imp, imp.world)},
        ).withDefault { { imp: Imperative -> imp.say("I can't ${imp.verb} a ${imp.noun}") }})
    }

Ah … I’m glad we’re having this little chat. I notice that the default isn’t set up to call into Room. Room does have a method to do the job:

    val unknown = { imperative: Imperative, world: World ->
        world.response.say("unknown command '${imperative.verb} ${imperative.noun}'")
    }

We apparently don’t have a test for that. Let’s fix it anyway, and we can argue about the test in a moment.

    { imp: Imperative -> imp.room.unknown(imp, imp.world) }

Test. Green, no surprise. Let’s do a test for the case. We just have to give an unknown command. I’m sure something interesting will happen. We’ll need a test that checks the sayings.

    @Test
    fun `unknown command`() {
        val world = world {
            room("woods") {
                desc("You are in the woods.", "You are in the dark woods.")
            }
        }
        val room = world.unsafeRoomNamed("woods")
        val response = GameResponse()
        val badCommand = Command("abcd efgh")
        world.command(badCommand, room, response)
        assertThat(response.resultString).contains("unknown command 'abcd efgh'")
    }

OK, happy now? Test passes. All tests pass. Commit: Added test for unknown command in game.

Where were we? Oh, yes, reflecting. I like this setup even better than the Command with its neat functional parsing. The parsing itself is a bit smaller, I think, and more direct. And the table-driven syntax and actions are nicer. So I think it’s an improvement.

We’ll need to experiment, as we build out the world, to see how much logic can be pushed into actions, without the need to add new methods to Room or World. I am hopeful about that. But all that is for another day. Is there anything we could do today?

I can think of one thing. We might be able to remove the big command class and replace it with a simple data class with nothing in it but the input line. Let’s try that:

Replacing Command

Let me check references to Command. Outside of Command itself, and the tests in CommandExperiment, there are just the creation and passing thru of input. What’s a good way to remove this stuff? I guess I’ll just tell IDEA to delete the files and see what happens. Or should I rename? Heck, I’ll try the removal.3

First the tests. They remove nicely. Now the Command.kt file. I’m sure it’ll complain about that.

Hmm, it mostly did it, but has indicated a red thing or two. Anyway I plan to create a class of the same name. Where shall I put it?

In with Player, I think.

data class Command(val input: String)

Test. Ha! Take that, you doubters! Green! Commit: Replace millions of lines of Command with single line data class.

OK, I look upon this and I see that it is good. Let’s sum up.

Summary

Over the course of a few days, we wrote a new class, Imperative, with friends Lexicon and ImperativeFactory, plus Synonyms, Verbs, and Actions, all quite small and direct, and very well tested. These new classes have no replaced the old less capable and less flexible Command and its friends.

What is worthy of note is that we did this in very small steps, getting all our tests to green every few minutes, so that we could at any time commit our code to the main line … which in fact we did. From initial creation to plugging it into the real game play, we were able to make changes to only a few lines at a time, guided along the way by tests.

We have accomplished the replacement of a key component that was relied upon by at least three classes, and that was central to the operation of the game, in small steps, never leaving the game broken.

This is not a tribute to our genius, though it goes without saying that you are in fact a genius. What it tells us is that quite often—I would say almost always—we can carry out even significant design changes in small steps that ever break the system. This isn’t proof: probably such a thing can’t be proven. But, in my view, it’s the way to bet … and the strategy to follow.

Lesson:
Let’s always begin by assuming that the changes we need can be done incrementally, in very small steps. Then we’ll work so as to make that assumption come true, by always finding another small step in the right direction. The worst that can happen is that finally, we’ll come to a big one … but even then, it’ll be smaller than it would have been, because of all the small bits we’ve already done.


  1. To remove the guts (viscera). Pull out all the unnecessary bits from the class. 

  2. As you’ll see, we do build a tiny class, enabling us to delete upwards of a million lines of old Command code. Probably a million. 

  3. Did he use a stronger word than “heck”? Probably not, Kitty has tender ears.