Kotlin 25 - Say Something!
I think the next thing I need is for a message to come out in response to a command. We need an extension to GameResponse. (For once, reverting would have been the wrong thing to do … probably.)
Part of the action on an attempted move through a closed grate—or an open one—may need to include some textual output, so the player will be informed of what happened. Let’s add some ability to GameResponse.
GameResponse doesn’t even have any tests yet, because it doesn’t have any behavior yet. It’s just a data class:
data class GameResponse(val name:String) {
var responseString: String = ""
var nextRoomName: String by singleAssign<String>()
var nextRoom: Room by singleAssign<Room>()
}
We’re about to change that, so let’s be having some testeration1 here.
class GameResponseTest {
@Test
fun `response has say method`() {
val r = GameResponse()
r.say("One thing")
r.say("Another thing")
assertThat(r.sayings).isEqualTo("One thing\nAnother thing\n")
}
}
This demands say
and sayings
. I provide:
data class GameResponse(val name:String="GameResponse") {
var sayings = ""
var responseString: String = ""
var nextRoomName: String by singleAssign<String>()
var nextRoom: Room by singleAssign<Room>()
fun say(s:String) {
sayings += s+"\n"
}
}
This isn’t terribly robust, but we can make it more so behind the scenes, so long as we continue to support a real or virtual property sayings
.
Now that we have this, let’s see if we can add a saying to our text for the imaginary grate:
@Test
fun `room go has optional block`() {
val myWorld = world {
room("first") {
desc("first room", "the long first room")
go("n", "second", { true })
go("s","second", { false })
}
}
val myRoom = myWorld.unsafeRoomNamed("first")
val response = GameResponse("foo")
myRoom.command("s", response)
assertThat(response.nextRoomName).isEqualTo("first")
val r2 = GameResponse("bar")
myRoom.command("n", r2)
assertThat(r2.nextRoomName).isEqualTo("second")
}
We extend the test to check the sayings:
val myRoom = myWorld.unsafeRoomNamed("first")
val response = GameResponse()
myRoom.command("s", response)
assertThat(response.nextRoomName).isEqualTo("first")
assertThat(response.sayings).isEqualTo("The grate is closed\n")
val r2 = GameResponse()
myRoom.command("n", r2)
assertThat(r2.nextRoomName).isEqualTo("second")
Now to make it so, we need to extend the room definition in the test:
val myWorld = world {
room("first") {
desc("first room", "the long first room")
go("n", "second", { true })
go("s","second", { false })
}
}
I wanted to say this:
val myWorld = world {
room("first") {
desc("first room", "the long first room")
go("n", "second", { true })
go("s","second", {
say("The grate is closed!")
false })
}
}
But say
has no meaning here. We’ll need to pass something in to our expectation lambda. I think we should pass it the room. I think there are at least a couple of ways to do it. One is just to give the lambda definition a parameter room
and pass in a room. I think there are others, and I’ll do some research shortly. For now, let’s try to make it “just work”.
Hm. This is harder than I thought. Must research and experiment. It’s 1228, I probably started this bit at 12:20.
Moments later, 1231, I have what may work:
typealias GoTarget = Pair<String, (Room)->Boolean>
fun go(direction: String, roomName: String, allowed: (Room)->Boolean = {r:Room -> true}) {
moves += direction to Pair(roomName, allowed)
}
And we’ll have to change the tests a bit, I think. Let’s let Kotlin/IDEA tell me. Test.
No value passed for parameter 'p1'
Complaining about this:
fun move(direction: String) :String {
val (target,allowed) = moves.getValue(direction)
return if (allowed())
target
else
name
}
This is a Room method so … this
?
Yes, but the Room doesn’t understand say
. Let’s fix that and get a rope across this chasm.
Arrgh. The Room doesn’t have the response. Let’s go back and change the signature of allowed to expect a GameResponse.
I’m tempted to revert, but I don’t have a save point that I like. I’ll press on a bit more. It’s 1238.
typealias GoTarget = Pair<String, (GameResponse)->Boolean>
fun go(direction: String, roomName: String, allowed: (GameResponse)->Boolean = {r:GameResponse -> true}) {
moves += direction to Pair(roomName, allowed)
}
fun move(direction: String, response: GameResponse) :String {
val (target,allowed) = moves.getValue(direction)
return if (allowed(response))
target
else
name
}
I’m demanding that response goes down to move …
fun command(cmd: String, response: GameResponse): Unit {
val name = when(cmd) {
"s" -> move("s", response)
"e" -> move("e", response)
"w" -> move("w", response)
"xyzzy" -> move("xyzzy", response)
else -> "unknown cmd $cmd"
}
response.nextRoomName = name
}
Test to see what still won’t hook up.
No value passed for parameter 'response'
Refers to this:
assertThat(world.roomCount).isEqualTo(2)
assert(world.hasRoomNamed("clearing"))
val clearing:Room = world.unsafeRoomNamed("clearing")
val newLocName:String = clearing.move("n")
assertThat(newLocName).isEqualTo("woods")
Give it what it wants:
assertThat(world.roomCount).isEqualTo(2)
assert(world.hasRoomNamed("clearing"))
val clearing:Room = world.unsafeRoomNamed("clearing")
val newLocName:String = clearing.move("n", GameResponse())
assertThat(newLocName).isEqualTo("woods")
Test again.
Expecting:
<"clearing">
to be equal to:
<"woods">
but was not.
Expecting:
<"The grate is closed!
">
to be equal to:
<"The grate is closed
">
but was not.
I like that second error, and not the first. Fix the second test:
myRoom.command("s", response)
assertThat(response.nextRoomName).isEqualTo("first")
assertThat(response.sayings).isEqualTo("The grate is closed!\n")
There’s a lot of squiggly underlining in the test code. We’ll come back to that.
I still have two errors:
Expecting:
<"unknown cmd n">
to be equal to:
<"second">
but was not.
Expecting:
<"clearing">
to be equal to:
<"woods">
but was not.
My true return isn’t working. it’s 1251. A wise man would revert, but I feel close. Staying in a bit longer.
It wants me to move my lambdas outside parens. I do that:
@Test
fun `room go has optional block`() {
val myWorld = world {
room("first") {
desc("first room", "the long first room")
go("n", "second") { true }
go("s","second") {
it.say("The grate is closed!")
false
}
}
}
}
I don’t think that will change the test results. It doesn’t. Why is my true
not working?
Oh this was wild. It’s 1311. I finally noticed that, somehow, I had deleted the possibility of moving north from this code:
fun command(cmd: String, response: GameResponse): Unit {
val name = when(cmd) {
"s" -> move("s", response)
"e" -> move("e", response)
"w" -> move("w", response)
"n" -> move("n", response)
"xyzzy" -> move("xyzzy", response)
else -> "unknown cmd $cmd"
}
response.nextRoomName = name
}
I am really glad that I didn’t revert. That’s quite rare, usually I regret not reverting. Hold on while I remove some printlns that I sprinkled around.
OK, we are green. Commit: go
command’s lambda expects a GameResponse parameter. GameResponse say
and sayings
work.
That was intense. Let’s sum up.
Summary
In this one case, we have the information we need in the GameResponse. We are not yet using the GameResponse in the View, and it’s not even accessible to us at the moment. We’ll deal with that next time.
It has been 80 minutes since I started, and while I did get through it without reverting, it took me far too long to notice that I had misplaced the northbound routing in command
. I can imagine a more robust test would have helped me see that it was a command drop-through rather than an error in my true/false code, but of course I was focused on that, and wasn’t really open to a higher-level error.
Would I have done better to revert? Hard to say. Perhaps, but I’m not sure when I lost the “n”, and I didn’t have a decent save point. I probably should have committed the test in working form, then extended it to break.
Had that not slowed me down, I might have managed to get the response accessible in the View and then we could have a sayings
appearing in the game output.
Maybe tomorrow. Tonight (Tuesday) is the Friday Night Coding Zoom Ensemble. Maybe I’ll get some good code advice as well.
I’ll accept the win, but it was a bit of a dirty win. Still a win.
See you tomorrow, GWATCDR.
-
Don’t bother to look this one up. Ain’t no such word AFAIK. ↩