GitHub Repo

What shall we do today, Brain? Same as every day, Pinky, try to create a world.

In implementing something the other day, I found that I was not adept at using the facilities of the game building objects, notably the tables used to look up actions. In my defense, I hadn’t built any game actions for a number of days and my thoughts had been on other parts of the structure. Still, when we don’t know how something in our program works, or perhaps don’t think of it right away when it would be useful, it’s a sign of something. I choose to think it’s not a sign of old age, though I have plenty of that going on. It may just call for practice in that area: it’s easy to forget things that we never do. Or, it could be a sign that there needs to be better demonstration in the code as to how to do that thing.

Building up the world would help in practicing the use of the features, it would help in discovering how they could be better, and it would provide useful examples to be used later on.

Aside:
I find myself thinking about this program as if it were real. Except for people who want to fork the repo and then play the game that’s in there, or create one of their own, this program is as close to not real as they come, other than the ones we just imagine that we could write if we wanted to. At least this one I really am writing.

But it’s odd. I could make a fairly fun game of this, and I kind of wish folks could experience it, and then study the program to see how it works and how to make it better. Ah well, it is what it is.

Let’s review our list of things we may need to do, from a couple of days ago. I’ll strike out the things that I think have been improved, not implying that they couldn’t be better still. And I’ll strike out the unreasonable dreams as well. [sigh]

  1. I’d like to work on room definition and connection to see if we can make that easier and more compact.
  2. There are weird things in the tests, like the one that makes XYZZY into a direction like East. Those things should be cleaned up.
  3. We should devise a few more puzzles, some perhaps requiring global information, and solve them. That should drive out some improvements somewhere.
  4. I'd really really like to be able to deploy this in some easy way. If that were possible, I'd code up a real game to play.
  5. I’m sure there are places in the code, especially in the initial setup, player area, and such, that need work. We’ll look for them.
  6. I wonder if there’s any value to publishing the program as an example, with these articles available as background, and new articles treating the finished system as an example of a game-building DSL, with write-ups describing how it all works, and enabling anyone who wanted to to build up a game of their own.

  7. (Added) Move toward more actions and vocabulary being defined in the world definition, rather than in the Kotlin code that I’ve written.

In the Room, we have the action DSL word, that lets us define an action for the room, such as “say xyzzy”, with an anonymous function that does whatever needs to happen. It’s implemented like this:

    fun action(verb: String, noun: String, action: Action) {
        action(Phrase(verb,noun), action)
    }

    private fun action(phrase: Phrase, action: Action) {
        actionMap[phrase] = action
    }

That stores the action in the action map specific to the room. The world has an action map as well. Where is it?

Ah. This is one of those areas that isn’t as spiffy clean as it might be. Let’s look at a bit more than just the action method:

class Room(val roomName:R) {
    private val actionMap = mutableMapOf<Phrase,Action>(
        Phrase() to {imp -> imp.notHandled()}
    )
    private val actions = Actions(actionMap)

    // DSL Builders

    fun action(verb: String, noun: String, action: Action) {
        action(Phrase(verb,noun), action)
    }

    private fun action(phrase: Phrase, action: Action) {
        actionMap[phrase] = action
    }

Notice that we are updating the actionMap when we add a room action. And that map just happens to be held in the object actions, an instance of Actions, which is holding on to the actionMap that we’re editing.

We should really add our new phrase to the Actions instance rather than wedge it into the object we’re holding on to. In addition, we’re priming the map, which also happens in World, as we’ll see shortly. We’d do better to hide all this inside Actions, I think. Let’s start here, by intention. We’ll change our private action thus:

    private fun action(phrase: Phrase, action: Action) {
        actions.add(phrase, action)
    }

IDEA wants us to implement that method. Seems fair.

    fun add(phrase: Phrase, action: (Imperative) -> Unit) {
        map[phrase] = action
    }

Test. Green. Commit: Actions.add created and used in Room.

Now we’d like the room not to create the map and send it in, but instead to create actions and tell it to add our default pair.

Here’s where we start:

    private val actionMap = mutableMapOf<Phrase,Action>(
        Phrase() to {imp -> imp.notHandled()}
    )
    private val actions = Actions(actionMap)

We should be able to create an Actions with no map and then add an item. Let’s code that and make it work … existing tests will fail until we’re ready.

    private val actions = Actions()
    init {
        actions.add(Phrase()) { imp -> imp.notHandled() }
    }

Now Actions needs to have a default map:

class Actions(private val map: ActionMap = mutableMapOf<Phrase,Action>()) {

Test. Green. Commit: Room now adds actions using Actions.add, no longer creates map and passes to Actions constructor.

Now what about the World’s table, which is far more extensive?

class World ...
    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            Phrase("go") to { imp: Imperative -> imp.room.move(imp, imp.world) },
            Phrase("say", "wd40") to { imp: Imperative ->
                imp.world.say("Very slick, but there's nothing to lubricate here.")
            },
            Phrase("say") to { imp: Imperative ->
                imp.world.say("Nothing happens here!") },
            Phrase("take") to { imp: Imperative -> imp.room.take(imp, imp.world) },
            Phrase("inventory") to { imp: Imperative -> imp.world.showInventory() },
            Phrase("look") to { imp: Imperative-> imp.room.look()},
            Phrase() to { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        ))
    }

I’ll just rewrite that to create the Actions first and add to it:

    private fun makeActions(): Actions {
        return Actions().also {
            it.add(Phrase("go")) { imp: Imperative -> imp.room.move(imp, imp.world) }
            it.add(Phrase("say", "wd40")) { imp: Imperative ->
                imp.world.say("Very slick, but there's nothing to lubricate here.")
            }
            it.add(Phrase("say")) { imp: Imperative ->
                imp.world.say("Nothing happens here!") }
            it.add(Phrase("take")) { imp: Imperative -> imp.room.take(imp, imp.world) }
            it.add(Phrase("inventory")) { imp: Imperative -> imp.world.showInventory() }
            it.add(Phrase("look")) { imp: Imperative-> imp.room.look()}
            it.add(Phrase()) { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        }
    }

I expect this to work. It does. We are green. We could remove the Actions creation parameter, except there may be tests creating them. I’ll look. There is one test using the ability to pass in a table. We’ll leave that for now, but it would be better to fix it soon. Our mission right now is to build an action word in the World DSL. And we can surely copy the one in Room now.

I paste them over as is, but I’d like to have a test. Should be easy enough.

    @Test
    fun `world can have special action`() {
        val world = world {
            action("exhibit","curiosity") { _: Imperative -> say("remember the cat")}
            room(R.Z_FIRST){
                desc("short first", "long first")
            }
        }
        val player = Player(world, R.Z_FIRST)
        var resultString = player.command("exhibit curiosity")
        assertThat(resultString).contains("remember the cat")
    }

Test runs green. Commit: World now supports action in DSL.

OK. Now, because the work day isn’t over and we still have some energy, let’s fix that test that passes a table to Actions.

The ImperativeTest is the culprit:

class ImperativeTest {
    private fun getFactory() = PhraseFactory(getLexicon())
    private fun getVerbs() = Verbs(TestVerbTable)
    private fun getSynonyms() = Synonyms(TestSynonymTable)
    private fun getActions() = Actions(TestActionTable)
    private fun getLexicon() = Lexicon(getSynonyms(), getVerbs())

private val TestActionTable = mutableMapOf(
    Phrase("take", "cows") to {imp: Imperative -> imp.testingSay("no cows for you")},
    Phrase("hassle") to { imp: Imperative -> imp.testingSay("please do not bug the ${imp.noun}")},
    Phrase(noun="cows") to {imp:Imperative -> imp.testingSay("what is it with you and cows?")},
    Phrase("go") to { imp: Imperative -> imp.testingSay("went ${imp.noun}")},
    Phrase("say") to { imp: Imperative -> imp.testingSay("said ${imp.noun}")},
    Phrase("inventory") to { imp: Imperative -> imp.testingSay("You got nothing")},
    Phrase() to { imp: Imperative -> imp.testingSay("I can't ${imp.verb} a ${imp.noun}") }
)

Let’s make it a creator, like the other one. It’ll need to be a function.

private fun makeTestActions(): Actions {
    return Actions().also {
        it.add(Phrase("take", "cows")) { imp: Imperative -> imp.testingSay("no cows for you") }
        it.add(Phrase("hassle")) { imp: Imperative -> imp.testingSay("please do not bug the ${imp.noun}") }
        it.add(Phrase(noun = "cows")) { imp: Imperative -> imp.testingSay("what is it with you and cows?") }
        it.add(Phrase("go")) { imp: Imperative -> imp.testingSay("went ${imp.noun}") }
        it.add(Phrase("say")) { imp: Imperative -> imp.testingSay("said ${imp.noun}") }
        it.add(Phrase("inventory")) { imp: Imperative -> imp.testingSay("You got nothing") }
        it.add(Phrase()) { imp: Imperative -> imp.testingSay("I can't ${imp.verb} a ${imp.noun}") }
    }
}

A bit of flashy multi-cursor editing and that’s done. Now use it:

    private fun getActions() = makeTestActions()

I’m expecting green. We’re good. Now we can remove the parameter from Actions:

class Actions() {
    private val map: ActionMap = mutableMapOf<Phrase,Action>()

Green. Commit: Actions member map now created internally, no longer passed as parameter.

Reflection

This change was easy, but it extends the capability of our DSL substantially. It is now possible to define all the actions of the game within the DSL itself. There are seven default actions, and we’ll leave them in their function for now:

class World ...
    private fun makeActions(): Actions {
        return Actions().also {
            it.add(Phrase("go")) { imp: Imperative -> imp.room.move(imp, imp.world) }
            it.add(Phrase("say", "wd40")) { imp: Imperative ->
                imp.world.say("Very slick, but there's nothing to lubricate here.")
            }
            it.add(Phrase("say")) { imp: Imperative ->
                imp.world.say("Nothing happens here!") }
            it.add(Phrase("take")) { imp: Imperative -> imp.room.take(imp, imp.world) }
            it.add(Phrase("inventory")) { imp: Imperative -> imp.world.showInventory() }
            it.add(Phrase("look")) { imp: Imperative-> imp.room.look()}
            it.add(Phrase()) { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        }
    }

The main reason to leave them as defaults is that there are tests that are counting on the world to initialize some of these commands. And, truth is, except for “wd40”, they are pretty generic and should apply to any world we might ever build.

I think we should look at the other tables that are built in, such as Synonyms and Verbs:

    private fun makeSynonyms(): Synonyms {
        return Synonyms( mutableMapOf(
            "e" to "east", "ne" to "northeast",
            "n" to "north", "se" to "southeast",
            "w" to "west", "nw" to "northwest",
            "s" to "south", "sw" to "southwest").withDefault { it }
        )
    }

    private fun makeVerbs(): Verbs {
        return Verbs(mutableMapOf(
            "go" to Phrase("go", "irrelevant"),
            "east" to Phrase("go", "east"),
            "west" to Phrase("go", "west"),
            "north" to Phrase("go", "north"),
            "south" to Phrase("go", "south"),
            "e" to Phrase("go", "east"),
            "w" to Phrase("go", "west"),
            "n" to Phrase("go", "north"),
            "s" to Phrase("go", "south"),
            "nw" to Phrase("go", "northwest"),
            "ne" to Phrase("go", "northeast"),
            "sw" to Phrase("go", "southwest"),
            "se" to Phrase("go", "southeast"),
            "say" to Phrase("say", "irrelevant"),
            "look" to Phrase("look", "around"),
            "xyzzy" to Phrase("say", "xyzzy"),
            "wd40" to Phrase("say","wd40"),
        ).withDefault { Phrase(it, "none")})
    }

Those synonyms are there so that when we say “go s”, we get “go south”, which will be understood by the “go” line in Verbs. Without that, we’d have to have even more go lines, because the go command now expects to look up a Direction by its name.

It’s a bit convoluted, to be honest, but it also works very nicely. Nonetheless, we would perhaps like to be able to add to those tables as part of the DSL. A wise person, which assumes people not in evidence here, would wait until there was a need.

Fortunately, a lazy person might also do that, so we’ll wait.

I can now line out a bit of item 7, leaving these items:

  1. I’d like to work on room definition and connection to see if we can make that easier and more compact.
  2. There are weird things in the tests, like the one that makes XYZZY into a direction like East. Those things should be cleaned up.
  3. We should devise a few more puzzles, some perhaps requiring global information, and solve them. That should drive out some improvements somewhere.
  4. I'd really really like to be able to deploy this in some easy way. If that were possible, I'd code up a real game to play.
  5. I’m sure there are places in the code, especially in the initial setup, player area, and such, that need work. We’ll look for them.
  6. I wonder if there’s any value to publishing the program as an example, with these articles available as background, and new articles treating the finished system as an example of a game-building DSL, with write-ups describing how it all works, and enabling anyone who wanted to to build up a game of their own.
  7. Move toward more actions and vocabulary being defined in the world definition, rather than in the Kotlin code that I’ve written.

Reflection^2

This change went in very easily. It would have been even easier had I been a bit more careful about objects always doing their own work. Creating a map and passing it in to the Actions object was always a bit naff. Having it be the only object that even knew what kind of data structure it used was always better, but we were evolving from a simple map toward more complicated things.

I continue to “discover” that wrapping collections early generally pays off. Slowly he learned, inch by inch, step by step1

When capabilities are added easily, we can be confident that that part of our design is pretty decent. When things are difficult … I think it’s fair to say that that’s a sign that the design isn’t as good as it could be in that area. Generally it’ll be a missing object, or a coupling that shouldn’t be there. Overall, we’re having relatively smooth results, which makes me feel good about what we have here.

I’m a bit over two hours’ work here. A good time to cut this article off.

Summing up, I’d just say that the design progresses nicely, the design seems quite solid, I continue to learn Kotlin, and I wish someone would create a real game with this tool, and that there were a way to publish it. Maybe text games are due for a comeback …

Then again, maybe not. See you next time! I think we’ll define some actual game play next time. Maybe it’ll inspire someone to help me deploy the thing.



  1. Niagara Falls …