Kotlin 102 - More Rooms, More Features
I have in mind recreating, or ripping off, some special rooms from the old adventure. Ideally, I’ll be able to build these with no new features. Whether we have all that we need, I’m not sure. We’ll find out.
I have three “features” in mind. One is the classic view out a window where a shadowy figure appears across the way. Another is a magical bridge that appears. And, finally, these games generally have darkness, with a need to light a lamp or be eaten, typically by a Grue. I don’t know what a Grue is, but I am sure that they must be gruesome1.
Of these ideas, I think darkness will probably be the most difficult. The shadowy figure will probably just involve fielding an action. Similarly with the bridge: it’ll require a consistent property in the world, but in principle it should be like the gate into the cave.
I’ll start with the shadowy figure room. And, before we get in much deeper, I think we’d best cater to having more than one function defining the world. Otherwise the definition will get ungainly, and might run up against compiler limitations.
I run into trouble. I need a one-word command, “wave”, and as far as I can tell, they don’t work.
Ah! I’ve made a rookie mistake. I have done two things in one go, rather than one at a time. I wrote a function that created a room into the existing world, and I used that function to create my new shadowy figure room. It turns out that the room creating function didn’t work as intended, so the room, of course, didn’t work as intended. Unfortunately, it worked a bit, which led me to look down all the wrong paths for the problem. Remind me to update the Habits article.
We create the World using this odd function:
fun world(details: World.()->Unit): World
= World().apply { details() }
This function creates a world, applies the details function to it, and returns that world.
OK, I think I see what I must do.
At the bottom of the existing function makeGameWorld
, this creates a new room:
with(theWorld) {
room(R.EastPit) {
desc(
"You are at the pit window.",
...
I had to provide the existing world, which is saved in theWorld
as the context for the subsequent room building. So I should be able to do similarly in a function:
fun makePitArea(world:World) {
with(world) {
room(com.ronjeffries.adventureFour.R.EastPit) {
desc(
"You are at the pit window.",
OK, the room works now.
> up
You're at a low window overlooking a huge pit, which extends up out of sight. A floor is indistinctly visible over 50 feet below. Traces of white mist cover the floor of the pit, becoming thicker to the right. Marks in the dust around the window would seem to indicate that someone has been here recently. Directly across the pit from you and 25 feet away there is a similar window looking into a lighted room. A shadowy figure can be seen there peering back at you.
> wave
The shadowy figure waves back!
You are at the pit window.
The text there is taken from some version of the original game that I found on line.
Things being green, let’s commit this. It needs work, however.
What needs work is that we have a bunch of functions in HelloApplication that are building our game world, but they have no useful organization. And one of them is rather huge. Let’s try to go in small steps. Here’s the beginning of the main game setup:
fun makeGameWorld(): World {
val theWorld = world {
room(R.Spring) {
desc(
"You are at the 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."
)
Recall that the pit area function looks like this:
fun makePitArea(world:World) {
with(world) {
room(R.EastPit) {
desc(
"You are at the pit window.",
I’m thinking to build out different areas of the game in different functions, each implementing just a few rooms. So each of those functions will look like makePitArea
, passed a World instance, and using with
to set the context. (I think we could repeat world.room
a lot if we wanted to, but we don’t want to.)
So let’s give the code inside makeGameWorld
the same shape.
fun makeGameWorld(): World {
val theWorld = world {}
with(theWorld) {
room(R.Spring) {
desc(
"You are at the 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."
)
Now we can extract that whole set of rooms into a separate method. I think it’s pretty much the easternmost area of my map, so I’ll call it makeEasternArea
. I grab the whole new with
block, and use the refactoring Function to Scope, pick where I want it, and get this:
fun makeGameWorld(): World {
val theWorld = world {}
makeEasternArea(theWorld)
makePitArea(theWorld)
return theWorld
}
private fun makeEasternArea(theWorld: World) {
with(theWorld) {
room(R.Spring) {
desc( ...
fun makePitArea(world:World) {
with(world) {
room(R.EastPit) {
desc( ...
That’s exactly what I had in mind. I’ve tested. Commit: Break out world creation into area functions.
What’s not to like? Well, the eastern area is … let me see … 177 - 20 …. 157 lines long. That’s pretty long. It’s just nine rooms, so we’re looking at an average room length of around 18 lines each. It looks to me that the minimum room length is about eight lines:
room(R.WoodsNearCave) {
desc(
"You are in the breezy woods.", "You are in the woods. " +
"There is a cool breeze coming from the west."
)
go(D.West, R.CaveEntrance)
go(D.North, R.WoodsS)
}
Assuming a 300 room world, the rooms would require about 5400 lines of description. Maybe not too bad. And we’re not going to create a real game anyway, are we?
To test the EastPit room, I wired it into the Spring, with the direction “up”. When working with the game, we’ll want to play it: you really don’t get the feeling for the words and actions unless you try the game play. We can make it do what we say with tests … but we can’t make it feel like fun just with tests. So we’re going to want a way to navigate quickly.
Let’s implement one. I think I’d better start with a test.
@Test
fun `can beam between rooms`() {
val world = world {
room(R.Z_FIRST) {
desc("first", "first")
}
room(R.Z_SECOND) {
desc("second", "second")
}
}
val player = Player(world, R.Z_FIRST)
val result = player.command("beam Z_SECOND")
assertThat(result).contains("second")
}
I propose to implement a secret command, “beam”, that takes a R enum name as its noun, and moves you to that room. I chose the term “beam” for obvious reasons2.
The beginning of the command is easy:
private fun makeActions() {
with(actions) {
add(Phrase("beam")) { imp -> imp.world.beam(imp)}
...
fun beam(imp: Imperative ) {
/ but what goes here?
}
The only question is, given a random string that might match an enum, how do we find the enum, if any, that we should apply as next room. The enum valueOf
function conveniently throws an exception if it doesn’t find the name. That won’t do. We’ve solved this before.
fun beam(imp: Imperative ) {
val match = R.values().find {
it.name.equals(imp.noun, ignoreCase = true)}
match?.let { response.moveToRoomNamed(it)}
}
This runs green. I want to test this in the game.
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.
> beam eastpit
You're at a low window overlooking a huge pit,
which extends up out of sight.
A floor is indistinctly visible over 50 feet below.
Traces of white mist cover the floor of the pit,
becoming thicker to the right.
Marks in the dust around the window would seem to indicate
that someone has been here recently. Directly across the pit
from you and 25 feet away there is a similar window looking
into a lighted room.
A shadowy figure can be seen there peering back at you.
Oh yes! Nice!
Commit: beam command.
OK, first new feature done, the shadowy figure. With random breaks in my work and such, I’m about 2 hours in. Before working on the magical bridge, which I think is probably easy, or the darkness, which I have no idea how I’ll do it, we’ll break here and publish. What have we learned?
Summary
Doing one thing at a time is paramount, if you plan to make any mistakes, and if I gave advice, I’d advise that you assume that you will in fact make mistakes. (Excuse me while I update the Habits list, q.v.3)
I suspect that I’d do well to have a general companion object or whatever it takes to have that match operation in all my enums, but I’ll save that idea for the next time I have to look up how I did it. Thing is, I’ve solved the problem twice now, and should really implement something standard, but I’m tired of playing with the computer, today is Saturday, and I’ve got books to read and people to talk to. So I’m rushing, and creating legacy code as fast as I can.
Next time I search an enum, I promise I’ll settle this. If I don’t, I’ll buy a drink for the first person who calls me on it.
To be brutally honest, I started working on beam
, just enough to put it in the actions table, before I wrote the test. Fortunately, I took a short break and came to my senses. It’s amazing how we can know with great certainly that something is good and yet we avoid it. Just me? No, I sincerely doubt it.
Anyway, a nice little feature. One thing that pleases me with this little program is that almost everything goes in in just a few lines. Most of the methods are short, and that’s a good sign.
I hope my reader will join me again next time. Bye for now!