GitHub Repository

I have what I think is a very nice idea. Let’s talk about it and then start working toward it. Is this BUFD1?

The Good Idea
The game consists of rooms and actions, when you get right down to it. You go between the rooms, doing actions, and somehow it turns into fun. The software we’re writing is aimed at making creating such a game. It’s just an exercise in learning Kotlin, but we might as well do it well.

We’re working toward the vocabulary of the game being defined in the same DSL that creates the rooms and populates them. Just now, we’re working on defining the “actions”, little scraps of code that allow the game to pick up a bottle or unlock a gate. We have the actions encapsulated in a Lexicon, which embodies our simple syntax and semantics. You type a couple of words, we munge them around just a bit, and then look up an action and perform it.

Such a game has a property that is obvious when you think about it, but that leads to an interesting design possibility. Take the command we built yesterday. You say type “shout hello” and the game replies “Your shot of HELLO echoes through the area.” But … in one area of our sample game, there are cows. It would be amusing (I claim) if, when you shout near the cows, the “echoes” line came out but also something like “The cows look at you, wondering whether you are OK.”

One question comes right to mind: how might we do that? One way would be to define a global “shout” command, with an if statement in it to check whether there were cows present, and if there are, issue the second “amusing” line. But I think there might be a better way.

What if “actions” were either global or local? Suppose when you are in a room and issue a command, the room’s local actions get first shot at the command, and they can do anything special that applies to that room, overriding the global command. There might even be a way to make a decision in the room’s actions to give the global commands a shot at the command as well, or not.

I have a p-baked2 idea about how to do this. Suppose that our Actions object held, not its current mutable map of verb to action, but instead it held a mutable map of local operations, whose default value was to return the action from the global table. Then the standard way to do a command in the room would be to flush the local table, load it with any special room-related commands, then execute the Imperative. If the command is recognized in the locals, it gets executed, and if not the global table does the job.

Now we can of course envision code that could do the same thing, something like if there are locals and if the command is in the locals then do the locals else do the globals. And in the end, we might do that. But the idea of having the logic all embedded in the tables, and in the Actions object, strikes me as an interesting one. There is often great value to representing what needs to be done as data. In some senses, that’s a lot of what objects are about.

I like this idea, and I plan to try it. I think we are two or three steps away from being able to do it reasonably, although we could surely create the new Actions object and its tables right now. And there might be some useful learning from that, because we could test out the logic before committing to the idea. An experiment is often useful, especially when we’re just learning the language as well as learning what our program wants to be, and how to help it become that.

I’m not sure which way I’ll go. I have the feeling that I’ll choose the experiment. Either way, I’m sure it’ll be the right decision, unless it isn’t.

A Bit More Design Thinking

Yeah, I’m going to experiment with it just a bit. There’s a good reason, it’s not just that I want to, and it’s my house my rules. As I described the problem and the idea above, a few concerns came to mind. For example, with the cows, we’d want the shout to echo first, and then for the cows to become concerned. So in that case, either we’d have to add the echo to the local handler, or we’d have to execute the global handler and only then do the local.

That triggers my over-design thinking to imagining a number of tables that are interrogated in order local-1, global-1, local-2, global-2, or something like that, to allow the game designer to use the global action before or after their own action, or, presumably, not at all. My original over-design thought was to return a true/false from the local command, signifying whether the global should take a cut at it. There’s something I don’t like about that, probably that it’s not sufficient to handle everything we might want to do.

Another possibility is that the local, if it is found, always prevails, but that there is a way to trigger the global if and when you want to. Sort of a superclass call idea. I think that’s better. And … since the Actions lookup is done via the verb in an Imperative, we might even be able to create a new Imperative, with a different verb, one that the user could never type, and use it to invoke general behavior that we might want to reuse. So … yes. I think we’ll provide a way to specify that an action is to be looked up only in the general table, and for any imperative to trigger a subordinate lookup.

I think we have access to everything we would need to access to make that work, since the Imperative knows the world and the world knows the Lexicon. I think it can be made to work. Let’s do an experiment.

Actions Have Layers3

I start by making sure I’m at a green bar, and then I look for obsolete things to remove, like the tests for the old parsing scheme used prior to Lexicon and Imperative. Safe delete helps, and of course it’s all in git if we ever need it. Test, green, commit. We have a clean save point.

I think we’ll add these tests in the ImperativeTests file. Probably a good person would create a new test class for Actions, since that’s all we’ll be playing with here.

Here’s the entire Actions class:

typealias Action = (Imperative) -> Unit

class Actions(private val verbMap: MutableMap<String, Action>) {
    fun act(imperative: Imperative) {
         verbMap.getValue((imperative.verb))(imperative)
    }

    fun put(action: Pair<String, (Imperative) -> Unit>) {
        verbMap.put(action.first, action.second)
    }
}

Not much to it. The action, as it were, is all in the map. Let’s look at where we set that up.

A Concern

I have a small concern: we’re about to make a map of local actions that points to the current map of global actions. It’s iffy to use a raw collection, as we are here, and we’re about to build a rather gnarly raw collection. That might call for an object, GnarlyMap, or something. We’ll see.

Here’s the code that builds the game’s Actions object:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            "go" to { imp: Imperative -> imp.room.move(imp, imp.world) },
            "say" to { imp: Imperative -> imp.room.castSpell(imp, imp.world) },
            "take" to { imp: Imperative -> imp.room.take(imp, imp.world) },
            "inventory" to { imp: Imperative -> imp.world.showInventory() },
//            "shout" to { imp: Imperative -> imp.say(
//                "Your shout of ${imp.noun.uppercase()} echoes through the area.")},
        ).withDefault {
            { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        }
        )
    }

I decide to create a new test file for this, the other one has too much stuff in it.

Here’s my first test, just working string to string:

    @Test
    fun `table defaulting to table`() {
        val g = mutableMapOf<String,String>(
            "go" to "went"
        ).withDefault { key -> "I have no idea" }
        val l = mutableMapOf(
            "say" to "said"
        ).withDefault { key: String -> g.getValue(key) }
        assertThat(l.getValue("say")).isEqualTo("said")
        assertThat(l.getValue("go")).isEqualTo("went")
        assertThat(l.getValue("whazzup")).isEqualTo("I have no idea")
    }

This works as advertised. We automatically find “say” in l, don’t find “go” there but default to finding it in g, don’t find “whazzup” in either l or g, and return the g default “I have no idea”. Let’s make it a bit nicer:

    @Test
    fun `table defaulting to table`() {
        val g = mutableMapOf<String,String>(
            "go" to "went"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf(
            "say" to "said"
        ).withDefault { key: String -> g.getValue(key) }
        assertThat(l.getValue("say")).isEqualTo("said")
        assertThat(l.getValue("go")).isEqualTo("went")
        assertThat(l.getValue("whazzup")).isEqualTo("I have no idea what whazzup is")
    }

Works as advertised.

Ah. For my next trick, I planned to test some kind of actions that would, in a local action, show that we can invoke a call on l that only searches in the globals. As things stand, we cannot: We can’t see inside the l table to find the table that is referenced in its default.

We do need some kind of an object. Just as I was beginning to suspect. I love it when my suspicions keep up with my ideas. Fortunately, we’re right here in a fresh test file, so we can create a smart object and bring it into submission to our needs. I’ll try not to go too far adding capability until I’m sure I need it.

I write the following test for a new object, SmartMap:

    @Test
    fun `smart map`() {
        val g = mutableMapOf<String,String>(
            "go" to "went",
            "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf(
            "say" to "said"
        ).withDefault { key: String -> g.getValue(key) }
        val sm = SmartMap(g,l)
        assertThat(sm.getValue("say")).isEqualTo("said")
        assertThat(sm.getValue("go")).isEqualTo("went")
        assertThat(sm.getValue("whazzup")).isEqualTo("I have no idea what whazzup is")
        assertThat(sm.getGlobalValue("say")).isEqualTo("global said")
    }

And a very simple class suffices to make this work:

class SmartMap(val global: MutableMap<String,String>, val local: MutableMap<String,String>) {
    fun getValue(k:String): String = local.getValue(k)
    fun getGlobalValue(k: String): String = global.getValue(k)
}

So we have a proof of concept. What we don’t have is an object that is ready to accept our kind of pairs, which are <String,Action>, if I’m not mistaken.

Let’s see if we can make this SmartMap more generic. That seems to be the done thing in Kotlin.

Oh my, this seems to work on the first try:

class SmartMap<K,V>(val global: MutableMap<K,V>, val local: MutableMap<K,V>) {
    fun getValue(k:K): V = local.getValue(k)
    fun getGlobalValue(k: K): V = global.getValue(k)
}

This means that SmartMap is “generic” and can deal with any two types, K and V. It expects as inputs, two mutable maps from K to V and its two methods also work from K to V by accessing the maps. When I look down at the test, Kotlin has worked out and displays that my sm is a SmartMap<String,String>, just right. I could declare it explicitly, and I think I’d like to do that. Kotlin helps: it offers “specify type explicitly”, resulting in:

    @Test
    fun `smart map`() {
        val g = mutableMapOf<String,String>(
            "go" to "went",
            "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf(
            "say" to "said"
        ).withDefault { key: String -> g.getValue(key) }

        val sm: SmartMap<String, String> = SmartMap(g,l)

        assertThat(sm.getValue("say")).isEqualTo("said")
        assertThat(sm.getValue("go")).isEqualTo("went")
        assertThat(sm.getValue("whazzup")).isEqualTo("I have no idea what whazzup is")
        assertThat(sm.getGlobalValue("say")).isEqualTo("global said")
    }

Nice. Tests are green. Let’s grab a save point: initial SmartMap and tests.

Now it seems to me to be unnecessarily difficult to create the two maps and thread them together. We should do that internally to SmartMap. Then, we also need to offer the ability to add (and remove?) items from each map. And to clear the local map. Let’s get TDDing … first I’ll just remove the default from the l map in the second test, which should make it fail:

    @Test
    fun `smart map`() {
        val g = mutableMapOf<String,String>(
            "go" to "went",
            "say" to "global said"
        ).withDefault { key -> "I have no idea what $key is" }
        val l = mutableMapOf(
            "say" to "said"
        )
        val sm: SmartMap<String, String> = SmartMap(g,l)
        assertThat(sm.getValue("say")).isEqualTo("said")
        assertThat(sm.getValue("go")).isEqualTo("went")
        assertThat(sm.getValue("whazzup")).isEqualTo("I have no idea what whazzup is")
        assertThat(sm.getGlobalValue("say")).isEqualTo("global said")
    }

It does fail:

Key go is missing in the map.

Critical error, of course. Makes me wonder whether we need to protect the SmartMap against being given a non-defaulted map as its global. But we’re here to protect the local:

class SmartMap<K,V>(val global: MutableMap<K,V>, val local: MutableMap<K,V>) {
    private val safeLocal = local.withDefault { key: K -> getGlobalValue(key) }
    fun getValue(key: K): V = safeLocal.getValue(key)
    fun getGlobalValue(key: K): V = global.getValue(key)
}

We’re green. I renamed all the keys to key rather than just k. Commit: SmartMap creates default link from local to global via safeLocal.

I get a warning that I don’t recognize:

Warning:(6, 50) Constructor parameter is never used as a property

It seems to be referring to local. None of the intentions it offers seem useful. If offers “Convert to secondary constructor”, and when I accept that possibility, it gives me this rewrite:

class SmartMap<K,V> {
    val global: MutableMap<K, V>
    val local: MutableMap<K, V>

    constructor(global: MutableMap<K, V>, local: MutableMap<K, V>) {
        this.global = global
        this.local = local
        this.safeLocal = local.withDefault { key: K -> getGlobalValue(key) }
    }

    private val safeLocal: MutableMap<K, V>
    fun getValue(key: K): V = safeLocal.getValue(key)
    fun getGlobalValue(key: K): V = global.getValue(key)
}

When I go to commit, it warns me that secondary constructor should be converted to primary one. I think that’s amusing. Roll back to what we had. I give up on wondering what that’s about. Passed the question on to tech support.

Reflection

We have a smart map that acts just like a map, at least insofar as getValue is concerned. I think it might be ready for prime time, so I’ll move the tiny class out of the testing area. I’ve been keeping multiple small classes together, and I’m starting to think that’s a bad idea. Let’s give this class a file of its own.

Kotlin obliges with F6, the obvious keystroke for move. Commit: move SmartMap to own file.

Ah. that property warning wanted me to remove val. I thought you couldn’t access the parameters at all without val or var. I don’t have construct time figured out yet. Anyway it’s nearly happy now.

OK, it’s 1038 and I started at 0819, so that’s 2 1/2 hours, time for a break. Let’s sum up.

Summary

I think this little smart map object will be just the thing for allowing local behavior to override the global, or to invoke it if it wants to. And I am more than a little pleased with the fact that I made it generic. Let’s review that for those for whom it’s new.

We were originally saying that SmartMap used maps from type String to type String:

SmartMap(
    val global: MutableMap<String,String>, 
    val local: MutableMap<String,String>) {

We can generalize it to take any two types, K and V, by plugging them into the right occurrences of String as key and string as value:

class SmartMap<K,V>(
    private val global: MutableMap<K,V>, 
    local: MutableMap<K,V>) {

    private val safeLocal = local.withDefault { key: K -> getGlobalValue(key) }
    fun getValue(key: K): V = safeLocal.getValue(key)
    fun getGlobalValue(key: K): V = global.getValue(key)
}

The original class handled only maps from String as key to String as value. The new one can handle maps from any key type to any value type, and Kotlin can still do all the type checking for which it is justly famous.

Very nice, and what’s best about it is that I’m beginning to understand the type notation. I wouldn’t say that I’m an expert yet, but at least I understand this one.

Next time, I think we’ll plug the SmartMap into the game. Then we can start to use it. I want to see those cows get interested.

I hope you’ll join me then!



  1. Big Up Front Design, a phantom that we used to chase in the olden days of “Agile”. Probably would have been better to try to fix Not Even Doing the Right Thing, but its initials weren’t as attractive. 

  2. You’ve heard of a half-baked idea, right? Well, clearly an idea could be one-third baked or three-quarters baked, or basically p-baked for any 0<=p<=1. We do not consider over-baked ideas in this paper. 

  3. Similar to ogres, which are themselves similar to onions in that regard.