GitHub Repo

I think I’ll do a bit more on darkness. With some TDD, of course.

We started yesterday on darkness, and I wrote a test for a dark room:

    fun `darkness room`() {
        val world = world {
            room(R.Z_FIRST) {
                desc("You are in nondescript room.",
                    "You are in a nondescript room. A darkened hall leads south.")
                go(D.South, R.Darkness)
            }
            room(R.Darkness) {
                desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
                go(D.North, R.Z_FIRST)
                action {
                    if (it.verb=="go" && it.noun=="north") {
                        it.notHandled()
                    } else {
                        say("You have been eaten by a grue!")
                    }
                }
            }
        }
        val player = Player(world, R.Z_FIRST)
        var result = player.command("s")
        assertThat(result).contains("grue")
        result = player.command("n")
        assertThat(result).contains("nondescript")
        player.command("s") // get back mama. maybe do this in other order?
        result = player.command("e")
        assertThat(result).contains("You have been eaten")
    }

Now, looking at this in the light of this morning’s first article, about TDD, I have to ask myself whether this is a “good” TDD test, or not.

Now it did get built up in three or four passes. I like tests that tell a bit of a story, at least sometimes, and however you cut it, setting up a new kind of room requires me to write some DSL, as in the test above. Then the tests places the player in the lighted room, goes south, gets a message about a grue, goes north to find the normal room, and then goes back to the dark room, tries some other move and gets eaten. That was build up in about four steps, first the DSL, then one move, and so on.

And the code required was quite short. A new action method that sets the room’s default so that it sees everything the player might type, which required an addition to an interface and concrete class:

    fun action(action:Action)
    override fun action(action: Action) {
        add(Phrase(), action)
    }

I elaborated the world-building part of the test a few times, which is actually not part of testing so much as implementing a dark room … inside a test, but it could be anywhere … and soon probably will be.

Now I could have done even smaller tests, I suppose. I could have tested just the all-consuming action with its own test. I could have written a test to be sure that notHandled worked as I thought it did. But those ideas were exercised by the test I had, so while going smaller was possible, I didn’t feel it necessary. Had things started to go awry, I’d likely drop down to smaller tests.

Is this TDD? I don’t know or care. It’s part of moving with small tests and small steps, and that’s part of my sweet spot in programming. I feel that it’s part of my TDD “practice”, perhaps out near the larger fringes, but definitely part of the style as I perceive it.

Let’s move on. I’ll begin by figuring out what I want this room to do and picking a bit to slice off.

The Dark Room

I think the core idea that I’m settling on is that there can be just one “dark room” in the game. When the player finds herself in the dark, the game teleports her to the dark room, where all the code for danger, scary messages, and what all, can be centralized. When the player does something to get back into the light (typically by lighting a lamp), we’ll teleport her back to the room she was in before. Which raises the question: how do we know where she was before? Let’s make that the next slice.

The player is in a room. They go south and are in a dark room. (The only dark room, in nascent form.) They say “lamp on” and find themselves back where they were. Let’s write the test. We’ll do it in a new test even though that will require us to duplicate some of the dark room code in the other test.

    @Test
    fun `lamp on returns player to prior room`() {
        val world = world {
            room(R.Z_FIRST) {
                desc("You are in well-lighted room.",
                    "You are in a well-lighted room. A darkened hall leads south.")
                go(D.South, R.Darkness)
            }
            room(R.Darkness) {
                desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
                action {
                }
            }
        }
    }

There’s the basic setup. As written, there’s no way out of R.Darkness. Let’s write the expectations now.

    val player = Player(world, R.Z_FIRST)
    var result = player.command("s")
    assertThat(result).contains("Darkness")
    result = player.command("lamp on")
    assertThat(result).contains("well-lighted")

Testing this should fail looking for “well-lighted”.

Expecting:
 <"Darkness
">
to contain:
 <"well-lighted"> 

Perfect. Now we “just” need to field the command and get back to the previous room.

    action {
        if (it.verb == "lamp" && it.noun=="on") {
            response.goToPriorRoom()
        }

I’m not sure whether response can implement that method or not. It was the first object I could think of. Let’s look and see how “go” works. Yes:

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

It all comes down to telling the response to move. So if response would be kind enough to remember the previous room, and to give us a method to go back to it, we should be in good shape.

Now, I can think of reasons not to like this approach. It might be better for this room to remember the prior room, because we might want to allow wandering in the dark or something. That’s not our current design, however, so I think we’ll go with this plan.

So in GameResponse, we’ll let IDEA create a method frame. Here’s the whole object with the added frame:

class GameResponse(startingRoomName:R = R.Z_FIRST) {
    var nextRoomName = startingRoomName
        get() = field // used only by tests
    var nextRoom: Room = nextRoomName.room
        get() = field

    val resultString: String get() = sayings + nextRoom.description() +"\n"+ nextRoom.itemString()
    var sayings = ""

    fun goToPriorRoom() {
        TODO("Not yet implemented")
    }

    fun moveToRoomNamed(roomName: R) {
        nextRoomName = roomName
        nextRoom = roomName.room
    }

    fun say(s:String) {
        sayings += s+"\n"
    }
}

The method moveToRoomNamed is preserving our requirement that nextRoomName and nextRoom are synchronized. I think that’s weird. We’ll talk about it later. Anyway, now we want a prior room name to refer back to. So we need to declare it in the object, init it to something reasonable, and update it on each move. Like this:

class GameResponse(startingRoomName:R = R.Z_FIRST) {
    var nextRoomName = startingRoomName
        get() = field // used only by tests
    private var priorRoomName = startingRoomName
    var nextRoom: Room = nextRoomName.room
        get() = field

    val resultString: String get() = sayings + nextRoom.description() +"\n"+ nextRoom.itemString()
    var sayings = ""

    fun goToPriorRoom() {
        moveToRoomNamed(priorRoomName)
    }

I kind of expect this to make the test run correctly. But it doesn’t. I still get the same message:

Expecting:
 <"Darkness
">
to contain:
 <"well-lighted"> 

Because I don’t understand why this happened, I try this in the dark room:

    action {
        if (it.verb == "lamp" && it.noun=="on") {
            response.moveToRoomNamed(R.Z_FIRST)
        }
    }

The test passes. I’ve done something dull. But what?

After too much digging, I am confused about how to keep track of the prior room. The thing is this:

When the player does a command, she asks the world to do the command in her current room:

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoom)
        currentRoom = response.nextRoom
        return response.resultString
    }

And the world creates a new response pointing to the room provided by the player. We do this because we’re allowing for there to be more than one player in the game at the same time, so only the player knows where they are. All the things I did to try to keep the prior room in the response were futile. It doesn’t know history.

Now the player could know the previous room. And they could pass it to the world, which could pass it to the response. So we could then ask the response to go to the prior room. Possibly.

Rollback

Let’s try it. I have rolled back all the code in the game, retaining only my test, which is asking me for goToPriorRoom() in response. Let’s let IDEA track us through this again.

In GameResponse:

    fun goToPriorRoom() {
        TODO("Not yet implemented")
    }

We’ll posit priorRoomName …

    fun goToPriorRoom() {
        moveToRoomNamed(priorRoomName)
    }

We’ll assume we’re passed this on creation:

class GameResponse(startingRoomName:R = R.Z_FIRST, var priorRoomName: R) {

And change the callers … I immediately see that I need a default:

class GameResponse(startingRoomName:R = R.Z_FIRST, var priorRoomName: R = startingRoomName) {
class World ...
    fun command(cmd: Command, currentRoom: Room): GameResponse {
        response = GameResponse(currentRoom.roomName)
        currentRoom.command(cmd, this)
        return response
    }

Here, I’ll demand to be given one, but I’ll default if not:

    fun command(cmd: Command, currentRoom: Room, priorRoomName: R = currentRoom.roomName ): GameResponse {
        response = GameResponse(currentRoom.roomName, priorRoomName)
        currentRoom.command(cmd, this)
        return response
    }

Senders of that. Innumerable tests, but the defaults should deal with those. For now. This is a bit tacky.

class Player(private val world: World, startingName: R) {
    var currentRoom = startingName.room
    var priorRoomName = startingName

    val currentRoomName get() = currentRoom.roomName

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoom, priorRoomName )
        priorRoomName = currentRoomName
        currentRoom = response.nextRoom
        return response.resultString
    }
}

Let’s test. I think this should work, but it is too deep for my brain, I need the computer to tell me what happened.

Perfect, we are green. But I bet I can break it. I think this may not be robust enough.

Right. This test fails. It does another command before turning on the lamp. Since every command is a move from a room to a room, possibly the same room, this loses the prior room name that was present when first we arrived.

    @Test
    fun `lamp on returns player to prior room`() {
        val world = world {
            room(R.Z_FIRST) {
                desc("You are in well-lighted room.",
                    "You are in a well-lighted room. A darkened hall leads south.")
                go(D.South, R.Darkness)
            }
            room(R.Darkness) {
                desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
                action {
                    if (it.verb == "lamp" && it.noun=="on") {
                        response.goToPriorRoom()
                    } else if (it.verb=="do" && it.noun == "something"){
                        // ignore command
                    }
                }
            }
        }
        val player = Player(world, R.Z_FIRST)
        var result = player.command("s")
        assertThat(result).contains("Darkness")
        player.command("do something")
        result = player.command("lamp on")
        assertThat(result).contains("well-lighted")
    }

Rollback

We have to go back to the drawing board. I’ll roll back everything except this test. Done. The test won’t even compile now, because it says this:

    action {
        if (it.verb == "lamp" && it.noun=="on") {
            response.goToPriorRoom()
        } else if (it.verb=="do" && it.noun == "something"){
            // ignore command
        }
    }

The “bug”, if you will, is the requirement that the game accommodate more than one player in the cave at once. This requirement is more than a little speculative, since we expect to have zero users, not several, and the original game was a single-player game.

Or is that the real problem? Even if there were only one player, since we move from room A to room A on every command, we’ll wipe out the prior move even for a single player.

Aside
Where is inventory kept? You guessed it: it’s in the world, not the player. The multi-player idea is at best p-baked for small p.

Perhaps the concept we have in mind for priorRoom is “the room you were last in that isn’t this room”. I almost wish I had kept that code that I just rolled back. Perhaps the trick is not to update priorRoom if we’re going to set it to the current room.

My mind goes to the notion of a move stack, containing all the rooms we’ve been in, in the order we were in them, without adjacent duplicates. Then we could just pop the stack to go back. That’s too much mechanism for what we have in mind. We only have one case of this.

But we need to remember these key facts:

  1. We create a new GameResponse on each command.
  2. Presently, the current room is remembered in Player and provided to response as a creation parameter.
  3. A room only retains one bit of game state: what it contains. That is OK even with multiple players, because if Joe gets the axe, it shouldn’t be there for Jane. Sorry, Jane.

There’s more. We represent our room names with an enum, R, and sometimes we refer to the name, and sometimes to the Room instance associated with the name. That instance is created when the enum is created, and get recreated when the game starts, via a call to R.freshRoom() when the room is created in the DSL.

Pop Your Mind, Ron

When our program is too complicated to put something in, make the program simpler, then update the simpler program.

The GameResponse is created on every command. And it contains this info at present:

class GameResponse(startingRoomName:R = R.Z_FIRST) {
    var nextRoomName = startingRoomName
        get() = field // used only by tests
    var nextRoom: Room = nextRoomName.room
        get() = field

    val resultString: String get() = sayings + nextRoom.description() +"\n"+ nextRoom.itemString()
    var sayings = ""

    fun moveToRoomNamed(roomName: R) {
        nextRoomName = roomName
        nextRoom = roomName.room
    }

    fun say(s:String) {
        sayings += s+"\n"
    }
}

Let’s simplify it by remembering just the room names, not the names and rooms:

class GameResponse(startingRoomName:R = R.Z_FIRST) {
    var nextRoomName = startingRoomName
        get() = field // used only by tests
    val nextRoom: Room 
        get() = nextRoomName.room

If I correctly understand Kotlin, I just changed nextRoom from being something stored, to a computed property, fetched by referring to it as it if is a property. And I changed the move function:

    fun moveToRoomNamed(roomName: R) {
        nextRoomName = roomName
    }

Test to see that this works as I believe it does. We’re green. Is anyone else holding on to the room? Who does what with that member?

The Player grabs it:

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoom)
        currentRoom = response.nextRoom
        return response.resultString
    }

And passes it to world. Better to pass the name, not the room. I think we want to treat the enum more and more like a room. We’ll go in two steps:

class Player(private val world: World, startingName: R) {
    var currentRoomName = startingName
    val currentRoom get() = currentRoomName.room

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoom)
        currentRoomName = response.nextRoomName
        return response.resultString
    }
}

Test. Green. One more change before we commit: passing the name to world.command, not the room. We want to change this signature:

    fun command(cmd: Command, currentRoom: Room): GameResponse {
        response = GameResponse(currentRoom.roomName)
        currentRoom.command(cmd, this)
        return response
    }

To expect currentRoomName: R.

    fun command(cmd: Command, currentRoomName: R): GameResponse {
        response = GameResponse(currentRoomName)
        currentRoomName.room.command(cmd, this)
        return response
    }

We will now have a number of callers of this method to fix. Let’s do that.

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoomName)
        currentRoomName = response.nextRoomName
        return response.resultString
    }

This test had to be recast:

        val myRoomName = R.Z_FIRST
        val secondRoomName = R.Z_SECOND
        val cmd = Command("s")
        val resp1 = myWorld.command(cmd, myRoomName)
        assertThat(resp1.nextRoomName).isEqualTo(R.Z_FIRST)
        assertThat(resp1.sayings).isEqualTo("The grate is closed!\n")
        val resp2 = myWorld.command(Command("s"), secondRoomName)
        assertThat(resp2.nextRoomName).isEqualTo(R.Z_SECOND)

I think there may be another few. They all go the same as the one above. Green. Commit: world.command and player.command now use room name, not room.

What else can we simplify here?

    fun command(cmd: Command, currentRoomName: R): GameResponse {
        response = GameResponse(currentRoomName)
        currentRoomName.room.command(cmd, this)
        return response
    }

A little feature envy there. Change:

    fun command(cmd: Command, currentRoomName: R): GameResponse {
        response = GameResponse(currentRoomName)
        currentRoomName.command(cmd, this)
        return response
    }

    fun command(cmd: Command, world: World) = room.command(cmd,world)

Test. Green. Commit: world.command forwards to currentRoomName:R, which forwards to the room.

I think we’ve simplified things a bit. Most folks don’t care about the name any more. Let’s see who’s looking at it and why.

In GameResponse we use our nextRoom function, which could be private, as follows:

    val resultString: String get() = sayings + nextRoom.description() +"\n"+ nextRoom.itemString()

We can forward both of those calls to nextRoomName.

Forwarding these:

    val resultString: String get() = sayings + nextRoomName.description() + "\n"+ nextRoomName.itemString()

Makes nextRoom redundant. Remove it. Commit: GameResponse forwards to roomName:R and no longer needs nextRoom.

I look for people sending room. to a name:

   textContents + "\n"
           + player.currentRoom.description() + ".\n"
           + player.currentRoom.itemString()

Those can be sent to the name. Now I think we don’t need currenRoom in player. Test, commit: i think no one is using room, currentRoom, or the like. Everyone refers to roomName, the R enum.

Heads up, reflect. Where are we on our original need, the previous room?

Reflection

We’ve made the design simpler by referring always to room name, which is an instance of the R enum. The name, while accurate, is misleading, in that for all intents and purposes, these things are the room to most folks. We won’t try to unwind that naming issue right now.

We want a command in response: goToPriorRoom. What do we want that room to be? We want it to be the most recent room we’ve been in, other than the current room.

Now presently, it is not legit to keep track of the prior room in the World, because of the specious requirement to allow for multiple players in the same world. Should we ditch that requirement, or not?

Let’s see if we can implement logic in Player to manage the prior room (name) properly. I think we’d like to have a test for that directly.

    @Test
    fun `player knows prior room`() {
        val world = world {
            room(R.Z_FIRST) {
                desc("F", "F")
                go(D.South, R.Z_SECOND)
            }
            room(R.Z_SECOND) {
                desc("S","S")
            }
        }
        val player = Player(world, R.Z_FIRST)
        assertThat(player.priorRoomName).isEqualTo(R.Z_FIRST)
    }

I implement just this on Player:

class Player(private val world: World, startingName: R) {
    var currentRoomName = startingName
    var priorRoomName = startingName

And the test runs so far. It’ll even pass this, because prior never changes:

        val player = Player(world, R.Z_FIRST)
        assertThat(player.priorRoomName).isEqualTo(R.Z_FIRST)
        player.command("s")
        assertThat(player.currentRoomName).isEqualTo(R.Z_SECOND)
        assertThat(player.priorRoomName).isEqualTo(R.Z_FIRST)

But I expect this to fail:

        val player = Player(world, R.Z_FIRST)
        assertThat(player.priorRoomName).isEqualTo(R.Z_FIRST)
        player.command("s")
        assertThat(player.currentRoomName).isEqualTo(R.Z_SECOND)
        assertThat(player.priorRoomName).isEqualTo(R.Z_FIRST)
        player.command("n")
        assertThat(player.currentRoomName).isEqualTo(R.Z_FIRST)
        assertThat(player.priorRoomName).describedAs("second prior").isEqualTo(R.Z_SECOND)

Sure enough, there’s no recollection of anything beyond the first setting of priorRoomName.

[second prior] 
Expecting:
 <Z_FIRST>
to be equal to:
 <Z_SECOND>
but was not.

Now what if we did this?

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoomName)
        if (response.nextRoomName != currentRoomName) priorRoomName = currentRoomName 
        currentRoomName = response.nextRoomName
        return response.resultString
    }

Here, if we come back with a next room name that is NOT the same as the current one, we save the current one in prior before setting up for the new one. I think our test passes now.

It does. Let’s make it harder, by doing something in the room.

        player.command("n")
        assertThat(player.currentRoomName).isEqualTo(R.Z_FIRST)
        assertThat(player.priorRoomName).describedAs("second prior").isEqualTo(R.Z_SECOND)
        player.command("inventory")
        assertThat(player.currentRoomName).isEqualTo(R.Z_FIRST)
        assertThat(player.priorRoomName).describedAs("second prior").isEqualTo(R.Z_SECOND)

Still green. Now if only we had some way for the room to ask what the priorRoomName was.

I think we can only do this if we allow the world or response to know the player. For that to happen, the world has to know:

class World ...
    fun command(cmd: Command, currentRoomName: R, player: Player): GameResponse {
        response = GameResponse(currentRoomName)
        currentRoomName.command(cmd, this)
        return response
    }

We have to hook up the callers. There are a few. If there were many, I’d go another way.

    fun command(commandString: String): String {
        val response = world.command(Command(commandString), currentRoomName, this)
        if (response.nextRoomName != currentRoomName) priorRoomName = currentRoomName
        currentRoomName = response.nextRoomName
        return response.resultString
    }

        val fakePlayer = Player(myWorld,R.Z_FIRST)
        val resp1 = myWorld.command(cmd, myRoomName, fakePlayer)
        assertThat(resp1.nextRoomName).isEqualTo(R.Z_FIRST)
        assertThat(resp1.sayings).isEqualTo("The grate is closed!\n")
        val resp2 = myWorld.command(Command("s"), secondRoomName, fakePlayer)
        assertThat(resp2.nextRoomName).isEqualTo(R.Z_SECOND)

In the real player.command, I just pass the player. In the test, I create a fake (but valid) player, and pass it. Another few tests are similar. Green. Commit: world knows current player during command execution.

Now I think I can update our original test and make it work. That turns out to be the case:

    @Test
    fun `lamp on returns player to prior room`() {
        val world = world {
            room(R.Z_FIRST) {
                desc("You are in well-lighted room.",
                    "You are in a well-lighted room. A darkened hall leads south.")
                go(D.South, R.Darkness)
            }
            room(R.Darkness) {
                desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
                action {
                    if (it.verb == "lamp" && it.noun=="on") {
                        response.goToPriorRoom()
                    } else if (it.verb=="do" && it.noun == "something"){
                        // ignore command
                    }
                }
            }
        }
        val player = Player(world, R.Z_FIRST)
        var result = player.command("s")
        assertThat(result).contains("Darkness")
        player.command("do something")
        result = player.command("lamp on")
        assertThat(result).contains("well-lighted")
    }

class GameResponse ...
    var player: Player? = null

    fun definePlayer(currentPlayer: Player) {
        this.player = currentPlayer
    }

    fun goToPriorRoom() {
        val name = player?.priorRoomName
        if (name != null ) moveToRoomNamed(name)
    }

class World ...
    fun command(cmd: Command, currentRoomName: R, player: Player): GameResponse {
        response = GameResponse(currentRoomName).also { it.definePlayer(player) }
        currentRoomName.command(cmd, this)
        return response
    }

This is rickety, even though it does work. GameResponse has a new member variable player, which can be null. It is defined when World creates a gameResponse. And, then, if a room tries to go to prior room, we check to see if we have a player, get its room name if we have one, and use it in the move.

What’s not to like? Well, a potentially null reference that requires us to check not once but twice for null. The .also that sets it, which is really begging us to have the GameResponse always expect a player. However … the test runs, and I’m fairly sure, about p = 0.6, that it’s a solid solution. And I’m quite tired: I’ve been writing since 0800 and it is after 1300 now. This little feature has caused me to do at least three partial rollbacks, and, once I got clear on who knows what and when they know it, required me to string a new thread from player to response, via the command methods.

I think we’ll be glad, someday, to have a player in the response object, because we should really be keeping the inventory in the player, unless we back away from our multi-user requirement.

But it’s rickety. We’ll commit it, because it works, and we’ll come back when we’re fresh to see about making it less rickety. Commit: Dark room (or any room) can return to prior room by messaging the response. GameResponse now optionally knows the Player. Should probably always know the Player, or at least a fake one. TODO.

Let’s try a summary. I think this will be inadequate: I’m on overtime and running on fumes.

Summary

We have what seems like a fairly simple problem, remembering what room you were most recently in that isn’t the room you’re currently in. This problem was made more difficult than it seems because:

  1. The game communicates almost entirely through the GameResponse, which has no long-term memory: We get a fresh one on each command.
  2. The game always moves you to a room for each command. If the prior command didn’t move you to a new room, you move to the old one. This makes keeping the prior room tricky, because often the prior room is the same as the current room, so we shouldn’t clobber the prior.
  3. The player holds the turn-to-turn knowledge of the current room, which needs to be passed in to commands and such.
  4. The various objects [used to] know both the room name and the room itself. Now they all know the name, which gets resolved to a room instance at the last moment.

I think my use of the also to pass in a prior room name if I have one was interesting, but in the sense of green shoes: interesting, but not generally sought after.

I wonder whether perhaps we don’t have the right split between player and game response. I really wanted the response to be immutable, with a new one popping up whenever we do a new command. So it’s probably wrong for the response to hold a player, which is a long-lived object.

Perhaps we should be passing the Player into the command and asking the player to produce a response? One way or another we need to think about the lifetimes of these objects.

All that’s for next time. My brain is fried for now.

But what about TDD? Has it failed somehow? I think not. My existing tests, and two new ones, enabled me to zero in on what would work. They served as stable points, scaffolding on which I could stand while manipulating the objects of the system. Without that stable platform … I wouldn’t want to be there at all. I can only move the earth if I have a place to stand.

Enough for now. See you next time! Maybe later today, more likely tomorrow.