GitHub Repo

I don’t know quite what we’ll do this morning. Something fun. Let’s start by talking about good times.

I’m pretty sure it was Hill who mentioned having a good time programming, at last Tuesday’s meeting of the Friday Night Zoom Ensemble. He was expressing that we so often encounter development teams who are not having a good time. They are, in fact, suffering. Often they treat their 8, 9, 10 hour days as a price they have to pay to get a few hours outside work, where they can have a good time.

That is no way to live, even if they money’s good. Now, there may be very little one can do about the mean boss who always demands more, or the extractive capitalists who run the company, or the bizarre society that allows all that to go on … but much of the time, developers do get to program, and that can always be fun. No, really! We can have a good time programming, while still producing whatever “they” want at whatever pace we can produce it while having a bad time. Of course, if we have too good a time programming, we might have to stay alert, so that when “they” show up, we can spray a little fake Blood, Sweat, Tears™ by L’Oreal on our forehead. We might have to groan a bit, just so “they” won’t know we’re having a good time.

We can have a good time.

I was thinking this morning about roller-blading, which I used to do. I’ve given it up because a collision between my body and the ground is now too horrible to contemplate, but it was fun. I enjoyed smoothly skating along, moving with a sort of elephantine grace. Nothing fancy, but decent enough, smooth enough, easily enough. A good time.

When programming goes smoothly and easily, it can be fun. For me, it is fun. When I’m doing it well, I write a simple test, make it work, write another test, make it work. Even when I break something, my tests usually point me right at the problem, so I’m rarely disrupted, rarely sent down a deep debugging hole.

Oh, there are times, sure. The other day IDEA itself got confused and I finally had to check in and check out a few times, exit the program, incant certain curse words. But then I was back on track and quickly regained my stable, smooth, easy-going flow.

I believe, Hill believes, all the ZoomGang believe that this good time feeling is available to almost every programmer everywhere. And we believe that the way we work causes good times, and that’s why we share what we do.

Probably, if you are reading this, you already know that feeling, and you already try to reach the good times more and more often. I hope that what we do here will inspire you to keep at it. Unfortunately, perhaps some of my readers have not found it: I can only urge them to keep trying. Fun in programming is quite attainable. Even more unfortunately, there are programmers out there who do not have the time, or the spirit, or the knowledge, to look for the programming good times.

Maybe you know someone in that situation. Maybe you’re the one who can nudge them in the direction of finding good times. Maybe you send them to me, or to Hill or to Bill. Maybe you show them some things yourself. However you do it, if you can help one, or two, or a few programmers find good times … you’ll find that that is a good time as well.

Find good times. Cause good times. Make the world a slightly better place. Please.

Programming?

Oh, right. Programming. Our text game with the little DSL. What should we do?

I started this exercise to learn how to use Kotlin to build the DSL that currently defines the world of the game:

val theWorld = world {
    room("spring") {
        desc("spring", "You are at a clear water spring. " +
                "There is a well house to the east, and a wooded area to the west and south.")
        item("water") {
            desc("water", "Perfectly ordinary sparkling spring water, " +
                    "with a tinge or iron that has leached from the surrounding rocks, " +
                    "and a minor tang of fluoride, which is good for your teeth.)")
        }
        go("e", R.wellhouse)
        go("w", R.woods)
        go("s", R.`woods toward cave`)
        action("take", "water") { imp->
            if (inventoryHas("bottle")) {
                inventorySetInformation("bottle", " of water")
                say("You fill your bottle with water.")
            } else {
                imp.say("What would you keep it in?") }
        }
    }

At first, everything in the DSL was Strings, room names and verbs and nouns. And it is a text game, and it will all come down to Strings. But yesterday I picked up an idea from Bryan, of using a Kotlin enum to define rooms, and what happened was that when I started typing the name of a room, as in the go command up there, IDEA prompted me with all the names that matched my typing. It was easier. I like easier. Easier is good times.

Now, part of my idea for this thing was that imaginary “game designers” would use the DSL to create the world definition, and that they’d have to do as little real programming as possible. We’re not going all the way to a separate language that is somehow compiled into Kotlin. We’re just trying to make it easier.

My thought, insofar as I had any, was that just typing in the names as strings would be pretty easy and not hard to remember. But would this be even easier?

action(V.take, N.water) { imp->
    if (inventoryHas(N.bottle)) {
        inventorySetInformation(N.bottle, " of water")
        say("You fill your bottle with water.")
    } else {
        imp.say("What would you keep it in?") }
}

It would require that every word the game understands would need to be defined into various enum classes, N and V and so on, but when you typed the t in take, IDEA would pop up a list of all the verbs starting with t, and you’d just click or enter.

I think we’d need a separate class for directions (east, west, etc), and perhaps other enums would turn up as we go forward. We might need to associate special values with some of the words, though I’m not sure at this instant just what that might be, but we can be pretty sure that whatever we need, we can create, because behind the scenes there’s a complete programming language to help us out.

And …

Another idea that came up Tuesday night was the notion of defining the connections between rooms differently. Some possibilities include:

  1. Instead of go(D.east, R.wellhouse), say east(R.wellhouse).
  2. Perhaps allow east(R.wellhouse, D.west) to mean that the way back is to the west, and you don’t need to mention it again in the other room.
  3. Perhaps we always mention both rooms and both directions in some form, like `connect(R.spring, D.East, R.wellhouse), implying the return is the opposite of east, and
  4. Perhaps if the return is different we say connect(R.spring to=D.south, from=D.up, R.valley).
  5. Perhaps there are special commands for one-way connections or conditional ones.

I think there’s always a trade-off between a language that requires us to declare everything, and one where we don’t have to declare things. The former makes us work a bit harder, but it’s very difficult to mistakenly mention “well house” instead of “wellhouse” and hit a run-time error. The latter allows for less typing and less up front design, but opens the door to mistakes that the former would prevent.

As it stands, our DSL is very open and free form, and I’ve found it to be error-prone and to require a lot more remembering than I’d like to have to do.

I think he’s coming up on a decision …

I think we’ll move toward all words being pre-defined, under various enum classes. R(oom), V(erb), N(oun), D(irection), and so on. We’ll cast the DSL to expect words of these “types”, so that the prompting will be immediate and useful as we create the world.

And I think I’d like a more compact definition for the basic map of rooms and their connections (represented by a pair of Directions). That one is going to take some reflection, and maybe some friendly readers will offer some suggestions. I think we can assume that the world designer will draw a map showing rooms with arrows labeled with directions.

Hey, is this something category theory could help us with? It has nodes and arrows … but I digress.

Today, let’s improve one thing. Let’s do directions. This might take a bit of work.

I’m going to assume that our existing tests will suffice to tell me when I’ve broken something. If I’m wrong, we’ll all see it happen and we’ll regroup.

Let’s create the D enum:

enum class D {
    north, south, east, west, nw, sw,ne,se, up, down
}

That’ll be enough to let us experience the idea. Now a new version of go:

    fun go(direction: D, roomName: R, allowed: (World)->Boolean = { _:World -> true}){
        go(direction.name, roomName, allowed)
    }

    fun go(direction: String, roomName: R, allowed: (World)->Boolean = { _:World -> true}){
        go(direction, roomName.name, allowed)
    }

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

I added the first one. Here, I’ve changed two go commands and left one for comparison:

    go(D.east, R.wellhouse)
    go(D.west, R.woods)
    go("s", R.`woods toward cave`)

I kind of like that better. We’ll go with it. I’ll change all of them and then test.

I quickly learn that D.se is offered before D.south, which is backward. I think I’ll have to spell all the words out. And I think we’ll get into issues with the synonyms table. We’ll see. Carrying on, I change the rest.

Ah. There’s an issue with changing all of them … the tests use some room names that are not in the R enum. I’ll proceed carefully.

I convert the game first. This was a mistake, because it doesn’t work. The directions never work. I think I know why.

    private fun makeSynonyms(): Synonyms {
        return Synonyms( mutableMapOf(
            "east" to "e",
            "north" to "n",
            "west" to "w",
            "south" to "s").withDefault { it }
        )
    }

I think we need not to do that at all. Or I could map e to east. That makes sense, I think. I had to change the verbs also:

    private fun makeSynonyms(): Synonyms {
        return Synonyms( mutableMapOf(
            "e" to "east",
            "n" to "north",
            "w" to "west",
            "s" to "south").withDefault { it }
        )
    }

    private fun makeVerbs(): Verbs {
        return Verbs(mutableMapOf(
            "go" to Phrase("go", "irrelevant"),
            "east" to Phrase("go", "east"),
            "west" to Phrase("go", "west"),
            "north" to Phrase("go", "north"),
            "south" to Phrase("go", "south"),
            "say" to Phrase("say", "irrelevant"),
            "look" to Phrase("look", "around"),
            "xyzzy" to Phrase("say", "xyzzy"),
            "wd40" to Phrase("say","wd40"),
        ).withDefault { Phrase(it, "none")})
    }

The game runs. How about the tests I was so proud of? Seven fail, of thirty-four. Not bad.

The cases are like this one:

    go("n", "second") { true }
    go("s","second") {
        it.say("The grate is closed!")
        false
    }

I think if I convert these, all will be well. Switching to long form directions is the issue. Or, I could change the Verbs table. Let’s try that:

I try adding these to the Verbs:

    "e" to Phrase("go", "east"),
    "w" to Phrase("go", "west"),
    "n" to Phrase("go", "north"),
    "s" to Phrase("go", "south"),

No joy. However changing the test to this does work:

    go("north", "second") { true }
    go("south","second") {

I don’t want to convert that to use D, because as written, I have to use R also, and there is no room second in the R enum, and I don’t want one.

I’m going to make these tests all run and then figure out the intricacies of the synonyms.

All the changes amounted to spelling out the text name of the direction, “east” for “e” and so on.

We are green. I’m going to commit this … no, first let’s do the long names for the 45 degree directions. Right. Green. Commit: add Direction enum D and use. Mods variously to update to the new words.

Reflection

That was a bit sloppy, to be frank. The D change made certain direction words “official”, and the Verbs and other Lexicon tables need to converge on the same terms. If the moves table in a room is going to say “north”, then when you type “n” in the game, it has to become “go north”, and so on. So those things needed to be updated.

In addition … surely we want the moves table to be different.

    private val moves = mutableMapOf<String,GoTarget>().withDefault { Pair(roomName) { _: World -> true } }

We note that this table violates our “rule” about covering native collections. More germane to the present concern, it should be a map<D,GoTarget> now that we’re treating directions as elements of D enum. And what is a GoTarget?

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

I reckon that wants to be a Pair<R, (World)->Boolean> where R is of course a canonical room name.

So we should be pushing our new enums down into the inner workings of the game.

Just for fun, I’ll change the definition of that table and see what IDEA has to say about it.

The changes I did were these:

class Room ...
    private val moves = mutableMapOf<D,GoTarget>().withDefault { Pair(roomName) { _: World -> true } }

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

    fun move(imperative: Imperative, world: World) {
        val (targetName, allowed) = moves.getValue(D.valueOf(imperative.noun))
        if (allowed(world)) world.response.nextRoomName = targetName
    }

That suffices to convert the map to map from D to a GoTarget rather than from String (direction). However there is one test that doesn’t like the change, the one that treats “xyzzy” as a direction. Adding that word to D makes all the tests pass.

Interesting. I think I’m done for the morning. It’s my wife’s birthday, I’ve been working for a couple of hours, and I have an errand to run. Let’s sum up.

Summary

This was definitely ragged. The changes went in readily, and the tests were easily brought into line. It was slightly irritating to have to change the Lexicon stuff, but let’s be realistic, when we’re changing the base vocabulary of the game from String to enum, there’s going to be some nudging. I was able to just tick through the tests, modifying them, but I can’t help feeling that with the right set of base changes, the old form of the tests would have just worked. There’s something about the flow through the Synonyms and Verbs and translate and the enums that isn’t quite right. This cascade is surely part of it:

    fun go(direction: D, roomName: R, allowed: (World)->Boolean = { _:World -> true}){
        go(direction.name, roomName, allowed)
    }

    fun go(direction: String, roomName: R, allowed: (World)->Boolean = { _:World -> true}){
        go(direction, roomName.name, allowed)
    }

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

The official one to use is the top one. It calls the middle one, and that calls the last one … and that one converts from string back to D. It’ll be better to invert those, and better yet to accept only the top one. That’ll require changing a lot of tests, unless we keep the bottom level override.

And I don’t want to force the top one, because there are a couple of room names in the tests that are not in the R enum.

Ragged. I should scope out a smooth flow, something like

  1. Convert the tests to use D and R;
  2. Fix any tests that try to use an R that isn’t in the main list;
  3. Or, provide a way to have more than one enum? No, just fix the tests;
  4. Remove all the old-fashioned implementations of go.

Similarly, figure something smooth for rooms with R.

Often, I like making a necessary change and following my nose through the tests that need to be modified. Today, somehow, it felt clumsy. Ragged. Still fun, but not as much fun as I deserve.

We’ll commit this (Change Room moves to map D to GoTarget.), and do even better tomorrow. See you then!