Time for a little planning, and some worrying. How far can I go before making this thing somehow playable? How far should I go?

Yesterday afternoon (wow, working two shifts lately) we implemented long and short room descriptions in our little DSL:

        val world = world {
            room("somewhere") {
                desc("You're somewhere.", long)
            }
        }

I’m imagining that this is a single-player text game, where things only happen when the player takes her turn by issuing a command from a limited selection thereof. The game interprets the command, changes its state, and some text comes out telling the player what happened, where they now are, what they see, and so on. I imagine it working in at least two slightly different ways. Perhaps it has a simple terminal window with a text prompt > and messages interleaved in a scroll.

You are in a pit.
> Dig
It is said that when you are in a hole, you should stop digging.
You are in a deeper pit.
> Dig sideways
You have broken through the side of the adjacent sewer. Your pit is filling with, well, let's call it water.
> Swim
You rise with the water and eventually get to the top of the pit.
You are beside a very tiny yet fragrant lake.
> ...

Another very similar implementation might have a window with a text input pane at the bottom, and a scrolling text pane above, containing all the output. (As I think of it, I suspect that we’d want that text pane to include the command: you’d probably like to look back and review what happened in the context of what you said.)

Now, because I took something that Hill said as a challenge, one of my objectives here is to TDD basically everything, rather than using whatever text stream it produces as my tests. Right now, those tests are easy, but if we imagine some dwarves roaming the world, appearing now and again to attack the player, perhaps with some clever repartee, those tests may get harder to write. But I’m not afraid … yet.

What I am quite ignorant about, and therefore, if not afraid at last concerned, is the whole topic of input and output in these modern old-fangled Macs and PC things. I know what I’d do on my more modern iPad, but here, I’ve got some learning to do about how to create a window or whatever I might need to do.

The good news is that this starter program that Hill gave me includes a rudimentary “Application” and “Controller”, whatever that means.

By way of putting a little knowledge in my tank, let’s review this code that I’ve been given, to see what we can learn from it. If you’re already a window / TornadoFX / JavaFX wizard, skip forward, or read on to laugh at the beginner. It’s OK either way.

There’s a file HelloApplication.kt:

package com.ronjeffries.adventureFour

import javafx.application.Application
import javafx.fxml.FXMLLoader
import javafx.scene.Parent
import javafx.scene.Scene
import javafx.stage.Stage
import tornadofx.App
import tornadofx.View
import tornadofx.launch
import tornadofx.pane

class MainView: View() {
    override val root: Parent = pane()
}

class HelloApplication : App(MainView::class) {
}

fun main() {
    launch<HelloApplication>()
}

Well, this appears to be about as simple as you could wish, except for knowing all those things you have to import. I imagine there is some relation between those and what is displayed when we run. Let’s read the other file, HelloController:

package com.ronjeffries.adventureFour

import javafx.fxml.FXML
import javafx.scene.control.Label

class HelloController {
    @FXML
    private lateinit var welcomeText: Label

    @FXML
    private fun onHelloButtonClick() {
        welcomeText.text = "Welcome to JavaFX Application!"
    }
}

Oh, and I found a “resource”, hello-view.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<?import javafx.scene.control.Button?>
<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.ronjeffries.adventureFour.HelloController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
    </padding>

    <Label fx:id="welcomeText"/>
    <Button text="Hello!" onAction="#onHelloButtonClick"/>
</VBox>

That looks like a window description of some kind. I wonder how it got hooked up that way. In particular I am sure that I never typed “com.ronjeffries.adventureFour.HelloController”. Magic. Anyway a window description. I think I’ll run the main and see what the window looks like.

Well. It opens a window, and the window doesn’t have anything in it, notable not a button saying “Hello”.

small plain window

That’s not as helpful as I might have preferred. But anyway I think I get the basic drift that we have a main with an Application with a View, a separate Controller that fields things like button clicks (and, I’d bet, text inputs), and some kind of FXML thingie that describes the window we want.

I do wonder why no button. I wonder if there should be more to this part:

class MainView: View() {
    override val root: Parent = pane()
}

Like a button definition or something. Excuse me a moment while I look at another project …

    override val root = pane {
        minWidth = SCREEN_WIDTH
        maxWidth = SCREEN_WIDTH
        minHeight = SCREEN_HEIGHT
        maxHeight = SCREEN_HEIGHT
        background = Background(BackgroundFill(Color.LIGHTBLUE, null, null))
        isFocusTraversable = true
        addEventHandler(KeyEvent.KEY_PRESSED) { event: KeyEvent ->
            input.handle(event)
        }
        addEventHandler(MouseEvent.MOUSE_CLICKED) { _ ->
            requestFocus()
        }
        this += bucket
    }

That’s from another demo project that Hill provided. Here we see the same root = pane thing but now it’s setting all kinds of info. I think this stripped down thing in my app is, well, stripped down.

OK, this brief exploration makes me feel confident that when the time comes, it probably won’t be impossible to hook the game up to a window of some kind.

Nota Bene:1 I can imagine that another team member, or a product owner or customer, might be more concerned, and might ask us to create the window now. If that were to happen, the wise thing might be to acquiesce rather than wave hands. To try to be honest with myself, it wouldn’t hurt me to learn a bit about how to do this. It’s just that I don’t think it would make for a good article.

I’m not saying that this here is a good article, but it’s the article I’ve got this morning.

Anyway, we’ve got room descriptions, albeit fairly rudimentary ones. What we don’t have is the game providing a response. That’s what we should really work on this morning.

Response

My tentative plan is that when the game executes a command, it produces a Response object. I’m not really saying how the game viewer gets the object. It could be a direct return, it could be an event bus, it could be via email. Do a command, get a response. Them’s the rules.

For now, let’s suppose that when you call the Game’s command function, you’ll get a Response back. We’ll populate the Response with information, probably mostly text. Remains to be seen.

I want to do a bit of a spike on this, to get a feeling for how it all might work. I’m on a green bar and a fresh commit, so I can code a bit with impunity and throw it all away. But I have a confession to make.

I am not as committed to throwing a spike away as I might be. Some folks will tell you that a spike is always thrown away, and I would be one of those people save for the fact that I will never tell you what you have to do, but it is a good idea to go in with the intention to bang around,learn something, then flush it all and do it right. That said, sometimes what I do can be refactored to right and, well, I just might keep it. Don’t tell anyone, please.

Here’s command as it stands:

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

Let’s suppose that we’re going to return a string for each command. It’s pretty clear that on the else we should return something like “I can’t understand $cmd”. It seems to me that there is a way to use when as an expression. I’ll just type something in and see what Kotlin says about it.

OK, well, I just typed this in on a whim:

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

    fun move(dir:String) : String {
        val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
        currentRoom = world.roomNamedOrDefault(name,currentRoom)
        return currentRoom.shortDesc
    }

I made command return a string, changed the trailing println to be just the string, then the red lines led me to change move to return the current room’s short description.

Then I enhanced one of the tests, to see if that actually worked.

    @Test
    fun gameCheck() {
        val world = world {
            room("woods") {
                desc("You're in a scary wods.", "You're in a very scary wooded area where there could be trouble")
                go("s","clearing")
                go("xyzzy","Y2")
            }
            room("clearing") {
                desc("You're in a clearing.","You're in a lovely clearing, Lambs are gamboling in the distance.")
                go("n", "woods")
            }
            room("Y2") {
                go("xyzzy","woods")
                go("s", "Y3")
            }
        }
        val game = Game(world, "woods")
        var msg = ""
        assertThat(game.currentRoomName).isEqualTo("woods")
        msg = game.command("s")
        assertThat(msg).isEqualTo("You're in a clearing.")
        assertThat(game.currentRoomName).isEqualTo("clearing")
        ...

Lo! and Behold! The test passes! The Game can return a response.

And now the devil on our keyboard tray tempts us. “This is nearly good. We can just create a little Response object, plug in the descriptions, return that instead of the Strings, and everything will be just fine. No need to throw this nice code away!”

devil duckling

And believe me, I’m inclined to listen to that cute little devil, but I do feel the need for a bit more thinking about all this, so let’s just revert before I lose what little will power I have. Get thee behind me, devil duckling!

OK, rollback done. I hope it only rolled back this morning’s work. Seems to be the case.

Let’s reflect.

Reflection

We learned a useful bit of Kotlin syntax, the use of a when block as an expression, allowing a single return rather than a return in each element. We could presumably use it in other ways. And I’d bet that an if can be used similarly. Those Kotlin guys, clever. I am definitely coming to like this language, and to at least tolerate the IDEA IDE. No, more than tolerate. I like how helpful it is, almost always, although it does occasionally pop up help when I don’t really want it. Its advice is often helpful, and it’s pretty good at creating a new class or method shell. I’ve not used it much for other refactorings, but I see that it knows a lot of tricks.

We see that it’ll be easy to at least arrange that the command call returns a response, and that means that it’ll mostly be a matter of making sure that everyone sets up a suitable response.

Looking forward, I can imagine that some commands might be pretty deep, and we might want some other scheme than a simple return. Maybe we’ll create a Response object in command and pass it to the various commands to be filled in. Too soon to decide, in my opinion.

And I find myself reaching a conclusion about what this exercise is, and what it is not. I think what I’ll do is rig up a simple window with a text input line and an output pane that contains all the output. In particular, I think I’ll draw the line under “text game like ADVENTURE”, and not compel myself to make it turn magically into some other kind of game. I’m here to learn, and I think that when this one reaches a sort of conclusion, I’ll move on.

That’s not to say that we won’t try some hard things like darkness and lamps, or dwarves or pirates. We might. But we might, instead, move on to some other learning opportunity in the Kotlin / IDEA world.

But for this morning, we’ve learned something, resisted temptation, and had some fun. I’m going to call it here, and maybe do something more this afternoon. No promises, but assuming that I survive, I’ll be back soon.

See you then!



  1. Gratuitous Latin phrase. This could be one of those days.