A little refinement, and then let’s press forward on Picov Andropov objects.

The famous chauffeur Picov Andropov was one of the best at picking up and dropping off people, and we need the player in our game to be able to pick up and drop objects, perhaps even throw them. Yesterday, I guess it was, we got to where we can define what objects are present in a “room”, and display them.

    @Test
    fun `room has contents`() {
        val world = world {
            room("storage") {
                desc("storage room", "large storage room")
                item("broom")
                item("broom")
                item("water")
                item("axe")
            }
        }
        val room = world.unsafeRoomNamed("storage")
        assertThat(room.contents).contains("broom")
        assertThat(room.contents.size).isEqualTo(3)
        val itemString = room.itemString()
        assertThat(itemString).contains("axe")
        assertThat(itemString).contains("broom")
        assertThat(itemString).contains("water")
    }

Our itemString method was the best I could do quickly, but I was sure there had to be a better way than this:

    fun itemString(): String {
        var report = ""
        contents.forEach { report += "You find $it.\n" }
        return report
    }

I looked at fold and whatever the other thing is, but then found this solution:

Wait! Tests fail. I back out my change and they still fail. I’ve broken something and somehow was unaware of it. Surely I ran my tests. I remember typing “green” a lot. Anyway, let’s find the issue:

Expecting:
 <"The grate is closed!
You find yourself in the fascinating first room..
">
to be equal to:
 <"The grate is closed!
You find yourself in the fascinating first room.">
but was not.

I have an extra period appearing. The test:

    @Test
    fun `game can provide sayings`() {
        val myWorld = world {
            room("first") {
                desc("You're in the first room.", "You find yourself in the fascinating first room.")
                go("n", "second") { true }
                go("s","second") {
                    it.say("The grate is closed!")
                    false
                }
            }
            room("second") {
                desc("second room", "the long second room")
            }
        }
        val game = Game(myWorld, "first")
        game.command("s")
        assertThat(game.resultString).isEqualTo("The grate is closed!\n" +
                "You find yourself in the fascinating first room.")
    }

Well, we clearly have to decide what the format is going to be. Let’s say that the descriptions should be full sentences, ending with periods. Now we can find resultString and ensure that it hasn’t taken on the responsibility to add periods. (I am sure that it has.)

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

Right. Remove that period. Test. Still tests not passing …

 <"The grate is closed!
You find yourself in the fascinating first room.
">
to be equal to:
 <"The grate is closed!
You find yourself in the fascinating first room.">
but was not.

We are not expecting the return at the end. We should be. Fix the test. That one runs green.

Do you remember, several articles back, when I commented that newlines were going to become a problem? It’s happening. We’ll work toward more consistency … and toward avoiding the question of newlines when we can.

Expecting:
 <"The room is locked by a glowing lock
You are in an empty room in the palace.There is a padlocked door to the east
">
to be equal to:
 <"The room is locked by a glowing lock
You are in an empty room in the palace.There is a padlocked door to the east">
but was not.

This looks like we need to fix both occurrences of that string, one to get a space between the sentences and a period at the end, and one to expect the newline.

OK, I’ll spare you the rest, unless they are interesting … they are not.

Now, as I was saying, I found a better way to do this:

    fun itemString(): String {
        var report = ""
        contents.forEach { report += "You find $it.\n" }
        return report
    }

Namely this:

    fun itemString(): String {
        return contents.joinToString(separator = "") {"You find $it.\n"}
    }

This does the job nicely. My tests all pass, but I see an undesirable effect in the game-play output:

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> s
You're in a charming clearing. There is a fence to the east.
You find cows.

> e
You can't climb the fence!
You're in a charming clearing. There is a fence to the east.
You find cows.

> n
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> e
You're in a pasture with some cows.

I have an unexpected space between the commands. However, I think I like it, because in play, it will help separate one command from the next, making the output more readable. We’ll let that ride.

Tests are all green, I swear it. Commit: Clean up text layout, use joinToString in Room.itemString.

Reflection

It’s interesting how a bump in the road can upset a whole plan. My intention was to pop in, do the joinToString thing, which I thought I knew exactly how to do, and then move right on to doing something useful on the Inventory story. But instead, I found that some fool had left tests red, and I had to fix that, and then the nice clear joinToString that I had tested in a playground on my iPad wasn’t quite the thing, so I had to fiddle with that.

I am now 45 minutes past when I started. Had things gone as intended, it might have been 15. (It takes me a while to write this stuff, you know, and I’m still not terribly speedy with Kotlin/IDEA.) So, to mix a metaphor, the wind is out of my sails and I need a moment to figure out what to do next.

Here’s my list of “tasks” from yesterday:

  1. The command language will need to understand verbs vs nouns: “take keys” vs “take rod”.
  2. We’ll need a repository for all the nouns that the player has collected, an “Inventory”.
  3. We’ll need a verb “inventory” to display the current inventory.
  4. We'll need a place in each room for all the nouns that are currently there.
  5. We'll need to display the room's contents when we enter the room.

In the current scheme, I reckon the inventory will be a collection on the World object, and I think that the rules will be that there is only one of any given item, so that the collection should be a set, as is the collection of items in the room. I think we’ll want to do this with a test at the world level.

For what it’s worth, I’ve developed the habit of closing all my program tabs when I switch to the next task / phase / whatever, so that the only ones that are open are the ones I’m actively changing. It helps me keep organized among IDEA’s forty-eleven possible windows and tabs.

I’ll start with this test. I’m not sure about the syntax, but it’ll get us going.

    @Test
    fun `world has inventory`() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
        }
        world.take("axe")
        assertThat(world.inventoryHas("axe"))
    }

This demands take and hasInventory on world. We respond:

class World {
    ...
    val inventory = mutableSetOf<String>()
    ...
    fun take(item:String) {
        inventory += item
    }

    fun inventoryHas(item: String): Boolean {
        return inventory.contains(item)
    }

And the test is green. Commit: World has inventory, take, and inventoryHas.

Let’s do the inventory command.

Now curiously, commands are sent to the current room, but the command function has the world as a parameter, so we can do this. Currently:

    fun command(cmd: String, world: World) {
        val name = when(cmd) {
            "s","e","w","n" -> move(cmd, world)
            "xyzzy" -> move("xyzzy", world)
            "cast wd40" -> castSpell("wd40", world)
            else -> "unknown cmd $cmd"
        }
        world.response.nextRoomName = name
    }

Let’s just jam the command in there at the top.

    fun command(cmd: String, world: World) {
        val name = when(cmd) {
            "inventory" -> inventory(world)
            "s","e","w","n" -> move(cmd, world)
            "xyzzy" -> move("xyzzy", world)
            "cast wd40" -> castSpell("wd40", world)
            else -> "unknown cmd $cmd"
        }
        world.response.nextRoomName = name
    }
    
    private fun inventory(world: World) {
        world.showInventory()
    }

This demands showInventory on world. Observing that, I realize that I can test that directly, so I’ll add a test calling for it.

    @Test
    fun `world has inventory`() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
        }
        world.take("axe")
        world.take("bottle")
        assertThat(world.inventoryHas("axe"))
        val s: String = world.showInventory()
        assertThat(s).contains("axe")
        assertThat(s).contains("bottle")
    }

And now, in world, with a bit of hammering:

    fun showInventory(): String {
        return inventory.joinToString(prefix="You have ", separator=", ", postfix="\n")
    }

I also had to fix this in Room:

    private fun inventory(world: World): String {
        world.showInventory()
        return roomName
    }

That’s because all commands are supposed to return the (new) room you’re in after the command.

Tests are green. I would like to see the whole inventory string, and so far, I haven’t. So:

        assertThat(s).isEqualTo("")

This will fail and we’ll find out what we have.

Expecting:
 <"You have axe, bottle
">
to be equal to:
 <"">
but was not.

Ah. Suffix needs a period. and the syntax isn’t marvelous, but it’ll do. I think I can use this string, because my understanding is that a Kotlin Set respects insertion order. If not, we’ll find out. Tweak the test:

        assertThat(s).isEqualTo("You have axe, bottle.\n")

And tweak the code:

    fun showInventory(): String {
        return inventory.joinToString(prefix="You have ", separator=", ", postfix=".\n")
    }

We’re green. Commit: World has showInventory returning string.

Wait! Thinking about that name tells me that I’ve messed up. showInventory shouldn’t be returning a string, it should be putting the inventory into the response, probably with say. Let’s change this around. The tests are going to break but let’s get the code right.

    fun showInventory() {
        say( inventory.joinToString(prefix="You have ", separator=", ", postfix=".\n") )
    }

Now the test will surely break. Probably won’t compile, as it expects a string. Right. We have:

        }
        world.take("axe")
        world.take("bottle")
        assertThat(world.inventoryHas("axe"))
        val s: String = world.showInventory()
        assertThat(s).contains("axe")
        assertThat(s).contains("bottle")
        assertThat(s).isEqualTo("You have axe, bottle.\n")

I think we recast that this way:

    @Test
    fun `world has inventory`() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
        }
        world.take("axe")
        world.take("bottle")
        assertThat(world.inventoryHas("axe"))
        world.showInventory()
        val s: String = world.resultString
        assertThat(s).contains("axe")
        assertThat(s).contains("bottle")
        assertThat(s).isEqualTo("You have axe, bottle.\n")
    }

I get an error I’ve never seen before:

Value has not been assigned yet!

Surely that’s something wrong with the test setup. Tracking through the backtrace … it’s looking for nextRoom:

data class GameResponse(val name:String="GameResponse") {
    val resultString: String get() = sayings + nextRoom.longDesc +"\n"+ nextRoom.itemString()
    var sayings = ""
    var nextRoomName: String by singleAssign<String>()
    var nextRoom: Room by singleAssign<Room>()

I’m not sure why that isn’t assigned. We did a command … Ah! We can’t just tell the world to show inventory. We need to do the inventory command so that the response is set up.

After too much messing around, I think this test needs to be at the game level to actually pass. I am unable to resist testing it in the game, and it displays

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> inventory
You have .

You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

That tells me that we’re hooked up, and makes me more certain that moving this test over into the GameTests will make it work. Or, just copying what those tests do …

    @Test
    fun `world has inventory`() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
        }
        world.take("axe")
        world.take("bottle")
        assertThat(world.inventoryHas("axe"))
        val game = Game(world,"woods")
        game.command("inventory")
        val s: String = game.resultString
        assertThat(s).contains("axe")
        assertThat(s).contains("bottle")
        assertThat(s).isEqualTo("You have axe, bottle.\n")
    }

The test gives an interesting error on the last assert:

Expecting:
 <"You have axe, bottle.


">
to be equal to:
 <"You have axe, bottle.
">

I have two extra returns. I think I’ll ignore that for now, and add it to the test. We’ll see how it looks and deal with tuning up the formatting after we get the feature running in the game.

OK, green. Commit: Inventory command says inventory. Formatting odd, extra returns somewhere.

This is somewhat naff, I should really be sweating this detail, but I’ve got too many balls in the air1. Let’s get a rudimentary take working. I’ll skip full parsing and just recognize some take commands

First, I want to show the odd but satisfactory results of my hacked take:

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> take bottle
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> inventory
You have bottle.

You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> take axe
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> inventory
You have bottle, axe.

You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

Here’s how that was done:

    fun command(cmd: String, world: World) {
        val name = when(cmd) {
            "take axe" -> take("axe", world)
            "take bottle" -> take("bottle", world)
            "take cows" -> take("cows", world)
            "inventory" -> inventory(world)
            "s","e","w","n" -> move(cmd, world)
            "xyzzy" -> move("xyzzy", world)
            "cast wd40" -> castSpell("wd40", world)
            else -> "unknown cmd $cmd"
        }
        world.response.nextRoomName = name
    }

    private fun take(item:String, world: World): String {
        world.take(item)
        return roomName
    }

Let’s enhance that code just a bit. Forgive me for not testing this, please, I’m getting ragged over here.

    private fun take(item:String, world: World): String {
        world.take(item)
        world.response.say("$item taken.")
        return roomName
    }

Run again, just for fun.

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> take bottle
bottle taken.
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> e
You're in a pasture with some cows.

> take cows
cows taken.
You're in a pasture with some cows.

> inventory
You have bottle, cows.

You're in a pasture with some cows.

My brain is full, the tests are green. I’m going to commit and take a break. Commit: hacked take commands add items to inventory. Inventory command displays as intended.

My eyes and brain need a rest. Let’s sum up.

Summary

We’ve got some patchwork scaffolding in there, but we have a rudimentary version of all these items working:

  1. [?] The command language will need to understand verbs vs nouns: “take keys” vs “take rod”.
  2. [√] We’ll need a repository for all the nouns that the player has collected, an “Inventory”.
  3. [√] We’ll need a verb “inventory” to display the current inventory.
  4. [√] We’ll need a place in each room for all the nouns that are currently there.
  5. [√] We’ll need to display the room’s contents when we enter the room.

It all went smoothly, not least because we managed to slice off small steps to implement. I still stumbled a bit along the way, notably when I mistakenly returned a string but needed to add the string to the response.

And I stumbled a bit when I couldn’t just test via room, needing a configured Game to do the test properly. I discovered along the way that the Game isn’t really in a valid state before the first command. I’ve made a sticky for that.

Additionally, there are two mistakes I’ve made almost every time I implement a command. First, I forget to pass the world down to the detailed command, and, second, I forget to return the “next room” back. I’ve made a sticky for that.

I’m sure there are other weaknesses. Surely we need better sentences in the output, and better formatting. And we still aren’t really parsing commands.

Still, when you look at the game play sequence, it looks a lot like a game. I’m pleased with that.

What’s next? More with the parsing, and making take only take what’s available, and remove it from the room. Probably. We’ll find out. See you then!

Welcome to Tiny Adventure!
You're in a charming wellhouse.
You find keys.
You find bottle.
You find water.

> take bottle
bottle taken.
You're in a charming wellhouse
You find keys.
You find bottle.
You find water.

> s
You're in a charming clearing. There is a fence to the east.
You find cows.

> take cows
cows taken.
You're in a charming clearing. There is a fence to the east.
You find cows.

> e
You can't climb the fence!
You're in a charming clearing. There is a fence to the east.
You find cows.

> inventory
You have bottle, cows.

You're in a charming clearing. There is a fence to the east.
You find cows.


  1. He’s just chock full of metaphors today, isn’t he?