Let’s parse a bit more of the DSL (Dungeon Scripting Language?) and then see what happens. I have high hopes, tempered by decades of experience.

Musing

I was thinking this morning that the job of a DSL like we’re working on has two aspects, both of which are quite important. On the one hand, it should be an easy and expressive way to describe whatever the DSL is about: a map or dungeon in our case. On the other hand, it should build a structure that is suitable for the software to use.

I guess we could describe a DSL as an interface between what a designer might want to say, and a software structure that is ideal for processing.

I would argue that the common way of creating window structures is optimized for the window-drawing software, building a tree structure that is quite good for the painting code. However, creating this structure common requires the programmer to do all the heavy lifting of remembering which nodes are where, creating child nodes, customizing them, then adding them in where they belong. The resulting code doesn’t look much like the window design.

The TornadoFX window building scheme, in contrast, uses a hierarchic DSL, much like the HTML sketch from a few articles ago, and much like the DSL we’re starting on here. Its window definitions explicitly show the hierarchy of panels. It’s a good thing.

Status

I am new to Kotlin, to IDEA, to [DELETED] Gradle, to all these tools, so I am finding my way slowly and with mistakes. I’m not yet to the point where I’m confident that whatever goes wrong, I can figure it out. I’ve had IDEA / Gradle get in a state where suddenly the tests are crashing, or where I can’t build at all, and because there’s no code to look at, and because the Internet is only so good at serving up answers to my questions, I’ve had to start over many times.

I’m on version 4 of this ADVENTURE thing, and I’m just starting the second article. That’s right: I had to throw away three versions of this program yesterday. Not really my idea of fun.

But we have a few tests and classes working now. There isn’t much code: let’s review all the tests and code right here.

Tests

class WorldTest {
    @Test
    fun createWorld() {
        val world = world {println("creating")}
        assertThat(world.name).isEqualTo("world")
    }

    @Test
    fun worldWithRoom() {
        val world = world {
            room("living room") {}
        }
        assertThat(world.rooms.size).isEqualTo(1)
        assertThat(world.rooms[0].name).isEqualTo("living room")
    }
}

Code

fun world(init: World.()->Unit): World{
    val world = World()
    world.init()
    return world
}

class World {
    val name = "world"
    val rooms = mutableListOf<Room>()

    fun room(name: String, init: Room.()->Unit) : Room {
        val room = Room(name)
        rooms += room
        room.init()
        return room
    }
}

class Room(val name: String) {

}

Not much to it, but I’m pretty sure that it’s working as intended, making a collection of rooms in the world. I do suspect we’ll want that structure to be a map or something different, in due time.

Today I’d like to work on navigation, with an eye to someday soon building a little “game” that can be run in terminal mode with println and whatever reads input, or in a text window with a command line at the bottom. Whatever I can make work. So what I need to do is to create the code in Room that supports navigation. Yesterday’s example, updated to today’s thinking, looks like this:

world
  room("well house")
    item("keys")
    go("s", "ravine")
  room("ravine")
    go("w", "woods")
    go("e", "cave")

All I need for navigation, of course, is the “go” command, which, I imagine, just builds a table in the Room saying where you can go. Let’s try to write a test for that.

    @Test
    internal fun roomsWithGo() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
    }

Two rooms, woods and clearing, connected on a south-north connection. I don’t have an assert in mind. Let’s at least do a bit of asserting.

        assertThat(world.rooms.size).isEqualTo(2)
        assertThat(world.rooms[1].name).isEqualTo("clearing")

That should do for a quick check, and now that I think of it, I haven’t even tested two rooms yet.

IDEA isn’t happy about the word “go”, and I can’t blame it. I let it help me create a method, but I have to do the heavy lifting myself. Now Room looks like this:

class Room(val name: String) {
    val moves = mutableListOf<Pair<String,String>>()
    fun go(direction: String, roomName: String) {
        moves += Pair(direction, roomName)
    }
}

And the tests run. Commit: Initial implementation of Room go.

Let’s enhance the test a bit to check the deets.

        val r1Moves = world.rooms[1].moves[0]
        val expected = Pair("n","woods")
        assertThat(r1Moves).isEqualTo(expected)

I’m sure there is some clever assertJ way to do that but I’m here to get this test working. And it does. I’ll ask my betters when they wake up. Commit: improved test.

Now It Gets Harder

OK, I think that this test has created a very small world with a woods and a clearing. I’d like to work toward a game that has a world in it, and that can deal with commands like “s” by checking the current room to see if there is a southward move, and if so, placing you in that room.

We’re going to wish those lists were maps, I bet. And it’s time to create at least one new object, with tests of course.

Let’s have some GameTests.

    @Test
    internal fun gameCheck() {
        val world = world {
            room("woods") {
                go("s","clearing")
            }
            room("clearing") {
                go("n", "woods")
            }
        }
        val game = Game(world)
    }

I want to report an odd sensation. I really feel that I don’t know how to do what I’m doing, but writing the test came easily, and I suspect the next bits will come easily … it’s like I know more than I think I do. I’m sure, however, that I also know less than I think I do. But it’s odd, as if I had just started something requiring skill and it is so far going surprisingly smoothly.

Anyway, IDEA thinks I need a class or something for Game.

class Game(world: World) {

}

That suffices to make the test run, since it doesn’t assert anything yet. What should happen? Well, let’s say that the Game is going to have a currentRoomName member variable, that it will initialize to the first room in the World, and that there will be a method, oh, command, that can include, among other choices, “n” and “s”.

Then we could test:

        assertThat(game.currentRoomName).isEqualTo("woods")

And

class Game(world: World) {
    val currentRoomName = world.rooms[0].name
}

Tests are green! Ship it!

OK let’s try a command.

        val game = Game(world)
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("clearing")

This demands a command function and I think I have no choice but to do a little work.

Woot!! I actually made this work! Callooh callay!

It’s not pretty, but I’ve done worse and we can surely improve from here:

Game command is just a dispatch on command:

    fun command(cmd: String) {
        when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            else -> {println("unknown cmd $cmd")}
        }
    }

And the move …

    fun move(dir:String) {
        val room: Room = currentRoom()
        val moves = room.moves
        val move = moves.firstOrNull{it.first==dir}
       currentRoomName = move?.second ?:currentRoomName
    }

Serious feature envy here, but my chops aren’t up to doing the dispatch and all … plus I might write it this way first in any case.

We get the current Room dynamically (I’ll show that in a moment), then fetch its moves and find the first move Pair whose first element is the direction we want. Then, if we got a pair, we set currentRoomName to its second element, otherwise set it to currentRoomName.

I think I could do that last bit by conditioning the fetch, but this made sense to me at the time. Here’s currentRoom:

    fun currentRoom(): Room {
        return world.rooms.first{it.name == currentRoomName}
    }

This will surely hurl if we don’t find the room of the current name, which could happen if a move mentioned a room that doesn’t exist. But our job here was to get a rope bridge across the chasm, and we’ve managed that.

That’ll do for today, I started about 0800 and it’s 0935 now, we’ll call it two hours and call it good. A decent stopping point, with things to do but tests all green. Commit: rudimentary Game understands n and s.

Bit of a lie there, didn’t check both n and s. Let’s do that at least:

        val game = Game(world)
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("clearing")
        game.command("n")
        assertThat(game.currentRoomName).isEqualTo("woods")

Green. Commit: OK tested “n”, you purists!

Summary

We now have a rudimentary round trip. Our DSL can create a connected world, and our tiny Game object can navigate between rooms.

It works. It’s not what we could call “right”. Issues include:

  • currentRoomName is inconvenient, we should probably have currentRoom instead or in addition.
  • navigation is done in Game and should defer to world and/or room.
  • we need more robust handling of missing elements.
  • we should probably do something about checking whether all move targets exist.
  • I need to learn better ways of testing for existence of elements.
  • I’m not what you’d call adept with Kotin, especially collections.

And more, I have no doubt. But it’s working in a very real end-to-end sense. And this is still only day two.

Woot! I get to quit on a up-tick today!