Kotlin 56: Crash and Burn
I have an idea for an object even smarter than the SmartMap, that should make specifying the world every so much easier, nicer, more compact.
When I begin to wake up, my thoughts sometimes turn to depressing things. When I notice this happening, I turn my thoughts, instead, to my “work”, which is writing code and writing about it. Other mornings, my thoughts start right out with code. I suppose that when I was younger, other, more interesting thoughts came to mind, but I am old now and this is the way it goes.
So this morning, I was thinking about a smarter object that might help with this text game DSL that I’m working on. There’s already the SmartMap
, which is a map with two “sections”, global and local. In the game, global is used for things in effect for the entire game, local for things specific to a given room. Right now, in use, the SmartMap
maps from a String (a verb) to an Action, which is a lambda expression that defines the action to be taken when the verb is looked up.
This structure is good and useful, and it has a property which isn’t quite wonderful. Take “take” for example. The “take” verb takes an object noun, the thing you want to take1. And the code for that is already moderately complicated, and is handled, not directly in the lambda, but as a method on room:
val take = { imperative: Imperative, world: World ->
val done = contents.remove(imperative.noun)
if ( done ) {
world.addToInventory(imperative.noun)
world.response.say("${imperative.noun} taken.")
} else {
world.response.say("I see no ${imperative.noun} here!")
}
}
Here, we try to remove the noun to be taken from the room contents. If that is done, we add it to the player’s inventory, and say “Taken”, otherwise that thing did not occur in the room and we say “I see no bird here”, or whatever was asked for. This is already a bit of code, but suppose that we wanted, in the case of the cows, to say “You can’t take the cows”. That idea only applies to whatever room has cows in it, and in most games, that will be at most a small percentage of rooms. But, as things stand, we’d have to check for cows everywhere, by editing this method, and if we were to add penguins with special handling, we’d have to add them.
One design criterion for this toy project is that a world designer should be able to specify anything they want to do, all in the DSL, without needing to modify any of the classes of the system. They will have to know a bit of Kotlin, enough to write a fairly simple lambda, but they won’t have to be modifying the system itself.
So this morning, I thought of another smart object to help with that. I’m calling it a PhraseMap
, not to be confused with Phase Map, which is an entirely different thing. The idea of a PhraseMap
is that it has three phases (sorry), one where it looks for an exact phrase (take cows), one where it looks for a verb, ignoring the noun (take anything), and one where it looks for the noun, ignoring the verb (menace cows). Naturally, if it finds nothing, we’ll do something clever, if the SmartMap
can’t already handle our needs.
My plan is that each of the maps now found in the SmartMap
will become a PhraseMap
, local or global as before. If we think of the flow of searches for some input “xxxx yyyy”, sent to the SmartMap
containing two PhraseMaps
, it’ll go
local_exact->local_verb->local_noun->
global_exact->global_verb->global_noun
A Question
With an idea like this one, for a low-level intelligent object, there’s a risk: the idea may not be useful enough to be worth the effort. When that happens we have some choices:
- Ask around to see if people think it’d be useful.
- Try writing some code using it before it even exists.
- Just go for it.
I’ve asked everyone here chez Ron2, and I all agree that it would probably be useful. I should probably try using it, writing code that won’t really work, just to get the feel of it, which could then wind up building it top-down, which could be just fine.
And, OK, I’ll even try some top down, just a little bit. I’ve already thought about what it would look like, so I’m pretty confident, but why not write a test anyway.
OK, I’ve talked myself into doing the right thing, even though I really want to dive into making this cool object. Here’s a test for the feature:
@Test
fun `get a feel for PhraseMap`() {
val world = world {
room("no cows") {
desc(
"You are in a room with no cows",
"You are in a lovely room with no cows.")
go("s", "cow room")
action("take cows") { imp ->
say("There are no cows here to be taken")}
}
room("cow room") {
desc(
"you are in the cow room",
"you are in a large room with many cows")
go("n", "no cows")
item("cows")
action("take cows") { imp ->
say("We hang rustlers hereabouts, podner")}
}
}
var room = world.unsafeRoomNamed("no cows")
var response = GameResponse()
var command = Command("take cows")
com.ronjeffries.adventureFour.world.command(command, room, response)
assertThat(response.resultString).contains("are no cows")
response = GameResponse()
command = Command("s")
com.ronjeffries.adventureFour.world.command(command, room, response)
room = response.nextRoom
response = GameResponse()
command = Command("take cows")
com.ronjeffries.adventureFour.world.command(command, room, response)
assertThat(response.resultString).contains("hang rustlers")
}
Aside from the fact that I need to jump through my own orifice to do the test, this shows pretty much what I would like to have happen. For completeness, I’d have to add in taking something from each room that can be taken, but this gives the idea for what one would say … and I like what one would say.
Now can I try to build the object?
- Aside
- I’m an hour and a half in. When I first wrote this test, I had all the running stuff inside the world definitions braces, which compiles just fine but gave really bizarre results when run. So I spent at least an hour chasing that bug and even learned a little bit about how to run the IDEA debugger, which I shall try to forget immediately. Anyway, I feel somewhat derailed.
PhraseMap
I’ll begin with a test …
@Test
fun `phrase map`() {
val map = PhraseMap()
var p1 = Phrase("take", "cows")
val p2 = Phrase("take")
val p3 = Phrase(noun = "bananas")
map.put(p1) {"take the cows"}
map.put(p2) { "take anything"}
map.put(p3) { "deal with bananas"}
assertThat(map.get(p1)()).isEqualTo("take the cows")
}
After a personal interruption and some fumbling, I’m now 2 1/2 hours in and have the test running against these classes:
data class Phrase(val verb: String?=null, val noun: String?=null)
class PhraseMap() {
val both = mutableMapOf<Phrase,()->String>()
val verbs = mutableMapOf<Phrase,()->String>()
val nouns = mutableMapOf<Phrase,()->String>()
fun put(phrase: Phrase, save: () -> String) {
if (phrase.verb!=null && phrase.noun!=null) {
both.put(phrase, save)
} else if (phrase.verb==null && phrase.noun!=null) {
nouns.put(phrase, save)
} else if (phrase.verb!=null) {
verbs.put(phrase,save)
}
}
fun get(phrase: Phrase): ()->String {
val fromBoth = both.getOrDefault(phrase, null)
if (fromBoth != null) return fromBoth
val fromVerbs = verbs.getOrDefault(phrase, null)
if (fromVerbs != null) return fromVerbs
val fromNouns = nouns.getOrDefault(phrase, null)
if (fromNouns != null) return fromNouns
return {"not found"}
}
}
This serves, I think, as a proof of concept, but of course the types aren’t correct, in particular the return type of the PhraseMap. I’d like to do it as a generic, like the SmartMap, but the fact is I have exactly one use for it, so let’s see about setting it up for that. It needs to take an imperative-style lambda as its parameter. That imperative-style thing is called an Action. Let’s see if we can make our PhraseMap deal with that.
I’m a bit over three hours in and, although it’s working, I’m not happy. Let me see if I can work out why. It might just be that I’ve spent a lot of time beating my head against the difficulty of testing whether you get the right Action back, because actions return Unit so there is nothing to test.
@Test
fun `phrase map`() {
val imp = makeImperative()
val map = PhraseMap()
var p1 = Phrase("take", "cows")
val p2 = Phrase("take")
val p3 = Phrase(noun = "bananas")
val i1: Action = {i:Imperative->"take the cows"}
val i2: Action = {i-> "take anything"}
val i3: Action = {i-> "deal with bananas"}
assertThat(i1).isEqualTo(i1)
map.put(p1, i1)
map.put(p2, i2)
map.put(p3, i3)
assertThat(map.get(p1)).isEqualTo(i1)
}
I had to break out the Actions, so that I could check them. The strings inside aren’t accessible because Action is ()->Unit, i.e. returns nothing. And my class is this:
data class Phrase(val verb: String?=null, val noun: String?=null)
class PhraseMap() {
val both = mutableMapOf<Phrase,Action>()
val verbs = mutableMapOf<Phrase,Action>()
val nouns = mutableMapOf<Phrase,Action>()
fun put(phrase: Phrase, save: Action) {
println("putting $save ${save.hashCode()}")
if (phrase.verb!=null && phrase.noun!=null) {
both.put(phrase, save)
} else if (phrase.verb==null && phrase.noun!=null) {
nouns.put(phrase, save)
} else if (phrase.verb!=null) {
verbs.put(phrase,save)
}
}
fun get(phrase: Phrase): Action {
println("both")
val fromBoth = both.getOrDefault(phrase, null)
println("fromBoth $fromBoth ${fromBoth.hashCode()}")
if (fromBoth != null) return fromBoth
println("verbs")
val fromVerbs = verbs.getOrDefault(phrase, null)
if (fromVerbs != null) return fromVerbs
println("nouns")
val fromNouns = nouns.getOrDefault(phrase, null)
if (fromNouns != null) return fromNouns
println("nothing")
return {"not found"}
}
}
But wouldn’t a simple map work just as well? Let’s try it.
val map = mutableMapOf<Phrase,Action>()
The test runs perfectly. This amazing three-level object accomplishes nothing that a plain vanilla single map can do. What makes it work, I guess, is the Phrase
, which explicitly allows for null
verb or noun. The map handles that just fine, and if we were to give it a default, in case nothing is found, it would be just fine. With a little fiddling, we should be able to convert our SmartMap to use maps from Phrase to Action, and these searches would work, and values could be overloaded into the local map, as we intend.
I’ve spent three hours of the last few years of my life on this, to discover that there’s an even simpler idea that might work.
- Am I Bovvered?3
- Am I bovvered by having spent a few hours playing with this idea? Honestly, no. I was feeling some tension from the many mistakes I made trying to get the types lined up, and then I got truly frustrated because I forgot that my Actions return Unit and therefore I couldn’t see those nice strings inside. (I have an idea now for something I might be able to do for better testing.)
-
I’m a bit saddened because it seems I don’t need this really cool object that I thought of, but I’m quite pleased at the possibility that a simple map from phrase to action might allow me to easily set up special actions in different rooms.
-
Presumably if I were a bit smarter, I’d have seen the possibility while still waking up, but if it takes me three hours to make the system better, that investment is worth it to me.
-
I ain’t bovvered!