Kotlin 105
Let’s see if we can figure out a clean way to accomplish yesterday’s accomplishment.
Yesterday, I finally recognized that only the Player
can properly know the prior room she was in. The World could remember it, but we are operating with a rule that there could be more than one player in the game at the same time, so that the player object needs to remember its current room, and if there is to be memory of where we were, that, too, needs to be in Player
. Therefore, the GameResponse
needs to have access to the player, so that it can have access to the prior room.
I don’t fully recall my motivation — often I move intuitively — but it was probably my concern that there were a lot of creators of GameResponse
objects that caused me to create this code that deals with the possibility that the response may not know the player:
class GameResponse(startingRoomName:R = R.Z_FIRST) {
var player: Player? = null
fun definePlayer(currentPlayer: Player) {
this.player = currentPlayer
}
fun goToPriorRoom() {
val name = player?.priorRoomName
if (name != null ) moveToRoomNamed(name)
}
The code accepts the possibility that player
might be null, and therefore that we might not get a name
. I think it is hard to be proud of this code, especially in the Kotlin context, where so much of the language is centered around avoiding trouble with nulls.
What do the creators of GameResponse look like? Despite my concerns, there are only three references to the class, but they do present issues:
class World(val actions: IActions = Actions()) :IActions by actions {
var response: GameResponse = GameResponse()
fun command(cmd: Command, currentRoomName: R, player: Player): GameResponse {
response = GameResponse(currentRoomName).also { it.definePlayer(player) }
currentRoomName.command(cmd, this)
return response
}
class PlayerResponseTest {
@Test
fun `response has say method`() {
val r = GameResponse(R.Z_FIRST)
r.say("One thing")
r.say("Another thing")
assertThat(r.sayings).isEqualTo("One thing\nAnother thing\n")
}
}
The last usage, the test, just tests the ability of the response to save up things “said”. I could imagine ditching that test, because the issue is covered by other tests of game play. Still, one hates to delete tests when they become inconvenient.
The code in World
shows us that the World creates the response, and that the command
is given the player, which the world then patches into the response after creating it. Makes shivers run up and down the spine, doesn’t it? When we see something like that, we just know we’re working around something that isn’t quite right.
Did we just make that change to pass in the player yesterday? Yes. And with so few creators of the response, let’s just change its constructor to require it. (An alternative would be to have Player
create it. This seems more straightforward.)
fun command(cmd: Command, currentRoomName: R, player: Player): GameResponse {
response = GameResponse(currentRoomName, player)
currentRoomName.command(cmd, this)
return response
}
OK, I’m demanding that the constructor accept player. IDEA tells me that it doesn’t. I am not surprised. But I can fix that, and I’ll do it by hand.
class GameResponse(startingRoomName:R = R.Z_FIRST, private val player: Player) {
fun goToPriorRoom() {
moveToRoomNamed(player.priorRoomName)
}
That looks more like code we could show at show and tell without covering our head with a napkin so that God cannot see who wrote it1.
We have that one test. Let’s just let it create a player. This is made more difficult by the fact that Player
wants a world. In for a penny:
@Test
fun `response has say method`() {
val r = GameResponse(R.Z_FIRST, Player(World(),R.Z_FIRST))
r.say("One thing")
r.say("Another thing")
assertThat(r.sayings).isEqualTo("One thing\nAnother thing\n")
}
That leaves me with this one, in World initialization:
class World(val actions: IActions = Actions()) :IActions by actions {
var response: GameResponse = GameResponse(R.Z_FIRST, Player(this, R.Z_FIRST) )
Because my Mommy didn’t raise no dummies2, I notice that GameResponse seems to be receiving the starting room name redundantly, since it’s in the Player. Tests are green. Commit: GameResponse is created with Player as well as starting room. It would seem that we could do something with this:
class GameResponse(startingRoomName:R = R.Z_FIRST, private val player: Player) {
var nextRoomName = startingRoomName
This seems likely to work:
class GameResponse(startingRoomName:R = R.Z_FIRST, private val player: Player) {
var nextRoomName = player.currentRoomName
Test. Green. So we can remove that first parameter throughout. IDEA is hot to help. IDEA considers this so easy that it raises just a yellow light, not a red one. Select and Voila! the parameter is removed from all creators.
Commit: GameResonse now created only with Player parameter.
Let’s reflect.
Reflection
I ended my session yesterday tired. I’d rolled back a time or N making the prior room work. My mind was frazzled. I knew the solution I had was rickety, but I also knew that the tests were green, which made it safe to commit what I had and take a break. Today, in a series of small and simple steps, the code is more simple and not rickety at all, at least not in this area. We might find things to improve elsewhere.
The mission over the last few sessions has been to see whether we can create a Room of Infinite Darkness, where we can send people when the lights are out, and which can return them to where they were, when the lights go back on. It appears that we can, and the code is rather nice:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
action {
if (it.verb == "lamp" && it.noun=="on") {
response.goToPriorRoom()
} else if (it.verb=="do" && it.noun == "something"){
// ignore command
}
}
}
There’s a secret thing happening here that we might want to mention and think about: the action
DSL command with no parameters causes the game to pass all commands first to that action lambda. This means, as written, that no commands, whether to move, take or drop things, take inventory, ask for help, will have effect unless we handle them directly here first. An example is in another test:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
go(D.North, R.Z_FIRST)
action {
if (it.verb=="go" && it.noun=="north") {
it.notHandled()
} else {
say("You have been eaten by a grue!")
}
}
}
Here, when we see “go north”, our call to it.notHandled()
causes the world to attempt to understand the command. The notHandled
is generally issued by a default empty command in the room’s actions. Here, we have overridden that empty command to bring us into our action block.
It occurs to me that we don’t have to check for commands in an if block. I think we can do this:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
go(D.North, R.Z_FIRST)
action {
say("You have been eaten by a grue!")
}
action("go", "north") {
it.notHandled()
}
}
Test that. Green. Commit: improve test code showing how to do dark room.
This tells me that in the other test I could say this:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
action {
}
action("do", "something") {
say("I can't see to do anything.")
}
action("lamp", "on") {
response.goToPriorRoom()
}
}
Ah. Now we see that what we really want this naked action
to do is replace the entry in actions that defers things we don’t understand to the world. So maybe we shouldn’t call it action
at all. We could create a method with a more ominous method name. Let’s do this:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
handleAllActions()
action("do", "something") {
say("I can't see to do anything.")
}
action("lamp", "on") {
response.goToPriorRoom()
}
}
I think we’ll implement this on IActions and Actions, renaming the empty actions
member, which is too dangerous to live:
interface IActions {
fun act(imperative: Imperative)
fun action(verb: String, noun: String, action: Action)
fun action(verb: String, action: Action)
fun action(commands: List<String>, action: Action)
fun add(phrase: Phrase, action: Action)
fun clear()
fun handleAllActions(action:Action)
}
class Actions: IActions ...
override fun handleAllActions(action: Action) {
add(Phrase(), action)
}
I think we may now have a few unhappy people who are calling the missing actions
. We’ll let IDEA find them and we’ll fix them up:
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
handleAllActions {}
action("do", "something") {
say("I can't see to do anything.")
}
action("lamp", "on") {
response.goToPriorRoom()
}
}
room(R.Darkness) {
desc("Darkness", "Darkness. You are likely to be eaten by a grue.")
go(D.North, R.Z_FIRST)
handleAllActions {
say("You have been eaten by a grue!")
}
action("go", "north") {
it.notHandled()
}
}
I think that’s a bit better. Commit: replace empty actions()
with handleAllActions
in DSL.
We’ll continue reflecting …
Reflection Continues …
It seemed to me that it would be too easy to type actions() { ... }
, meaning to put in the words later, and wind up with a room where nothing worked, and leaving us with no idea why. So it made sense to me to have a special word for handling all actions. The change was trivial. Broke a couple of tests who were using the old form, just as intended: we want an error if you forget to put in any words.
Is that the best possible name? Maybe not. Would defaultAction
or defaultAllActions
be better? How about handleAllOtherActions
?
I’m not sure. I’ll let it ride and see what I feel when implementing the dark room. It seems likely that there may never be another use for this capability. Well, there could be a trap room of some other kind, I guess.
Looking ahead, we will find ourselves implementing a lot of general commands in the dark room, as mentioned above,, “inventory” and the like. We do have the ability to define a batch of commands all at once, so that may not be too tedious. We’ll see when the time comes.
Deeper Reflection …
There is a pattern to my behavior that I’d like to underline here. I am very comfortable with partial solutions. They have to work: I’m not comfortable with things that will throw exceptions or break the game. But if the code runs and the words aren’t just right, or the design is a bit rickety, I am OK with that. Software development is a long game made up of short moves, and when something isn’t quite right, we’ll probably see it again soon. Even one night’s sleep often gives us a better idea. Sometimes some other team member has a better idea. When we see a better way, if our code is modular and not muddled, the changes are generally easy to make, and can often be made incrementally if need be. And if we never again stumble over the code that isn’t quite lovely, that’s OK too … if it doesn’t hurt, it ain’t broke. Let it be.
Usually, we do see it again, and when we do, well, I’m confident in the tests and in my ability to do something better tomorrow than I did today.
This isn’t just one man’s clumsy way of working. On the contrary3, I’ve chosen the style consciously and used it for years, because I wanted to see what would happen if we wrote the best code we could on any given day, but without a lot of large grand design: just decent code, small bites, on and on.
And, if I do say so myself, most of this program is quite modular, quite decent. As I scan the code, the vast majority of methods are one line long. In a spirit of too much is not enough, I’ve converted most of them to “expression body”, to get a sense of using that idiomatic aspect of Kotlin. One of the weirdest might be this:
fun yes(s:String): Boolean = true.also { say(s) }
This could be, and indeed used to be written this way:
fun yes(s:String): Boolean {
say(s)
return true
}
I think the one-line way is more in the style we’d like to adopt here at L’Atelier Ron4.
I’ll submit that question to the Zoom Ensemble, Friday Geek’s Night Out tonight, to see what abuse they want to heap upon me.
Sliding into Summary
Be that as it may, we have, in a few days of very simple steps, created a powerful new capability, the Trap Room, that can take over the entire game’s input if it cares to, and that can be used to implement global situations like darkness. We’ll see if we have additional use for it, but even for that single use, it’s an inexpensive and capable new feature.
A good morning’s work. See you next time!
-
Thank you, Anthony Bourdain. RIP. ↩
-
She left me out in the woods to be eaten by wolves, where instead I was found and raised by a band of roving Jesuits. Thus my unquestioned brilliance and argumentative nature. OK, questioned brilliance. ↩
-
Why didn’t he say “Au contraire”? He’s pretending not to have a weird fascination with rudimentary French. Can’t fool us. ↩
-
There he goes. ↩