Some interesting insights from last night’s Zoom inspire me to morph my design. A lot of thinking, a little coding. A decent direction, I think.

The Friday Zoom Ensemble Last Night (Tuesday)

I plan to write about the session1 itself later, time permitting ATCDR2, because it was a very delicious meeting. Meanwhile, you can take a look at this thread from GeePaw Hill. We had an amazing and deep, wild session.

Before we really got going, we reviewed my Room code and talked about ways to improve it. We then compared it to Hill’s DSL which is superficially similar to mine, and yet quite different. In particular, he has a design in mind, or at least some solid design directions, while I myself have not. We discussed our different styles. I may talk about this at another time but let me put down some summary thoughts here:

Vague and yet real differences

Both GeePaw and I work with very small steps, we work with TDD-stye tests—microtests, as he styles them—and wee agree on almost the principles of software development that I write about here. But there is one big difference in our styles. We even pretty much agree on the difference, and we do not argue about it because we both know that it’s just overlapping areas on some spectrum.

Briefly, Hill has much more of a design in mind when he programs, and I have much less. We are both thinking all the time about design, but our focus is different, in that he has in mind a pretty solid although changing view of what he wants the design to be, and I have a changing view of what the design and I want it to be.

Doesn’t sound very different. It isn’t, and it is. Part of the difference comes from what I’m trying to do. I may have a large design in mind (in fact we’ll work on one today) but I do all my work, not so much aiming for that design as in taking tiny steps that are mostly justified by the code as it is, and very little—but somewhat—justified by the larger picture. I will rarely do something now that is intended for the future. My changes are made now, to make things slightly better now. Hill is more likely to set up a structure or do something to move the code in the larger desired direction.

Still doesn’t sound very different, even to me. But it is, and we both see it, and feel it. I’ll try once more.

I usually make small changes that the code wants now, while sort of leaning in a larger direction. Hill makes small changes that will take him in the desired direction.

I preserve or create good design locally while thinking of the larger aspects, while Hill thinks of the larger aspects while making small changes that preserve or create good design locally and globally.

Those don’t seem very different, do they? And yet, they are different enough that we can sense the difference and agree about it.

Which is right? Well, we both are. Or neither of us.

Let’s get to work.

Improving My Program

I need to talk a bit about some larger design ideas that came out last night. (See? I do think about larger design.) We were reviewing Room, in particular those lambdas that are called to do the things that the game needs. Here’s the main command handler and a couple of those lambdas:

    fun command(cmd: String, world: World) {
        val command = Command(cmd).validate()
        val action: (Command, World)->String = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        val name:String = action(command, world)
        world.response.nextRoomName = name
    }

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world))
            targetName
        else
            roomName
    }

    val take = { command: Command, world: World ->
        val done = contents.remove(command.noun)
        if ( done ) {
            world.addToInventory(command.noun)
            world.response.say("${command.noun} taken.")
        } else {
            world.response.say("I see no ${command.noun} here!")
        }
        roomName
    }

We talked around this topic a fair amount. My original question to the group was, essentially, “what would be better than this?” We talked about some options, and I think the preferred one was the idea that these lambdas might belong inside the Command object, not the Room. It’s pretty clear that they must belong to Command or World, since they almost never reference the Room, except for one detail: they usually return roomName, which is a member variable of the Room. Returning it means that the next command takes place in the same room as this one.

Since all the lambdas can return roomName, and most of them do while one or two can return another room name, that duplication should be removed somehow. My idea is that since we have the world to talk to, we should just tell it, or the response, directly.

All the discussion reminded me of an overall design issue that I’ve had in mind for these lambdas anyway. There are two reasons to make the room’s commands dynamic, whatever for they take. First, the way the thing is coded now, if a room designer wants a new magic word, say “zork”, they’d have to impose on me to implement it, by putting it into one of my tables used in the Command’s parsing. Second, and not unrelated, if they want any particular special behavior in a room, there’s no way to define that, and, again, they might call upon me to build something for them.

So what I’ve had in mind, and I think I’ve mentioned it in the past, is that there should be a table of word->action, and that using the DSL, the game designer should be able to add words and actions to that table, both to be taken anywhere … a new magic word that charges your radio battery, resulting in some random words … or in a particular room or rooms, where charging the radio battery gives you a special useful message.

This might be “coded” by having two or more command lists, populated dynamically by the DSL, which come into play either all the time, or in particular rooms.

I think this will become more clear as we do it. Suffice to say that I think that the command’s word->action should be represented in one or more tables, and those should be able to be populated with the DSL as well as with regular Kotlin code.

We’ll start moving in that direction, right now.

Change the return of the lambdas

In command, we see that the lambdas return a string, the name of the room to “move to” after the command. It’s almost always the same room as we are currently in, roomName.

    fun command(cmd: String, world: World) {
        val command = Command(cmd).validate()
        val action: (Command, World)->String = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        val name:String = action(command, world)
        world.response.nextRoomName = name
    }

Let’s change this. When a command wants to change the room, for now we’ll have it do the assignment that’s at the bottom of this method, and we’ll ignore the return here.

Where I’m going is to change the signature of all those lambdas to return nothing but let’s go in tiny steps. We’re green now, on a fresh commit.

    fun command(cmd: String, world: World) {
        world.response.nextRoomName = roomName
        val command = Command(cmd).validate()
        val action: (Command, World)->String = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        val name:String = action(command, world)
//        world.response.nextRoomName = name
    }

This alone would break tests, but there’s just one place that returns something other than roomName:

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world))
            targetName
        else
            roomName
    }

We’ll add the assignment of nextRoomName here:

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world)) {
            world.response.nextRoomName = targetName
            targetName
        } else
            roomName
    }

I expect this to pass the tests, unless there is another return of a different room name somewhere.

A pity. Some tests do fail. Let’s see what’s up. Ah, they are all “Value has already been assigned”. I reckon that’s a val and we need a var for now:

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 think we remove the by singleAssign on nextRoomName and try again. Kotlin and IDEA require me to add the lateinit modifier. I’m not adept at that but we’ll let it be. Test. Green. Commit: return value from command lambdas no longer used, nextRoomName assigned directly.

With that done, I can remove the name that’s returned from the call to action:

    fun command(cmd: String, world: World) {
        world.response.nextRoomName = roomName
        val command = Command(cmd).validate()
        val action: (Command, World)->String = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        val name:String = action(command, world)
//        world.response.nextRoomName = name
    }

You can see that I was pretty tentative about that change. I wonder why. Anyway:

    fun command(cmd: String, world: World) {
        world.response.nextRoomName = roomName
        val command = Command(cmd).validate()
        val action: (Command, World)->String = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

Now, we really should put a method on Response for this, but let’ change the signature of the action instead and fix them all up.

    fun command(cmd: String, world: World) {
        world.response.nextRoomName = roomName
        val command = Command(cmd).validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

This is what we want … and now all the arrows below point to a red squiggled item, because they’re all wrong. We’re here to change that:

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world)) world.response.nextRoomName = targetName
    }

    private val unknown = { command: Command, world: World ->
        world.response.say("unknown command '${command.verb} ${command.noun}'")
    }

    private val inventory = { _:Command, world:World ->
        world.showInventory()
    }

    private val take = { command: Command, world: World ->
        val done = contents.remove(command.noun)
        if ( done ) {
            world.addToInventory(command.noun)
            world.response.say("${command.noun} taken.")
        } else {
            world.response.say("I see no ${command.noun} here!")
        }
    }

    private val castSpell = { command: Command, world: World ->
        when (command.noun) {
            "wd40" -> {
                world.flags.get("unlocked").set(true)
                world.response.say("The magic wd40 works! The padlock is unlocked!")
            }
            "xyzzy" -> {
                move(command, world)
            }
            else -> {
                world.response.say("Nothing happens here.")
            }
        }
    }

Green. Commit: command lambdas now return Unit. Let’s reflect about where we’re going.

Reflection

Looking at those lambdas, they have almost no references to the Room. But a close look has told me that there is at least one:

    val move = {command: Command, world: World ->
        val (targetName, allowed) = moves.getValue(command.noun)
        if (allowed(world)) world.response.nextRoomName = targetName
    }

The moves reference is definitely a member in the Room:

class Room(val roomName: String) {
    val contents = mutableSetOf<String>()
    private val moves = mutableMapOf<String,GoTarget>().withDefault { Pair(roomName) { _: World -> true } }
    var shortDesc = ""
    var longDesc = ""

And the declaration reminds me that we have a reference to contents as well. If we’re to move those lambdas out into some kind of table, we’re going to need to be able to get the contents and the moves (and perhaps other future variables of the room). We need to sort this and we are led to an interesting issue, the relationship between Game and World.

I’ve commented before that these two classes seem to be sharing one responsibility. One odd thing is that Game knows the current room, and provides it to World, when Game calls World.command. The sequence looks like this, starting at Game:

class Game(val world: World, startingName: String) {
    val resultString: String get() = world.resultString
    var currentRoom = world.unsafeRoomNamed(startingName)
    val currentRoomName get() = currentRoom.roomName
    val roomReferences: Set<String> get() = world.roomReferences

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

That’s all there is to Game. And in World.command:

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

I think in principle, that there is no need for the Game class, though you could make an argument for it. We could consider the world to just represent the rooms and their contents. The world has no notion of “current room”, even now. It just knows all the rooms and all their contents. (It does know inventory, which is a anomaly, and I think I’m getting an idea what to do about it.) The Game knows current room and provides it to the World for executing a command. Game is a “cursor” in the World.

Perhaps Game is trying to be the player.

We could imagine a game with more than one player in the same world. If one player takes the bird, it’s gone from the room and in that player’s inventory. If another player comes to that room, there’s no bird to be found.

I don’t have even the most minimal intention of making this exercise into a multi-player exploration game, but the thought experiment suggests that we might think of the Game as the player.

That’s so compelling an idea that I propose to change the class name.

Wow. Rename in IDEA does a wonderful job. It even renames variables and tests and even a test file for me. I think it even did it right. We are green. Commit: Rename Game->Player.

This may focus my mind on separating the concerns of the two classes, Player and World. In particular, it now makes sense for Player to have currentRoom, and suggests that it should have inventory, while World would continue to have moves.

I’m two hours in. Let’s think a bit about where we might go next.

Reflection

The general direction we’re heading is to house the command lambdas somewhere that makes more sense than in code in the Room. What makes sense? Certainly the available words and meanings are aspects of the World. They are needed by command parsing and interpretation, but are about the world. The “xyzzy” command is not available in every world. At least, when I say it, nothing happens here …

Since they’re defined, at least in part, by the particular world, and since we want our imaginary world designers to be able to define them, we can be pretty sure that wherever they reside, the World has access to them. During parsing, the Command will also need access. As written, the command function, residing in Room, has access to the world, so that would suffice to support an agreed format for the necessary words and meanings. We don’t need to define that now: we can work toward it.

Then there’s the GameResponse class. It looks like this:

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

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

Presently, the World gives itself a new response when command is called:

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

And, the Player (née Game3) provides the resultString in the application:

    fun someoneTyped() {
        val cmd = myCommand.text
        player.command(cmd)
        myText.appendText("\n> " + cmd)
        myText.appendText("\n"+player.resultString)
        myCommand.text = ""
        myCommand.appendText("")
    }

Player just gets that from the response, oddly via the world:

// Player
    val resultString: String get() = world.resultString
// World
    val resultString: String get() = response.resultString
// GameResponse
    val resultString: String get() = sayings + nextRoom.longDesc +"\n"+ nextRoom.itemString()

I think that the rules of engagement need a little revision. We’ve already had some difficulties testing World because it doesn’t always have a response. The response, currently named GameResponse, is a communications object shared between the Player and the World. It’s created by the World, but the World shouldn’t really want one. I think it’s probably a TurnResponse or CommandResponse, and that it should be passed in from Player in the command call.

Sort of an idea …

I’m beginning to think that the response should be in the Command, and that, if it’s needed, the response could have a pointer to the world, and that the Command should do all the parsing and execution, with reference to the world and room as it may need. And it would be ideal if the world did not know the response, and certainly shouldn’t create it.

Let’s start moving in that direction. But first a tiny digression:

I don’t have this figured out

I think GeePaw would have this figured out by now, or if he didn’t, would stop and think a bit more until he did. I could be wrong. Me, I just have a glimmer that this is the right direction, so I’ll try moving that way and see what the outlook is from there.

Move Response to Player

We’ll leave the member variable response in World, and we’ll even let it create the first one, because, I think, that’s relied upon by some tests. We should get rid of that init at some point. We’ll think of the response as being a conveniently cached parameter.

We change this:

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

To this:

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

I’m providing the default because I don’t want to edit a bunch of tests just now. But for the main command call:

class Player(val world: World, startingName: String) {
    val resultString: String get() = world.resultString
    var currentRoom = world.unsafeRoomNamed(startingName)
    val currentRoomName get() = currentRoom.roomName
    val roomReferences: Set<String> get() = world.roomReferences
    var response: GameResponse = GameResponse()

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

I expect green. I am disappointed, because “lateinit property nextRoomName has not been initialized”. I do not understand how this came about. What if I remove the provided one in Player and let it use the default again?

No, wait. I created it but didn’t pass it in. That can’t be good.

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

How can this be any different? Confused, I roll back. Oh, now I see it. I didn’t change the member variable in World.command. Let me make that change again, more correctly this time. From this:

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

To this:

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

Now tests should still be green … and they are. Now I can pass in my own GameResponse from Player:

class Player(val world: World, startingName: String) {
    val resultString: String get() = world.resultString
    var currentRoom = world.unsafeRoomNamed(startingName)
    val currentRoomName get() = currentRoom.roomName
    val roomReferences: Set<String> get() = world.roomReferences
    var response: GameResponse = GameResponse()

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

Green. Now let’s use our own response to get some of these results.

    val resultString: String get() = response.resultString

Green? Yes. Commit? Yes, let’s: Player provides and begins to use its own GameResponse instance.

What else? Well:

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

How does world return the room?

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

Since we are given the response and write into it, the room is there. Let’s not return it.

    fun command(cmd: String, currentRoom: Room, newResponse: GameResponse = GameResponse()) {
        response = newResponse
        currentRoom.command(cmd, this)
        response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
    }

And in Player, this:

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

Becomes this:

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

Green. Commit: Player now gets next room from response.

So that’s rather nice. Let’s reflect and see what we want to do now. It’s 1052 and I started at 0747, so I’m about 3 hours in.

Reflection

We’re moving in the direction of the World retaining no game- or player-specific information. Probably the largest violation of that at present is inventory.

Our larger goal is to get the various command lambdas moved off to some kind of table so that they can be defined or augmented in the DSL. And the lambdas clearly don’t belong on Room, where they now reside, since they don’t often reference the Room. (Although they do sometimes refer to moves and inventory.)

The moves clearly belong in World, as they specify the paths between Rooms. The inventory clearly does not. It “clearly” belongs in Player, in my somewhat tentative view. We really should have an Inventory object, by the way, since it’s a raw collection and we know that sooner or later we generally regret using vanilla collections. Let’s try creating it on Player, and having all the references to it bounce through the response. That means the response must know the player. I think that’ll be OK, except … we have all those default responses being created with no player.

Meh. Too messy. Need better idea. There’s really only one inventory at present. Maybe we just leave it alone for now, it’s harmless there, at least for now.

Sub-Reflection

It occurs to me that perhaps we’re building too tight a relationship between some of these objects and that “just” making certain properties observable and letting the observer stuff, however it might work, inform the right objects.

No, I think that is more mechanism than we need for this simple game. Maybe later.

One thing that is holding me back is the tests. The reason for making the response init itself on World creation, and auto-init on command was to support some tests. Some just don’t create a response in command. We can find and fix those by removing the default. Some are manipulating world directly, and those should be changed. They predate the Player-Game-World-Response structure.

Remove the Default Response

Let’s break ‘em and fix ‘em. Remove default game response from World and let it be lateinit.

Now we should get some compilation and test errors. Hm, no compilation errors, and only one test:

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val clearing:Room = world.unsafeRoomNamed("clearing")
        val command = Command("go n").validate()
        clearing.move(command, world)
        assertThat(world.response.nextRoomName).isEqualTo("woods")
    }

Let’s just do a real command here:

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        assertThat(world.roomCount).isEqualTo(2)
        assert(world.hasRoomNamed("clearing"))
        val player = Player(world, "clearing")
        player.command("go n")
        assertThat(world.response.nextRoomName).isEqualTo("woods")
    }

OK, now we always pass a response to the World when we issue a command. It happens to cache the response for its own convenience.

// World
    fun command(cmd: String, currentRoom: Room, newResponse: GameResponse) {
        response = newResponse
        currentRoom.command(cmd, this)
        response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
    }

// Room
    fun command(cmd: String, world: World) {
        world.response.nextRoomName = roomName
        val command = Command(cmd).validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

Thinking

Remark
It occurs to me that for a guy who earlier was talking about Hill doing more design thinking than the guy does, I’m doing a lot of design thinking. However, there’s a difference: I’m not committing to any large design solutions. I’m thinking about the whole space, and then making small moves on what seem like fruitful directions. Anyway ..

We’re on a path to removing the command logic from the room. Commands do use two aspects of the room, its contents, and its moves. There might be other such bits of info. However, we could make a case that a Command’s “context” is the room and the world it’s in, but that it is not in the room or the world.

Perhaps what the Player should do, given the command input line, is to create a Command instance, including the necessary context (room, world, response object, whatever), and tell the Command to execute. The command would do the necessary work to fill in the response, referring to its context.

In fact, if we were to create an actual Context object, it could contain pointers to world and room as needed, and if instead of asking it to provide those, we could give it all the setters and getters needed to make things work.

Let’s examine Command and see how difficult it would be to give it a context, allowing it to grow organically as we move forward.

class Command(val input: String) {
    val words = mutableListOf<String>()
    var operation = this::commandError
    var result: String = ""
    val verb get() = words[0]
    val noun get() = words[1]

    fun validate(): Command {
        return this
            .makeWords()
            .oneOrTwoWords()
            .goWords()
            .magicWords()
            .singleWordCommands()
            .errorIfOnlyOneWord()
            .findOperation()
    }

All those methods, except for findOperation, are about parsing, but some of them do know lists of words:

    fun goWords(): Command {
        val directions = listOf(
            "n","e","s","w","north","east","south","west",
            "nw","northwest", "sw","southwest", "ne", "northeast", "se", "southeast",
            "up","dn","down")
        return substituteSingle("go", directions)
    }

    fun magicWords(): Command {
        val magicWords = listOf("xyzzy", "plugh", "wd40")
        return substituteSingle("say", magicWords)
    }

    fun singleWordCommands(): Command {
        if (words.size == 2) return this
        val ignoredNounWords = listOf("inventory", "look")
        if (words[0] in ignoredNounWords) words.add("ignored")
        return this
    }

We could imagine that the direction words would be known here, as they might apply to all worlds. Even they, however, might be subject to expansion from the world, as the world builder adds them via the DSL. Maybe “go uphill” or something like that. the magic words seem clearly to be world-dependent, and we can certainly imagine the game designer wanting to add some new word, maybe “flee”, to the vocabulary. Maybe “flee” takes you a random number of steps in random directions, leaving you in some connected but random room. I don’t know, I can imagine it.

What does findOperation do? I think we’ll find that it just stores a value that we use in the tests:

    fun findOperation(): Command {
        operation = when (verb) {
            "take" -> ::take
            "go" -> ::go
            "say" -> ::say
            "inventory" -> ::inventory
            "verbError" -> ::verbError
            "countError" -> ::countError
            else -> ::commandError
        }
        return this
    }

Right. Also those are methods on command, which will be an issue. Anyway, let’s remove findOperation from validate and fix what breaks. I’m moving toward Command becoming more important to the Player.

Most of the CommandExperiment tests fail … all with command error. Let’s look at one of them.

    @Test
    fun `go command`() {
        val command = Command("go east")
        val result = command
            .validate()
            .execute()
        assertThat(result).isEqualTo("went east.")
    }

I think if we change the test to call findOperation, or add it to execute things will start to work. Let’s do the latter.

    fun execute(): String {
        result = findOperation().operation(noun)
        return result
    }

All green. Commit: modify Command’s test execution approach to remove findOperation from validate.

I’m thinking of creating a new Context class and passing it to Command. Let’s hold off on that and see about creating the command in Player and passing it to World:

// Player
    fun command(cmd: String) {
        response = GameResponse()
        world.command(cmd, currentRoom, response)
        currentRoom = response.nextRoom
    }

// World
    fun command(cmd: String, currentRoom: Room, newResponse: GameResponse) {
        response = newResponse
        currentRoom.command(cmd, this)
        response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
    }

This will ripple down to room. I’ll start at the top and see about fixing on down.

This could get dicey, but I’m ready to roll back if the going gets tough.

    fun command(commandString: String) {
        response = GameResponse()
        var command = Command(commandString)
        world.command(command, currentRoom, response)
        currentRoom = response.nextRoom
    }

The word “command” appears a lot there. I’ll let it ride … and fix:

In world:

    fun command(cmd: Command, currentRoom: Room, newResponse: GameResponse) {
        response = newResponse
        currentRoom.command(cmd, this)
        response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
    }

And in Room:

    fun command(command: Command, world: World) {
        world.response.nextRoomName = roomName
        command.validate()
        val action: (Command, World)->Unit = when(command.verb) {
            "inventory" -> inventory
            "take" -> take
            "go" -> move
            "say" -> castSpell
            else -> unknown
        }
        action(command, world)
    }

I expect this to work. No, a couple of tests don’t compile. Thanks, tests, that’s why we have you. There was probably an IDEA way to do this, but maybe not. Anyway:

With a bit of fiddling, I get the tests back to green. Changes not interesting enough to review. Commit: Player now creates command instance used in World and Room.

I’m now 4 1/2 hours in. Time flies when you’re thinking and writing. Let’s quickly reflect and sum up.

Reflecto-Summary

We’re moving toward the Command having more prominence in the design. I think that we’ll give it a context that will allow it access to the world and room as needed, and we might even be in a position to plug in different contexts for testing or—I don’t know—some other nefarious purpose.

Among the things the command will get from its context will be the various word lists. In the case of the actual Player-World-Room hookup, those will be associated somehow with the world, with perhaps some predefined elements or perhaps all the elements defined via the DSL. In tests, we might provide those lists in some other way, more focused on testing.

Last Night
We were saying that Hill’s design outlook probably causes him to do too much, and mine probably causes me to do too little. Today, I did a lot, and it hasn’t done me any noticeable harm, and we do seem to be going in a decent direction.

However, I don’t mind saying that I am seriously considering drawing a picture to help me envision where things might want to go. If I do, I’ll include it in the subsequent article. And even if I do draw a picture, I’ll feel very little compulsion to do what the picture says.

We’re munching forward, keeping our many tests running, and up to 115 commits so far. Not too impressive, 40 articles in, but not bad either. Seven today, eighteen yesterday, ten the day before. Not bad. Today was a slow day, all that thinking and a few tiny steps.

We’ll see what happens. Join me then!



  1. Hill refers to our Friday Night Zoom Ensemble, held each Tuesday evening, as “Friday Geek’s Night Out”. I myself do not, because I do not care for the word “geek”, perhaps because I associate it with a particular odious summer job back years ago. 

  2. “And The Crick Don’t Rise”. (The correct pronunciation, and in my view spelling, is “crick”. Some say “creek”, but they are wrong.) Cf. e.g. Wikipedia. I, of course, prefer the secular version. 

  3. Again with the French. Who is he fooling with the fake fancy talk? Surely not you and me.