Feeling rather done with Gilded Rose, I take another look at the text game building code. There are issues.

GitHub Repo

Like it says, I’m coming back to the ADVENure program after about 10 articles looking at Gilded Rose. I sneaked in a small feature last night, while waiting for the Zoom Ensemble neé Friday Geek’s Night Out meeting. It’s at a troubling stage of development.

To refresh our minds, let me start with the new work I did.

I added a few “rooms” to the game. We’ll look at those in detail shortly, but first another little thing that I think is nearly good.

A “room” has two descriptions, a short one and a long one:

room("woods") {
    desc("woods", 
    	"You are in a dark and forbidding wooded area. 
    	It's not clear which way to go.")

As things were up until last night, whenever you entered a room, the long description was displayed:

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

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

My intention all along was that we’d display the long description the first time you enter the room, and thereafter whenever you return to it, the short description would come out. It was also my plan to have a “look” command that would display the long description in case you wanted to review it for clues.

So the question was, how to keep track of whether we had been in the room before or not. And I came up with what I think is rather a nice solution. When the room’s desc DSL command is executed, it used to just save the short and long descriptions. Now it does this:

    fun desc(short: String, long: String) {
        shortDesc = short
        longDesc = long
        theDesc = long
    }

We’ve added a new member variable, theDesc, which is initialized to equal the long description. Then we changed the display:

    val resultString: String get() = 
    	sayings + nextRoom.description() +"\n"+ nextRoom.itemString()

Note the call to a new function, descriptions(), implemented thus:

    fun description(): String {
        return theDesc.also {setShortDesc()}
    }

The also function runs its block, passing the receiver, theDesc in this case,, and returning it after the block. So we still return the current value of theDesc. But what does setShortDesc do?

    fun setShortDesc() {
        theDesc = shortDesc
    }

It sets theDesc to the short description, so that the next time we call description we’ll return the short description (and set it again, redundantly). In a language without also, I might have written something like this:

val msg = shortDesc
setShortDesc()
return msg

I think that once one gets used to the various “scope functions” of Kotlin, they’ll become second nature. I freely grant that right now, they bring me to a pause and I have to think a moment, but in a situation like this, I think they’re pretty useful and clear enough.

With this much done, the game displays the long description of a room just once, and thereafter, the short. So then I wanted the “look” command, which I implemented in the base-level makeActions:

    private fun makeActions(): Actions {
        return Actions(mutableMapOf(
            Phrase("go") to { imp: Imperative -> imp.room.move(imp, imp.world) },
            Phrase("say") to { imp: Imperative -> imp.room.castSpell(imp, imp.world) },
            Phrase("take") to { imp: Imperative -> imp.room.take(imp, imp.world) },
            Phrase("inventory") to { imp: Imperative -> imp.world.showInventory() },
            Phrase("look") to { imp: Imperative-> imp.room.look()},
            Phrase() to { imp: Imperative -> imp.room.unknown(imp, imp.world) }
        ))
    }

That required a simple method, look, in Room:

    fun look() {
        setLongDesc()
    }

All it has to do is set theDesc back to the long description, because when the command ends, it displays the response, which will call description, which will return the long description while setting theDesc back to short.

I thought that was rather nice. In play it looks like this:

Welcome to Tiny Adventure!
You are at a clear water spring. There is a well house to the east, and a wooded area to the west and south..
You find water.

> take water
What would you keep it in?
spring
You find water.

> e
You are in a small house near a spring.
You find bottle.
You find keys.

> take bottle
bottle taken.
well house
You find keys.

> take keys
keys taken.
well house

> w
spring
You find water.

> take water
You fill your bottle with water.
spring
You find water.

Of course, we immediately learn that our short descriptions probably need not to be too short, perhaps saying “You are at the spring.” rather than just “spring”, but that’s a note for us in game designer mode, not programmer mode. The game designers might ask for helpful features but for now, it’s up to what the designers (me) put in the descriptions.

So, what did I learn from this?

Reflection

The new capability was quite easy. I had originally thought I’d need an extra flag for “visited” but it came to me that I could just have a new description variable and modify it from long to short. I don’t know how that idea came to me, but I also don’t know why I bought that REDACTED used Lamborghini years ago, so this seems a relatively small concern compared to that.

To add a command that works everywhere, we have to add it to the makeActions function, and add a mew method, typically to Room. That’s a bit off, because I’d really like to have those commands defined by the “game designers”, that is, inside the DSL. We might work on that in due time.

More troubling is this next topic. I decided to build up a few more rooms supporting our recent features of the bottle that can hold water, and the new long / short / look capability. Here’s the code for the current game:

fun makeGameWorld(): World {
    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") {}
            go("e", "well house")
            go("w", "woods")
            go("s", "woods toward cave")
            action(Phrase("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?") }
            }
        }
        room("woods") {
            desc("woods", 
            	"You are in a dark and forbidding wooded area. 
            	It's not clear which way to go.")
            go("e", "spring")
            go("n", "woods")
            go("2", "woods")
            go("w", "woods")
            go("nw", "woods")
            go("se", "woods")
        }
        room("well house" ) {
            desc("well house", 
            	"You are in a small house near a spring.")
            item("bottle") {}
            item("keys") {}
            go("w", "spring")
        }
        room("woods toward cave") {
            desc("breezy woods", 
            	"You are in the woods. There is a cool breeze coming from the west.")
            go("w","cave entrance")
            go("n", "spring")
        }
        room("cave entrance") {
            desc("cave entrance",
                "You are at an entrance to a cave. There is a locked gate blocking your way west.")
            go("e","woods toward cave")
        }
    }
    return theWorld
}

Taken one at a time, the room descriptions don’t seem too bad to me. Even the implementation of taking water is pretty reasonable looked at on its own:

            action(Phrase("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?") }
            }

Very likely most specialized commands will be about that complicated, perhaps a bit more, so well within reason to implement.

But the issues that I had when creating those five rooms make me think that creating a real game isn’t as easy as one might like. Now, it’s never going to be totally easy, because there will be many rooms and many connections. And most rooms will only have two connections, unlike my woods up there, where no matter where you go, there you are. But … when I’m in the tulgey wood and I want moving north to take you to the mushroom garden, I have to type

go("e","mushroom garden")

And “mushroom garden” had better be the name of a room, because if it isn’t, you get a run time error and you’re stuck in the wood. Now, we can build some code to validate at least that all the room references are valid. That would be easy. But in building the game, you’d really like to get some help with the name, so that if you type “mus”, the editor would pop up the likely room candidates, “mushroom garden” and “muskrat ramble” and you could arrow to the right one and plug it in.

Now part of this may not be as major an issue as it feels to me. When I created the five rooms above, I was just kind of winging it. In a real game, I’m sure we’d draw a big map, and the map would probably have sections, so that you could work on the area to the west of the Crystal Bridge and I could work on the area to the east and north. And we’d draw the connections on our map and label the rooms, and when we enter the data, we’d have the map to refer to. If that way of working, we’d still want some validation but it’d be a lot easier than it is to just wing it.

Last night, the Zoom Crew offered a couple of suggestions. One was that we could specify both ends of a connection just once, perhaps in the first room where the path appears. We could say something like this:

        room("well house" ) {
            desc("well house", 
            "You are in a small house near a spring.")
            item("bottle") {}
            item("keys") {}
            go("west", "spring", "east")
        }

And the last line there would mean “go west from here to get to the spring and east from there to get back here”. We could even default the second value and have it be the “opposite” direction, to be overridden if “east” wasn’t what you wanted or if no return was possible.

I think Bill Wake also suggested that a better notation would be

east("spring")

That would be a simple albeit tedious addition to the DSL and it would make the job somewhat easier.

To make up for that easy idea, Bill also suggested validating that the world was connected, that is, you can get to any room from any other. This isn’t as easy as it sounds, since some paths are only open after you unlock the door or kill the dragon or steal the airplane. Still, it would be possible, and probably a good idea in a real game building system. There’s not much more frustrating than being deep in a game and encountering a problem that is unsolvable owing to a bug.

I think it was Bryan who suggested that rooms could be defined in an enum, which might allow the prompting stuff to prompt for the names. That would be possible, though it would require one to predefine the names in the enum. Still, could help a lot if made to work.

Plan …

At this writing, I have no plan to create a game that can actually be played and enjoyed. I’m not at all sure how I could deploy it. If someone out there wanted to take what’s here and create a free game, perhaps as a live example of real code and improving it (a massive multiplayer Gilded Dungeon perhaps) I’d certainly grant permission.

So there’s no upside for me in creating a large and complex world, though there may be fun in creating a few new puzzles and working out how to implement code to solve them. My guess, however, is that they’ll all break out to be much like what’s already here.

Got a challenge for me, that makes sense in the world of a two-word commanded text game? Tweet me up. It might be fun.

Want to take on the task of deploying this as a tool for would-be game makers? Want to implement it on phones? Tweet me up.

I’m sure there are little things. One example, suggested by Hill, relates to the item lines:

room("well house" ) {
    desc("well house", "You are in a small house near a spring.")
    item("bottle") {}
    item("keys") {}
    go("w", "spring")
}

The {} are required, which is a minor but irritating pain. We can make them optional:

    fun item(thing: String, details: Item.()->Unit): Item {
        val item = Item(thing)
        contents.add(item)
        item.details()
        return item
    }

Truth is, I’m not sure why that’s even allowed. Premature implementation, I guess. Ah, not quite. The Item can have a description desc, with a short and long description, with the intention being that the short description is displayed in the inventory command, and that a new command, perhaps “examine axe”, would display the longer more florid description.

Maybe we’ll work on that tomorrow. This morning, I think we’ll just make the {} optional by providing a default here:

    fun item(thing: String, details: Item.()->Unit = {}): Item {
        val item = Item(thing)
        contents.add(item)
        item.details()
        return item
    }

That should allow me to remove the empty ones. I do that, and add a description to one item, just to remind myself what the {} are for.

fun makeGameWorld(): World {
    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", "well house")
            go("w", "woods")
            go("s", "woods toward cave")
            action(Phrase("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?") }
            }
        }
        room("woods") {
            desc("woods", "You are in a dark and forbidding wooded area. " +
                    "It's not clear which way to go.")
            go("e", "spring")
            go("n", "woods")
            go("2", "woods")
            go("w", "woods")
            go("nw", "woods")
            go("se", "woods")
        }
        room("well house" ) {
            desc("well house", "You are in a small house near a spring. " +
                    "The house is sparsely decorated, in a rustic style. " +
                    "It appears to be well kept.")
            item("bottle")
            item("keys")
            go("w", "spring")
        }
        room("woods toward cave") {
            desc("breezy woods", "You are in the woods. " +
                    "There is a cool breeze coming from the west.")
            go("w","cave entrance")
            go("n", "spring")
        }
        room("cave entrance") {
            desc("cave entrance",
                "You are at an entrance to a cave. " +
                    "There is a locked gate blocking your way west.")
            go("e","woods toward cave")
        }
    }
    return theWorld
}

I’m starting to be sure that one could make a fun game with this. For now, we’re green. I’ll do a commit “various small enhancements”.

That’ll do for today. Tomorrow, we’ll figure out how to look at the water and elicit that lovely description. That might be amusing to do, if we want “look” to elicit the room’s long description and “look water” to examine the water.

Until then, good cess to you all. See you soon!