Kotlin 96
More world-building.I’ll try to devise some affordances.
World-building is tedious. I certainly understand why G*d spent only 6 days at it. And there are a lot more conditionals and glitches in the real world than in my game one. I’ve given up on fixing most of the real-world problems. In here, maybe I can do something.
Here’s what it takes to provide a gate that can be unlocked, and we’re not even done with the feature yet:
action("unlock", "gate") {
if (inventoryHas("keys")) {
say("You fumble through the keys and finally unlock the gate!")
flags.get("openGate").set(true)
} else {
say("The gate is locked.")
}
}
go(D.West, R.LowCave) {when(flags.get("openGate").isTrue) {
true -> true
false-> {
say("The gate is locked")
false }
}
}
One issue is that the allowed
anonymous function on go
needs to see a true or false return. So that function above gets the flag, checks it for truth, then either just returns true or says “The gate is locked” and returns false.
I think we can do better. One idea would be to have two functions for saying things, yes
and no
, that say the thing, then return true or false. Let’s try that first.
I should probably write a test for these but I’ve had a rough morning, and I’m sure it’ll be just fine, so I’ll test it in the game.
fun yes(s:String): Boolean {
say(s)
return true
}
fun no(s:String): Boolean {
say(s)
return false
}
And in play:
go(D.West, R.LowCave) {
when (flags.get("openGate").isTrue) {
true -> yes("You open the gate and enter.")
false -> no("The gate is locked.")
}
}
We could implement a helper on flags …
class GameStatusMap {
private val map = mutableMapOf<String, GameStatus>()
fun get(name:String): GameStatus = map.getOrPut(name) { GameStatus() }
fun truth(name: String): Boolean = map.getOrPut(name){ GameStatus() }.isTrue
}
And then:
go(D.West, R.LowCave) {
when (flags.truth("openGate")) {
true -> yes("You open the gate and enter.")
false -> no("The gate is locked.")
}
}
Maybe that’s better. What we’ll want to do is to settle on a particular way of writing these and use it all the time. Then existing code will guide future code.
I test and commit.
The syntax here is a bit arcane. we could implement a helper in World or Room. We’ll leave that for now, I think.
I wonder whether we should declare all the flags as an enum
. Then we could just talk to them instead of decoding those strings. F.openGate.truth
sort of thing. We’ll keep that in mind.
Now what about the other bit, the unlocking:
action("unlock", "gate") {
if (inventoryHas("keys")) {
say("You fumble through the keys and finally unlock the gate!")
flags.get("openGate").set(true)
} else {
say("The gate is locked.")
}
}
That’s not too bad … except that we’d like to have “unlock” and “open” and “use keys” also do this same action.
Let me just type something in so as to look at it.
val actions = listOf<String>(
"unlock gate", "use keys", "unlock"
)
action(actions) {
if (inventoryHas("keys")) {
say("You fumble through the keys and finally unlock the gate!")
flags.get("openGate").set(true)
} else {
say("The gate is locked.")
}
}
The new version of the action
DSL word would create multiple entries. Note that I’m taking a chance by allowing a two-word string rather than forcing everyone to type Phrase
or something.
Let’s try to make that work. Should be “easy”.
- Aside
- I’ll show the code in a moment, but it appears to me that we cannot have a single word action defined. I provide that as well. Here’s what I’ve got now:
fun action(verb: String, noun: String, action: Action) {
actions.add(Phrase(verb, noun), action)
}
fun action(verb: String, action: Action) {
actions.add(Phrase(verb), action)
}
fun action(actions: List<String>, action: Action) {
actions.forEach { makeAction(it, action) }
}
fun makeAction(command:String, action: Action) {
val words = command.lowercase().split(" ")
when (words.size) {
1 -> action(words[0], action)
else -> action(words[0], words[1], action)
}
}
There are now three possible action
phrases, one with verb and noun, one with just verb, and one with a list of strings. The latter creates multiple entries, one for each element of the list. It’s very permissive. If you were to give it “bump the troll”, it would make an action for “bump the”.
The new feature with the list of actions works … in the room. I should duplicate it in the World.
Easily done, but now we have four identical methods in World and Room classes. Now I need to devise some useful way to reduce that duplication, given that I actually need three of those four methods to be accessible in both World and Room class.
- Aside
- I am somewhat discombobulated this morning, owing to an issue that we’ll skip over, and it has made me unwilling to write tests. A curious disability. Let’s bear down and write a few.
@Test
fun `actions with one and two words, in a list`() {
val world = world {
room(R.Z_FIRST) {
val actions = listOf("unlock gate", "unlock", "open gate")
action(actions) {
say("unlocking")
}
}
}
val player = Player(world, R.Z_FIRST)
var result = player.command("unlock gate")
assertThat(result).contains("unlocking")
result = player.command("unlock")
assertThat(result).contains("unlocking")
result = player.command("open gate")
assertThat(result).contains("unlocking")
}
That wasn’t so bad. Runs green. Let’s do world similarly:
@Test
fun `world actions with one and two words, in a list`() {
val world = world {
val actions = listOf("wave wand", "lux", "say aperto")
action(actions) {
say("no magic allowed here")
}
room(R.Z_FIRST) {
}
}
val player = Player(world, R.Z_FIRST)
var result = player.command("wave wand")
assertThat(result).contains("no magic allowed here")
result = player.command("lux")
assertThat(result).contains("no magic allowed here")
result = player.command("say aperto")
assertThat(result).contains("no magic allowed here")
}
Tests are green. Commit: New actions capabilities tested in both Room and World.
I think, given the events of the day, we’ll stop here.
Summary
We’ve added a couple of valuable things here. First, we’ve enabled one-word action commands, which didn’t exist before, although I wasn’t aware of it because I’d never wanted one before today. Second, we’ve built a fairly convenient way to allow synonymous commands in actions.
Arguably, all those synonyms should be sorted out in parsing. At this moment, I’m not up for working on that. This may indicate that despite its simplicity, the command parsing and interpretation is either unclear or otherwise flawed. When we fear to tread somewhere, there’s a reason for it, and it’s generally that the code needs improvement.
I used the game for my initial tests, because I had the code in mind, and because I am somewhat upset, which made me less inclined to do the tests. But I finally squared my shoulders and wrote the tests. They were easy.
There is duplication of the action-related code in World and Room. We should get rid of that somehow. A helper object might do the job, leaving the World and Room forwarding methods to that helper, but all the actual code in the helper. Still some duplication.
There might be some way to build an interface for the action-making, and delegate its implementation to an instance of it? But … our methods need a parameter, which is the list into which the actions should be placed. Still, maybe if Actions class were to have those methods, which it probably should, maybe we could have Actions implement an Interface and then use Kotlin’s by
keyword?
I’ve never managed to make that work. This may be an opportunity.
For later. I’m done for now. See you next time!