Kotlin 22 - Finding Our Way
In order to find out what our DSL should be like, I propose to code up a few scenarios “by hand”.
Since our DSL room-building tool is just Kotlin code in a convenient format, it should be possible, if not easy, to cause a room to behave in any way we want, “simply” by embedding Kotlin code that is to be executed at appropriate times when the room is active. Now, because we’re building our little language to be helpful, we’ll want to package up commonly-used bits and make them readily accessible, but aside from the need for a few hooks, it’s probably already possible to implement all my scenarios,
I’m not going to tick through them to support that assertion. I’m just going to pick some easy ones, try to make the game support examples of that kind, and see what I learn.
One way of looking at it is that we’re going to program a game, and then extract the useful DSL bits from it. How well will that work? Follow me, we’ll go find out.
Phases
I don’t want to try anything too difficult, but other than that, I don’t have a serious concern over what to do. I do have a kind of “design idea” that I’d like to share.
I’m assuming that we’re providing a way to build in “custom behavior” for a room. Because this is a computer we’re talking about, custom behavior will come down to a block of code that is selected at the appropriate time, that is executed, and that produces effects that are internal to the game, and effects that are external, that is, that show up in the scrolling text.
And … it seems to me that there are probably “phases” or “sections” in the output strings given a move. I’m thinking that we’ll want to specify things that come out before the standard output, and things that come out after. Beginning, middle, and end, perhaps.
The idea is still ill-formed. I don’t know what to do with it, but I’m letting it swirl around with my other thoughts.
Random Thoughts
We did a spike a few days back, where I changed the game code to return a string result. The phases idea makes me think that we may want a structured result, so that when a given bit of the code executes, it can append strings to any of a few “sections”, in the output. Again, these might be beginning, middle, and end.
It’s also possible that the Result should include a section for deferred action, only to be executed right before the result is to be returned. I don’t see a use for this just now, but since we’ll be editing game state, I can imagine the need.
These are just things floating around in my mind. I share them as examples of the kind of thinking that I do even when I’m about to do something very small and practical. Design issues are always on my mind.
Result
I think we should improve the way that we handle results right now. I think you’ll agree with me when we review how the game handles printing its results now. The relevant code is here, in our View:
fun someoneTyped() {
val cmd = myCommand.text
game.command(cmd)
val newLine = "\n"+game.currentRoom.longDesc
myText.appendText("\n" + cmd)
myText.appendText(newLine)
myCommand.text = ""
myCommand.appendText("")
count++
}
We get a command from the input field, into which someone has typed, execute it as a command, and display the long description of the current room. Since the only commands we understand at all move us from one room to another, or leave us in the same room, this is enough to make a simple maze game playable. It’s not very interesting.
I think the first thing we should do is to change the agreement between this view and the game. We want to have the game return a result. Is it sufficient for that result to be a string? For now, it probably is, since all we do is print stuff in the view. We know, however, that our preferred practice is to wrap base level objects in objects of our own, so we should probably at least provide a Result that holds the string.
We’ll do that in a few steps, the smaller the better.
What we’re about to do here will be a refactoring, at least as seen in the game window. We’re going to change how it gets the string. We’re going to improve the design of how it gets the string. But to do that, we’ll surely be changing how a lot of this code works, including the current agreed interfaces.
Should the game return its result? Arguably the view should ask the game for the result, and if we do it that way, things might go more smoothly.
Let’s start there. We’ll display the command in the view, as we do now, and then we’ll display the game’s result string. Sure, why not?
I should mention that I’m already tired of remembering to put newlines on the beginning of my output bits. I think they should probably go on the end, as in println
. We’ll do that sometime along the way. Probably. Maybe.
I have to confess, I just went ahead and did this.
fun someoneTyped() {
val cmd = myCommand.text
game.command(cmd)
myText.appendText("\n" + cmd)
myText.appendText("\n"+game.resultString)
myCommand.text = ""
myCommand.appendText("")
}
class Game(val world: World, startingName: String) {
val resultString: String get() = currentRoom.longDesc
...
The good news about this is that we’ve now isolated the creation of the resultString in the Game class. The View now just asks for it, instead of asking for whatever detailed thing it used to request.
I should mention that some of the oddness of that code in someoneTyped
is there to make the cursors in the windows go where I want them. That code could be, and should be, more clear. Not yet, we have bigger fish to catch and then fry.
What’s next?
I’m glad you asked. Next I think we’d like to change the resultString
from a property to an actual value, into which we place whatever output we wish. I frankly think this is a questionable way to go, but it’s the best idea that I have. But wait, there’s something nagging at my mind.
Where’s the test for that?
Right. I just slammed that in and, truth to tell, tested in in the “GUI”. This is not our way. We write executable tests for things here 1 Ron, and we’re trying to learn to do that more and more frequently, converging on always, or at least on “always, except”.
This feature, the resultString, is really a game feature, although we’re going to want to set it in our room code. There’s a bit of an issue right there: clearly it’s the room-based code that will be creating all the results. How will it know how to set the result if it’s a game property? Should we be passing the game to all the rooms?
Well, perhaps not quite … but let’s review the code from Game on down … see how things actually work around here.
class Game(val world: World, startingName: String) {
val resultString: String get() = currentRoom.longDesc
var currentRoom = world.unsafeRoomNamed(startingName)
val currentRoomName get() = currentRoom.name
val roomReferences: Set<String> get() = world.roomReferences
fun command(cmd: String) {
when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> {println("unknown cmd $cmd")}
}
}
fun move(dir:String) {
val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
}
Currently, commands are executed in the Game, and the only commands we really have execute a move by asking the current room for some information and then telling the world to fetch a new room.
Feature envy. Game is reaching, not just into World, but also into Room, to get things done. We should be doing “tell, don’t ask” here, telling the world, to do the command, and the world, probably, telling the room.
But let’s look further, to get a good picture of how things work.
class World {
val name = "world"
private val rooms = Rooms()
val roomCount get() = rooms.size
val roomReferences: Set<String> get() = rooms.roomReferences
fun room(name: String, details: Room.()->Unit) : Room {
val room = Room(name)
rooms.add(room)
room.details()
return room
}
fun hasRoomNamed(name: String): Boolean {
return rooms.containsKey(name)
}
fun roomNamedOrDefault(name: String, default: Room) :Room {
return rooms.getOrDefault(name, default)
}
fun unsafeRoomNamed(name: String): Room {
return rooms.unsafeRoomNamed(name)
}
}
This object, World, has no brains to speak of. It’s just a container for rooms managed by name, in the Rooms object. I wonder whether the world should just be an instance of Rooms, or some merging of the Rooms idea and World idea.
World doesn’t have the notion of current room: Game has that. Curious.
What about the Room? That’s perhaps a bit more clever.
class Room(val name: String) {
val moves = mutableListOf<Pair<String,String>>()
var shortDesc = ""
var longDesc = ""
val roomReferences: Set<String> get () {
return moves.map { it.second}.toSet()
}
fun go(direction: String, roomName: String) {
moves += direction to roomName
}
fun move(direction: String) :String {
return moves.first { it.first == direction}.second
}
fun desc(short: String, long: String) {
shortDesc = short
longDesc = long
}
}
We could improve this code by making a more clear differentiation between the methods used in the DSL, go
and desc
, and the more operational methods, move
, which is used to return the name of the room to move to in a given direction, used in game play, and roomReferences
, intended to be used to validate the game, ensuring that we don’t reference rooms that are undefined. That method is only semi-used, but there is a test for it.
Let’s tidy this a bit:
class Room(val name: String) {
val moves = mutableListOf<Pair<String,String>>()
var shortDesc = ""
var longDesc = ""
// DSL Builders
fun desc(short: String, long: String) {
shortDesc = short
longDesc = long
}
fun move(direction: String) :String {
return moves.first { it.first == direction}.second
}
// Game Play
fun go(direction: String, roomName: String) {
moves += direction to roomName
}
// Utilities and Other
val roomReferences: Set<String> get () {
return moves.map { it.second}.toSet()
}
}
That’ll do for now. Maybe Kotlin has a better way of doing that.
By the way, IDEA’s move
capability is quite smart, moving methods up one whole method at a time. Nice!
Test, green, commit: Tidying Room (and other random changes). OK, that was awkward, I didn’t commit the change to the result string. Sorry. I was on the way to test that when I did this tidying. I could, of course, have just committed Room. Perhaps a wiser man would have, but my real use of Git is as a creator of save points, so I generally commit everything when the tests are green.
I’m sure you do much better things.
I have too many balls in the air. Let’s settle down, and regroup.
Reflection
I definitely want to get a test in there for the result string. Along the way to doing that I realized that we don’t have a secure path for rooms, where the action mostly is, to pass results up to the world and thence to the game.
That led me to improve the code in Room, where the action is, because it’s in room where most everything should be happening, or so it seems to me.
Let’s write the test, and then use it to push the command code to where it belongs. I think but am not certain, that we will want most commands to be common to the game level, and for some commands to be handled by the individual rooms. That may make for an interesting protocol, but it’s too soon to tell.
I’ll start with a test at the Game level, because we do want the game to be all that the View knows.
@Test
fun `game gets good results`() {
val world = world {
room("first"){
desc("short first", "long first")
go("s","second")
}
room("second") {
desc("short second", "long second")
go("n", "first")
}
}
val game = Game(world, "first")
game.command("s")
assertThat(game.resultString).isEqualTo("long second")
}
I’m using Kotlin’s cute capability of naming a method with spaces in it, which is good for making tests with useful names, and which I hope no one ever uses anywhere else.
The test above runs green. Commit: Game has simple test for resultString.
Now I think I’d like for resultString to be something a bit more dynamic, but for that to happen, don’t I need to push the command downward first?
fun command(cmd: String) {
when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> {println("unknown cmd $cmd")}
}
}
fun move(dir:String) {
val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
An Idea for a Small Step
OK, how about this for a next small step? We’ll do the top-level command parsing here in Game.command, but we’ll defer the move
to the … well, to the currentRoom, which we have at Game level. (I don’t feel right about bypassing the world, but it’s pretty dumb, so maybe that’s OK.)
That idea doesn’t hold water. Look at the code above. In the command
method, we could pass each command on to currentRoom
but then we’d have to do … no, wait … let me try this:
fun command(cmd: String) {
val name = when(cmd) {
"s" -> currentRoom.move("s")
"n" -> currentRoom.move("n")
"xyzzy" -> currentRoom.move("xyzzy")
else -> "unknown cmd $cmd"
}
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
This is clearly madness. But it works, at least on the existing tests. Will it work if I try to move in an illegal direction? I was sure there were tests for that, but we can do another.
@Test
fun `game gets good results`() {
val world = world {
room("first"){
desc("short first", "long first")
go("s","second")
}
room("second") {
desc("short second", "long second")
go("n", "first")
}
}
val game = Game(world, "first")
game.command("s")
assertThat(game.resultString).isEqualTo("long second")
game.command("s")
assertThat(game.resultString).isEqualTo("long second")
}
We’ve added an attempt to go south from “second”, which isn’t in its go
table. Run the test. We get a neat error:
Collection contains no element matching the predicate.
That’s coming from here:
fun move(direction: String) :String {
return moves.first { it.first == direction}.second
}
I’m kind of surprised that this compiled but now I realize that Kotlin planned the exception all along. I’d rather IDEA had told me that was possible, but you can’t have everything.
We can use firstOrNull and provide our own default. Let’s try that.
fun move(direction: String) :String {
val pair = moves.firstOrNull { it.first == direction}
return pair?.second ?: name
}
This is the “Elvis” operator. I originally wrote the code out longhand and let IDEA take me through moving the return to the front and then converting to Elvis. You may not like it, but this is what idiomatic Kotlin looks like. We should get used to seeing it and writing it.
Let’s commit and then reflect. I want to think about defaults. Commit: room.move deals with unknown directions.
Reflection
It seems to me that when you send a move command to the room, it is defaulting not to move. I think there is a thing that acts like a table with a default value. That would obviate2 the need for the special handling of the null above. In addition, it seems to me that making the go an array list of Pair(direction,name) is weird. Shouldn’t it be a map from direction to name?
class Room(val name: String) {
val moves = mutableListOf<Pair<String,String>>()
fun go(direction: String, roomName: String) {
moves += direction to roomName
}
Let’s change that, it has to make our life better.
class Room(val name: String) {
val moves = mutableMapOf<String,String>()
fun go(direction: String, roomName: String) {
moves += direction to roomName
}
fun move(direction: String) :String {
return moves.getOrDefault(direction, name)
}
I was a little surprised that the go
command could add an element to the map that way, but it’s apparently fine. However, the test for roomReferences
fails. That’s supposed to return the values of the map as a set. It used to say this:
val roomReferences: Set<String> get () {
return moves.map { it.second}.toSet()
}
That won’t do. This will:
val roomReferences: Set<String> get () {
return moves.values.toSet()
}
Green. Commit: Room.moves is now a map.
I think I could give the map a default. Let’s learn how to do that.
class Room(val name: String) {
val moves = mutableMapOf<String,String>().withDefault { name }
fun move(direction: String) :String {
return moves.getValue(direction)
}
We must say getValue
there. If we were to say moves[direction]
, the Kotlin spec requires that the call return NULL if the key is not present. Personally I think that’s a bit of a crock, but it’s not my language and they probably had a good reason.
Anyway, green, commit: Room uses map.withDefault.
The time is 1115, and I started at 0805. Well past time for a little break.
Reflection
I think I’m supposed to be pushing responsibility for the responseString downward, so that the room can set it. And … it seems that what really ought to be happening here is something like this:
- A command comes in to the Game;
- It goes to the current Room;
- The current Room can deny knowledge of the command;
- If the current Room denies knowledge, the Game (or World?) gets a shot.
- If the current Room processes the command, it needs to return some text to display and a possibly new room (name?) to serve as the new current Room.
This is making me think that the Response object has at least three fields, a flag indicating whether the command should be processed by game / world, a result string and a room name.
I’m inclined to make the Response a mutable object. We’ll create a blank one at the top, and pass it down to the room to be dealt with.
I feel like this is going to get messy.
- He’s not wrong
- Ron’s right, it’s going to get messy. Let’s see how long it takes for him to notice and what he accomplishes, if anything.
Since we only process move commands, let’s change things so that the command is sent to the current room will’e nil’e, together with a Response object. We’ll do it in small steps, first moving the command down. We have enough tests to drive us, I think. We’ll change:
fun command(cmd: String) {
val name = when(cmd) {
"s" -> currentRoom.move("s")
"n" -> currentRoom.move("n")
"xyzzy" -> currentRoom.move("xyzzy")
else -> "unknown cmd $cmd"
}
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
To:
fun command(cmd: String) {
val name = currentRoom.command(cmd)
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
That requires a new method in Room:
fun command(cmd: String): String {
return when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> "unknown cmd $cmd"
}
}
I rather expect my tests to run. If they don’t, I’ll learn something. They do run. Woot! Commit: commands are now parsed in Room, not Game.
- OK, got away with a bit
- But can he really do a Response object. Survey says no.
Nice. Let’s invent a Response object, very simple one, with just one field, and return it.
fun command(cmd: String): String {
val roomName = when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> "unknown cmd $cmd"
}
return GameResponse(roomName)
}
I think IDEA would like to help me build this class. I am mistaken, it doesn’t know that I have a class in mind here. Interesting. Anyway … I start with this:
data class GameResponse(name:String) {
var responseString: String = ""
var nextRoom: String = name
}
This won’t quite reach, but let’s first use it in Game:
fun command(cmd: String) {
val response = currentRoom.command(cmd)
currentRoom = world.roomNamedOrDefault(response.nextRoom)
}
This is going to take a few more strands of rope than I’d like but with a few changes we get there:
fun command(cmd: String) {
val response = currentRoom.command(cmd)
currentRoom = world.roomNamedOrDefault(response.nextRoom, currentRoom)
}
fun command(cmd: String): GameResponse {
val roomName = when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> "unknown cmd $cmd"
}
val resp = GameResponse(roomName)
resp.responseString = "You have arrived at $roomName"
return resp
}
Note that I created a new response string. There’s a problem here: we don’t know the other room, because in Room we don’t know the other rooms, so we cannot create the long description here.
So this code …
val resultString: String get() = currentRoom.longDesc
Which is used here:
fun someoneTyped() {
val cmd = myCommand.text
game.command(cmd)
myText.appendText("\n" + cmd)
myText.appendText("\n"+game.resultString)
myCommand.text = ""
myCommand.appendText("")
}
Can’t hook up to the response.
Let’s make a command decision here. The responses back from Room.command should not include the room description: that will be fetched by Game, after it has the room. The responseString in the GameResponse will—for now—be displayed before the room description.
I’ve lost the thread. Got in too deep with these changes. Better revert.
- Excellent Decision!
- This is just the thing to do. Once I lose the thread, I rarely get it back. Much better to revert, review, rest, and then try again. Well done, Ron!
OK, I’m back to green. Let’s review the code, since I have no clear understanding of where we are. And we might break for the morning, since I’ve been at this for about four hours, well past my sell-by date.
fun someoneTyped() {
val cmd = myCommand.text
game.command(cmd)
myText.appendText("\n" + cmd)
myText.appendText("\n"+game.resultString)
myCommand.text = ""
myCommand.appendText("")
}
class Game(val world: World, startingName: String) {
val resultString: String get() = currentRoom.longDesc
var currentRoom = world.unsafeRoomNamed(startingName)
val currentRoomName get() = currentRoom.name
val roomReferences: Set<String> get() = world.roomReferences
fun command(cmd: String) {
val name = currentRoom.command(cmd)
currentRoom = world.roomNamedOrDefault(name,currentRoom)
}
}
OK, good, the command is being deferred down to the Room, though the response is still just a room name. We can do better, next time. Also good, the game is just asking for the resultString, so when we improve that, the View will be just as happy as ever. (Probably some tests could break. We’ll not be too startled by that.)
In Room:
fun command(cmd: String): String {
return when(cmd) {
"s" -> move("s")
"n" -> move("n")
"xyzzy" -> move("xyzzy")
else -> "unknown cmd $cmd"
}
}
fun move(direction: String) :String {
return moves.getValue(direction)
}
We return the name, from our map with default.
Definitely time for a summary, and a break. I’ll push the article right after I summarize.
Summary
OK, I lost the thread. This happens when a change requires more steps than we can keep in our head. The trick is recognizing the situation, because, since we never really have it all in our head, we’re tempted to keep banging at the thing until it works. Fortunately, I recognized the symptom, and recognized that I’m tired, so I reverted rather than bashing my head against the keys for too long.
I think the key issue is that the relationship between the Game and the World is really a relationship with Room and world is by room name, because the Room can’t return a room different from itself.
Further, I think I tried to do too much with the GameResponse object. We should probably proceed in two or more smaller steps, one where it’s just a cover for the same string, and then another step or two making it smarter. It might also pay off to get the World involved, which would let it pass the Rooms collection down to the individual Room commands, so that they could return a Room back rather than a name, leaving all the name matters down at the Room level.
We’ll see. I’ll come at this with a fresher mind, maybe this afternoon, more likely tomorrow.
For now, we made a little progress and gained a little understanding of our application and of Kotlin and IDEA. Good enough, though not great.
See you next time!