GitHub Repo

Typing ‘go blue’ instead of something like ‘go east’ results in a runtime exception. I take exception to that. Let’s fix it.

Let’s write a test.

    @Test
    fun `go blue`() {
        val world = world {
            room(R.Woods) {
                go(D.South,R.Clearing)
            }
        }
        val player = Player(world, R.Woods)
        player.command("go blue")
        assertThat(world.response.nextRoomName).isEqualTo(R.Woods)
    }

As expected:

Index 0 out of bounds for length 0

That’s coming from here:

enum class D {
    North, South, East, West,
    Northwest, Southwest,Northeast,Southeast,
    Up, Down, XYZZY;

    companion object {
        fun valueMatching(desired: String): D {
            return values().filter {it.name.equals(desired, ignoreCase = true)}[0]
        }
    }
}

… from here:

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

We could allow valueMatching to return a null. Let’s start with that.

companion object {
    fun valueMatching(desired: String): D? {
        val list = values().filter {it.name.equals(desired, ignoreCase = true)}
        return when(list.isEmpty() ) {
            true -> null
            false -> list[0]
        }
    }
}

And here:

fun move(imperative: Imperative, world: World) {
    D.valueMatching(imperative.noun)?.let {
        val (targetName, allowed) = moves.getValue(it)
        if (allowed(world)) world.response.nextRoomName = targetName
    }
}

The ?.let ensures that in what follows, it is non-null. The move command has no effect when the move is not done: we stay in the same room. I expect the test to pass. It does.

Commit: Fix problem where “go blue” caused exception.

I wonder whether we could do this a bit more cleanly. I’d rather not have that null escaping from the valueMatching call.

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

fun executeWithDirection(desired: String, function: (D) -> Unit) {
    val list = values().filter {it.name.equals(desired, ignoreCase = true)}
    if (list.isNotEmpty()) { function(list[0]) }
}

Let’s rename that execute method, and the parameter too:

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

fun executeIfDirectionExists(desired: String, function: (D) -> Unit) {
    val list = values().filter {it.name.equals(desired, ignoreCase = true)}
    if (list.isNotEmpty()) { function(list[0]) }
}

That seems decent. We can do better.

fun executeIfDirectionExists(desired: String, function: (D) -> Unit) {
    val list = valuesMatching(desired)
    if (list.isNotEmpty()) { function(list[0]) }
}

private fun valuesMatching(desired: String): List<D> {
    return values().filter {it.name.equals(desired, ignoreCase = true)}
}

Let’s commit that one: Improve D companion code, provide executeIfDirectionExists.

I wonder if we can do better still.

companion object {
    fun executeIfDirectionExists(desired: String, function: (D) -> Unit) {
        valuesMatching(desired)?.let { function(it) }
    }

    private fun valuesMatching(desired: String): D? {
        return values().find {it.name.equals(desired, ignoreCase = true)}
    }
}

Yes. If valuesMatching returns non-null, we execute the function with it. And valuesMatching returns null if it finds nothing. The D? is nicely hidden inside just one function.

I like this one. Commit: Refactor D.executeIfDirectionExists.

Summary

The transition from pure strings for room names and directions to enums has been easy, but a bit ragged. The issue simply came down to the need to change every call to room and go to use the new enums, and there were 50 or so in the tests. All the changes were trivial, with a few requiring me to choose new room names.

I am a bit disappointed with the need to use the same room names in tests and the real game, but I can’t see a pleasant way to allow for two different enums and still get the convenient prompting that we get now. And the Kotlin requirement for CamelCase names is a bit irritating but we’ll get used to it, and the savings from avoiding errors like accidentally typing “caveroom” instead of “cave room” will be substantial. All the descriptions, of course, are plain strings and can say whatever we want them to say.

I am fond of this new lookup, which needs to deal with input that doesn’t match the enums, and I like how it produces a D? and then immediately only processes what follows if it’s a D. That seems quite nice to me. I’ll submit the code to the committee and see if they accept it.

All in all, though it has taken a few small sessions, I like how this has turned out. The big lesson to learn, for me, is to bear down on using tiny classes of my own rather than raw strings and such.

That might pay off even now … I’ll think about that.

As for what remains:

  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.

I wonder. See you next time!