Kotlin 39
A fix for the warnings, but do I like it?
After some helpful conversations on Twitter, I’ve found a change that will make (almost?) all the warnings go away. Recall that they were coming out when a command action didn’t need all of the verb, noun, and world parameters that I pass down. Some of the actions do need all three.
So I’ve changed the parameter list to pass in the command instance, which does include the noun and verb. It looks like this, for example:
fun command(cmd: String, world: World) {
val command = Command(cmd).validate()
val action: (Command, World)->String = when(command.verb) {
"inventory" -> ::inventory
"take" -> ::take
"go" -> this::move
"say" -> ::castSpell
else -> ::unknown
}
val name:String = action(command, world)
world.response.nextRoomName = name
}
fun move(command: Command, world: World): String {
val (targetName, allowed) = moves.getValue(command.noun)
return if (allowed(world))
targetName
else
roomName
}
I’m left with one case:
private fun inventory(command: Command, world: World): String {
world.showInventory()
return roomName
}
I think that I can perhaps express that as a lambda and then use the underbar notation. Let’s try:
fun command(cmd: String, world: World) {
val command = Command(cmd).validate()
val action: (Command, World)->String = when(command.verb) {
"inventory" -> inventory
"take" -> ::take
"go" -> this::move
"say" -> ::castSpell
else -> ::unknown
}
val name:String = action(command, world)
world.response.nextRoomName = name
}
Note that I removed the :: from inventory, because it is now:
val inventory = {command:Command, world:World ->
world.showInventory()
roomName
}
This works fine. It seems to me that this function no longer needs to be a method in Room, indeed doesn’t need to be in any class. I’m not sure where it should live. Fully public seems wrong. For now, for consistency, I’ll change the others. Ive committed this one already. Further changes:
val take = {command: Command, world: World ->
val done = contents.remove(command.noun)
if ( done ) {
world.addToInventory(command.noun)
world.response.say("${command.noun} taken.")
} else {
world.response.say("I see no ${command.noun} here!")
}
roomName
}
That’s kind of immense, but works. This is even worse:
val castSpell = {command: Command, world: World ->
val returnRoom = when (command.noun) {
"wd40" -> {
world.flags.get("unlocked").set(true)
world.response.say("The magic wd40 works! The padlock is unlocked!")
roomName
}
"xyzzy" -> {
val (targetName, allowed) = moves.getValue(command.noun)
if (allowed(world))
targetName
else
roomName
}
else -> {
world.response.say("Nothing happens here.")
roomName
}
}
returnRoom
}
Probably I can refactor these to be simpler. For now I just want to get them all in one form for consistency.
val unknown = {command: Command, world: World ->
world.response.say("unknown command ${command.verb}")
"unknown cmd ${command.verb}"
}
We’re green. A quick run of the game just to be sure. I think I can break the game, because of that return of “unknown cmd” … game runs and I can’t break it. Let’s commit and then figure out what’s up with that unknown
.
OK, what happens if I enter “argle bargle” as a command?
Let’s add that as a test to the CommandExperiment tests. Hm, we already have said test:
@Test
fun `evoke command error`() {
val command = Command("vorpal blade")
val result = command
.validate()
.execute()
assertThat(result).isEqualTo("command error 'vorpal blade'.")
}
I want more information, since we aren’t using execute:
@Test
fun `examine command error`() {
val command = Command("vorpal blade")
val result = command
.validate()
assertThat(result.verb).isEqualTo("vorpal")
assertThat(result.noun).isEqualTo("blade")
}
That passes. What happens in our case then?
On closer examination, it does say “unknown command vorpal” and I think it stays in the same room because we don’t change the room if given an invalid room name:
// World
fun command(cmd: String, currentRoom: Room): Room {
response = GameResponse()
currentRoom.command(cmd, this)
response.nextRoom = roomNamedOrDefault(response.nextRoomName, currentRoom)
return response.nextRoom
}
When we find that weird room name we stay in the same room. We should change our unknown
to do that explicitly, however:
val unknown = {command: Command, world: World ->
println("Arrived in unknown")
world.response.say("unknown command '${command.verb} ${command.noun}'")
roomName
}
That works. Commit: change unknown command to explicitly return the current roomName.
I noticed that I failed to convert move
to lambda. So:
val move = {command: Command, world: World ->
val (targetName, allowed) = moves.getValue(command.noun)
if (allowed(world))
targetName
else
roomName
}
Green. Commit: convert move to lambda.
Reflection
There’s something I don’t like about this design, which is that an object like Room (in particular) has methods for building, like go
and desc
, and methods for running, like move
and take
. (I changed go
back to move
when I realized I had overridden the existing one.)
I think this might call for a “companion object” but I’ll probably like to read about them more first. ALthough if IDEA wants to help …
IDEA isn’t interested in companion objects any more, but it did suggest making some things private (weak warnings), so I did that. If time permits at tonight’s Zoom session, I’ll ask my colleagues about how to do what we have here. For now, I’ve got all tests running, the game plays as intended, and there are no warnings.
Some of those lambdas are pretty large, something will have to be done about that. Maybe tomorrow!
And there are Italian sausages for supper, coming right up. See you next time!