I have some design thinking to report, but my hope for today is to have an operating grate somewhere in the world.

When I’m dozing, or just waking up, I habitually think about whatever I’m currently working on, in this case our little DSL and corresponding text game. Some of those ideas include:

Game vs World
I continue to think that we don’t need both the Game object and the World object. I propose to explore that a bit this morning.
World Knows All
I’m thinking it’s best to keep the GameResponse object as something that belongs to the World and is passed to the current Room to be filled in. The View really just wants some text. We can have it ask the Game or World for it. For now, at least, neither the Game, if we keep it, nor the View, need more than that.
Translation
The game might be better if it could support synonymous phrases, such as “go north” and “north” and “n”. Somewhere between fetching the input string from the text field, and executing the command, we’ll need some kind of a translation phase, so that the Rooms can be defined in simpler terms. For example, movement commands, right now, are just “n”, “s”, and so on, although they are just strings and could be “kindly move northward if you please” …. if we pleased. We do not please. So somewhere there may need to be a translation thingie.
Room vs World: Defaults
We’ll have rooms where you can go “e” and “w” but not “s” or “n”. Whenever you try to go in an impossible direction, we’ll want some standard message like “You can’t go that way”. Similarly if you try to take something that isn’t here, we might want “I see no parrot here”. And so on. We may possibly want, when a command comes in, for the current Room to get a shot at responding to it, and then, if the Room doesn’t understand it, the World takes a shot. This would allow the world to handle all the defaults, but it might also provide for commands that mean things to the World and not the Room, such as “Inventory”. Definitely premature expectation at this point, but it’s on my mind.
Generalizing
I think it is natural and desirable to think about our work, although I’d be quick to point out that we need to think about our health, our loved ones, and the drying pants on the deck, not just our work. But thinking about the work seems generally good to me, though I try not to make firm decisions based on speculative thinking. I might decide to try something, but I prefer to be driven primarily by what is, not just what might be. You do you, of course, but I think that thinking is good, we just shouldn’t believe everything we think.

Today’s “Plan”

Scenario
“You find yourself in a depression in the ground. There is a path to the north. A small tunnel appears to the west. It is blocked by a closed grate.”

If you try to go west, the game says “The grate is closed”. If you say “open grate”, the game says “Opened”, and you can go west. If you check the room’s description again, it’ll say something like “An open grate reveals a tunnel to the west.”

My current design idea for this is that we’ll allow the go commands to include an optional block of code, which defaults to {true}, meaning that you can go that way. If the block has code in it, that code will be executed and can do whatever it wants, say whatever it wants, within whatever limits we may later discover or define.

I guess we’ll start by adding that code block to the go command and making it work. And, thanks for reminding me, we’d better start with a test.

Where and How?

How is motion handled now, I ask myself. Like this, I answer. In game:

    fun command(cmd: String) {
        currentRoom = world.command(cmd, currentRoom)
    }

The Game expects a Room back from World.command. In World:

    fun command(cmd: String, currentRoom: Room): Room {
        response = GameResponse("useless name")
        currentRoom.command(cmd, response)
        response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
        return response.nextRoom
    }

The Room gets the command, and:

    fun command(cmd: String, response: GameResponse): Unit {
        val name = when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            "xyzzy" -> move("xyzzy")
            else -> "unknown cmd $cmd"
        }
        response.nextRoomName = name
    }

    fun move(direction: String) :String {
        return moves.getValue(direction)
    }

We define a room with our DSL, with a function on World, thus:

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms.add(room)
        room.details()
        return room
    }

I guess I’ll put this test in RoomTest, even though we’ll create a World as well as a Room. My plan is to provide the optional block on thego command. I start with this much:

    @Test
    fun `room go has optional block`() {
        val world = world {
            room("first") {
                desc("first room", "the long first room")
                go("n", "second", { true })
                go("e","second", { false })
            }
        }
    }

This is enough to anger IDEA and the compiler, because those blocks are not expected. Let’s check the go implementation on Room:

    fun go(direction: String, roomName: String) {
        moves += direction to roomName
    }

Ah. We will have to do something better than stuffing pairs into moves, won’t we? First, let’s provide for the block, which needs to return a boolean, I guess. (I’m really guessing. We might want it to return the current move, I’m honestly not sure. I’m just going with the flow for now, to get the block in there.)

    fun go(direction: String, roomName: String, allowed: ()->Boolean = {true}) {
        moves += direction to roomName
    }

I think this will let me compile throughout, since I defaulted the parameter for the many go commands who do not provide it. Let’s see. I’ll not be surprised if this isn’t quite the syntax. Tests are green. So far so good.

Now we need to save the allowed block in the moves. What even are they?

    val moves = mutableMapOf<String,String>().withDefault { name }

We really need some little object of our own here, but we’ll push forward a bit. Let’s make that a map from String to Pair(String, ()->Boolean) for now.

I can’t quite make that work. I think I’ll try the typealias thing.

OK, I had to hammer a bit to make this go:


typealias GoTarget = Pair<String, ()->Boolean>

class Room(val name: String) {
    val moves = mutableMapOf<String,GoTarget>().withDefault { Pair(name, {true}) }
    ...

    fun go(direction: String, roomName: String, allowed: ()->Boolean = {true}) {
        moves += direction to Pair(roomName, allowed)
    }

    fun move(direction: String) :String {
        return moves.getValue(direction).first
    }

    val roomReferences: Set<String> get () {
        return setOf("Y3", "Y2", "clearing", "woods")
        //return moves.values.toSet()
    }

That last thing: I had neither the time nor the inclination to fix that method, so I made it return a literal set that makes the test pass. All of this was driven by repeated compilation and dealing with the compile messages and the changes all made sense.

Now let’s extend our new test to make it allow the one move and not the other. Note that move returns the String name of the room in the move (or the current room’s name).

    @Test
    fun `room go has optional block`() {
        val world = world {
            room("first") {
                desc("first room", "the long first room")
                go("n", "second", { true })
                go("e","second", { false })
            }
            val response = GameResponse()
            room.command("s", response)
            assertThat(response.nextRoomName).isEqualTo("first")
        }
    }

I expect this test to fail, with “second”, expecting “first”. My expectations are not met. I have to satisfy Kotlin and IDEA first. Also, I have no variable called room to send the message to. I finally get the test to fail as expected:

    @Test
    fun `room go has optional block`() {
        val myWorld = world {
            room("first") {
                desc("first room", "the long first room")
                go("n", "second", { true })
                go("s","second", { false })
            }
        }
        val myRoom = myWorld.unsafeRoomNamed("first")
        val response = GameResponse("foo")
        myRoom.command("s", response)
        assertThat(response.nextRoomName).isEqualTo("first")
    }

Fails as expected:

Expecting:
 <"second">
to be equal to:
 <"first">
but was not.

Perfect, because we’re not evaluating the function yet. So now we can (finally) enhance the move function:

    fun move(direction: String) :String {
        val (target,allowed) = moves.getValue(direction)
        return if (allowed())
            target
        else
            name
    }

We destructure the pair for direction into target, the room name string, and the function, allowed. We evaluate allowed and return either the target or our own name. I think the test should run green. It does. We should check the other side of the branch:

    @Test
    fun `room go has optional block`() {
        val myWorld = world {
            room("first") {
                desc("first room", "the long first room")
                go("n", "second", { true })
                go("s","second", { false })
            }
        }
        val myRoom = myWorld.unsafeRoomNamed("first")
        val response = GameResponse("foo")
        myRoom.command("s", response)
        assertThat(response.nextRoomName).isEqualTo("first")
        val r2 = GameResponse("bar")
        myRoom.command("n", r2)
        assertThat(r2.nextRoomName).isEqualTo("second")
    }

Works as intended. Green. Commit: go command has optional allowed function returning true if move is allowed false if not.

This has taken me 80 minutes, including writing time. I’ll take a little break and then reflect.

Reflection

That was actually pretty straightforward, just had a few spots to touch. We had to change the map from direction to roomName to a map from direction to (roomName, boolean function). We did that in a straightforward but arguably weak way, just using Pair to find the two items together. Still, it was simple enough.

The compiler found all the places that I needed to change, and quite honestly, I think that if I had been working in Lua, those problems would have been found, but at run time, which would have made them a bit harder to find. Not a big win for Kotlin/IDEA, but a small one. Certainly the messages were far more direct telling me that I couldn’t do whatever it didn’t like, pointing right to the statement that it objected to.

There’s some awkwardness to the test, but some of that is occasioned by the fact that I started with the assertions inside the world creation, which confused the compiler and me for a while. Still, the test is pretty straightforward: you create a little world with some rooms and go definitions and the moves happen, or fail, as intended.

Why is that important? Well, it’s a very small step along the way to making our grate work. Because now, we could say something like this:

world {
  room {
    go("w","tunnel") {
      gateIsOpen()
    }
  }
}

And if the gate is open, you go. We will probably want to say more, something like this:

world {
  room {
    go("w","tunnel") {
      if gateIsOpen() {
        say("You pass through the open grate")
        true
      }
      else {
        say("The closed grate bars your way!")
        false
      }
    }
  }
}

The point is, and I think it’s important, once we can write a block of code to be executed when you try something, we open the door (or grate) to doing grate things. (Sorry, not sorry.)

There’s plenty left to do, including some way to define and change grate status, some way to adjust the room description on the basis of grate status, and, indeed, some way to get messages like the one above to display at all. We’ll have to stuff them into the GameResponse somehow.

With any luck and a bit of skill, these things will all be able to be done in tiny steps. That’ll be good, because I seem to have disproportionate trouble when I take larger ones.

Now I need to think a bit, rest a bit, after this heavy lifting. I’ll probably do another article this afternoon. We’ll see. If not, then tomorrow.

For now, we’ve taken a very small step that provides the basis for grate advances. (Sorry again, not sorry again.)

See you next time!