Kotlin 42
This morning I plan to share some early thoughts and perhaps turn them into code. Chaos almost guaranteed. To enjoy this article, please set your expectations to ‘I wonder how Ron might go from a vague dreamy idea to something that might actually be useful’.
It Seems To Me …
- Vague Beginning
- I begin with some very vague early morning ideas … but in not too long a time, I’ll get down to code. Things will go better after that.
What we’re trying to get from the Command object and the tables in the Context and all that is something I’ll call an Imperative. A thing the game is to “do”. An imperative, in its final form will want to come down to a lambda, the code that does the thing, and an argument, the “noun” that, if it exists, tells the command just what to do. “go east”. “say xyzzy”.
Now it seems to me that we can represent our entire syntax, or most of it, it’s still early here, with a big table.
Let’s imagine that there’s a noun <none>, just in case we need it, and I think we will. Think of this table:
e -> @go east
east -> @go east
w -> @go west
go w -> @go west
...
xyzzy -> @say xyzzy
inventory -> @inventory <none>
take X -> @take X
I’m thinking of the “@a b” notation as meaning that the result is an Imperative, and its verb is a particular lambda associated with the @ word, and the second bit is the noun. I’m not sure if it’s a simple string or a little object. It’d probably be best if it were an object: things are usually better that way.
The implication of this thinking would be that the conversion of an input string into a legitimate imperative, including the management of synonyms might be managed with a simpler series of operations than our current one, and with fewer tables.
I’m fussing in my mind with the large number of substitutions, “e” for “east” and so on, that will permit just one definition of where to go in a room, so that in the DSL you can define
go("n","wellhouse")
And not need to also say
go("north","wellhouse")
because “n” and “north” are built-in synonyms.
Now there are “advanced” and even just “well-known” lexing and parsing techniques that can deal with simple issues like these with one hand tied behind their back. But if anyone here has those all memorized and to hand, it’s you, because I’ve forgotten all I knew about compilers literally decades ago. And we’re not here to write a compiler or learn a compiler framework, we’ve just got a handful of one- and two-word commands that need sorting.
But we’d like to do it neatly enough that when someone reads this code, they’d see what we did, understand it, and think “hey, that’s not bad”.
We do have a particular opportunity here, which is the possibility that we could define all of this in our DSL. Now that, I think, would be quite nice.
I’m wanting to look at some of yesterday’s code, the brand new operations table:
override val operations: Map<String, (Command) -> String> = mutableMapOf(
"commandError" to {command: Command ->
"command error '${command.input}'." },
"countError" to {command: Command ->
"I understand only one- and two-word commands, not '${command.noun}'."},
"go" to {command: Command ->
"went ${command.noun}." },
"inventory" to {command: Command ->
"Did inventory with '${command.noun}'."},
"say" to {command:Command ->
"said ${command.noun}." },
"take" to {command:Command ->
"${command.noun} taken."},
"verbError" to {command: Command ->
"I don't understand ${command.noun}."},
Let’s clean this up a bit. Our map is really a map from a Verb to a command. Let’s use typealias
to say that.
typealias Verb = String
...
override val operations: Map<Verb, (Command) -> String>
= mutableMapOf(
"commandError" to {command: Command ->
"command error '${command.input}'." },
...
That’s a bit nicer. Tested, commit: typealias Verb = String in Command…
Lets review all the existing tables in our CommandContext and think about commonalities.
interface CommandContext {
val directions: List<String> // {e, east, w, west, up, dn, down ...}
val ignoredNounWords: List<String> // {inventory, look }
val magicWords: List<String> // {xyzzy, plugh }
val operations: Map<Verb,(Command)->String> // {take->(), ...}
}
That second one,ignoredNounWords, is very poorly named. Rename to “verbsTakingNoNoun”. Test, commit: rename context table to verbsTakingNoNoun. So, as I was saying, reviewing the existing tables:
interface CommandContext {
val directions: List<String> // {e, east, w, west, up, dn, down ...}
val verbsTakingNoNoun: List<String> // {inventory, look }
val magicWords: List<String> // {xyzzy, plugh }
val operations: Map<Verb,(Command)->String> // {take->(), ...}
}
My morning fantasy is/was that these tables can be generalized. Right now we have some generality in the code:
fun goWords(): Command {
return substituteSingle("go", context.directions)
}
fun magicWords(): Command {
return substituteSingle("say", context.magicWords)
}
fun substituteSingle(sub: String, singles: List<String>): Command {
if (words.size == 2) return this
if (words[0] in singles) words.add(0, sub)
return this
}
What exactly are you up to, Ron?
Now, there’s a principle here that I’m trying to apply, in my vague way of working. The notion is that if there’s a choice between representing something as data or as code, it’s generally preferable to represent it as data. (Certainly if the data is going to look too much like JSON or XML, this idea may have been taken too far. Still, it’s often a good thing to do.)
What does that code above do, and how could we better represent it, perhaps by providing better data?
What those two methods do is take a single word (they check words.size != 2) and transform it to a pair of words, either verb = “say” or “go”, and noun = the first word.
What might such a transformation look like as a Kotlin collection of some kind? Or packaged as a table lookup? Let’s think about it.
We want to express transformations like this:
"xyzzy" -> Imperative("say", "xyzzy")
"e" -> Imperative("go", "e")
"east" -> Imperative("go", "e")
Maybe we don’t want to deal with the “e” and “east” here but maybe we do.
- Down To Code
- Here I begin to make things concrete. My thoughts weren’t hanging together well enough to keep building cloud castles. I need to get concrete. Some folx might turn to drawing pictures. I choose to create code.
Let’s play with some code. If I were more adept with Kotlin, I might skip this but I have a feeling that even if I were to reach full wizard, I’d like the code to participate in this discussion1. I’ll make a new test file to spike in.
I quickly start with this:
class Imperative(val verb: String, val noun: String)
class ImperativeSpike {
@Test
fun `create Imperative`() {
val imp = Imperative("go", "east")
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
}
}
Let’s make a note that while we might want the verb available, we probably really want to execute an imperative in a context, so it might have, or have quick access to, the lambda corresponding to the verb.
So suppose we turn up with “east” and we want to end up with the Imperative above. What kind of table could handle this?
I’m starting to think this is futile, wasted effort. The table, though, let’s TDD it out:
- Not Futile
- It turns out that while I was tempted to stop and throw all this away, something caused me to work just a bit more to improve some things, and I think we wind up in a very enviable position.
@Test
fun `look up some imperatives`() {
var imp: Imperative = impTable.get("east")
assertThat(imp).isEqualTo(Imperative("go","east"))
}
What is impTable
? With a switch to getValue
, we can do this:
@Test
fun `look up some imperatives`() {
var imp: Imperative = impTable.getValue("east")
assertThat(imp).isEqualTo(Imperative("go","east"))
}
}
val impTable = mapOf("east" to Imperative("go","east")).withDefault {
(Imperative("none", "none"))
}
I freely grant that I’m just feeling my way here. I think I want an object here, not just a relatively raw table. Let’s try this:
@Test
fun `look up some imperatives`() {
val imperatives = ImperativeMap(impTable)
var imp: Imperative = imperatives.getValue("east")
assertThat(imp).isEqualTo(Imperative("go","east"))
}
}
class ImperativeMap(val map:Map<String,Imperative>) {
fun getValue(s:String): Imperative {
return map.getValue(s)
}
}
val impTable = mapOf("east" to Imperative("go","east")).withDefault {
(Imperative("none", "none"))
}
That passes, and now we have an intelligent object to work with. Let’s make our test harder:
@Test
fun `look up some imperatives`() {
val imperatives = ImperativeMap(impTable)
var imp: Imperative = imperatives.getValue("east")
assertThat(imp).isEqualTo(Imperative("go","east"))
imp = imperatives.getValue("e")
assertThat(imp).isEqualTo(Imperative("go","east"))
}
Note that we want “e” to turn into “go east”. We could just add that pair to the map, but instead let’s add a synonyms table. I’ll default it in:
class ImperativeMap(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun getValue(s:String): Imperative {
return map.getValue(synonyms.getValue(s))
}
}
val synMap = mapOf("e" to "east", "n" to "north").withDefault { it }
val impTable = mapOf("east" to Imperative("go","east")).withDefault {
(Imperative("none", "none"))
}
I’m starting to like where this is going. It seems that we could fairly easily extend our DSL to allow the definition of all these things. Maybe.
Let’s go a step further and make an Imperative able to act. For now, we’ll give it no context to act within, just simple action lambdas.
@Test
fun `imperative can act`() {
val imperatives = ImperativeMap(impTable)
var imp: Imperative = imperatives.getValue("east")
assertThat(imp.act()).isEqualTo("went east")
imp = imperatives.getValue("e")
assertThat(imp.act()).isEqualTo("went east")
}
I implement a stub method on Imperative:
data class Imperative(val verb: String, val noun: String) {
fun act():String {
return "foo"
}
}
This should suffice to get the test to fail finding foo rather than whatever it wanted:
Expecting:
<"foo">
to be equal to:
<"went east">
but was not.
Now we need to map our verbs to actions. Let’s inline that, sort of:
data class Imperative(val verb: String, val noun: String) {
fun act():String {
val action:(Imperative) -> String = when(verb) {
"go" -> { i -> "went $noun" }
else -> { i -> "I can't $verb a $noun"}
}
return action(this)
}
}
The test passes. Let’s make it harder:
@Test
fun `imperative can act`() {
val imperatives = ImperativeMap(impTable)
var imp: Imperative = imperatives.getValue("east")
assertThat(imp.act()).isEqualTo("went east")
imp = imperatives.getValue("e")
assertThat(imp.act()).isEqualTo("went east")
imp = Imperative("forge", "sword")
assertThat(imp.act()).isEqualTo("I can't forge a sword")
}
That passes. Commit: ImperativeSpike handling synonyms, single word commands, and imperative actions. Early days.
Oops, I see that I didn’t commit that rename earlier. Let’s hope that I don’t have to back this out. Time to lift head out of sand and reflect on what we’ve got happening here.
Reflection
We have an object, the Imperative, that knows a verb, a noun, and can produce an action that is looked up in a table. The action lookup expects to pass the imperative to the lambda. If the imperative had a Context, then the lambda would have access to anything it needed. We’ll see whether we go that way, whether we use this at all.
There is an ImperativeMap
that currently maps from a string value to an imperative that is expanded. This map is intended to translate, for example “e” to “go east”.
It seems to me that our ImperativeMap
should have a better entry point than just “get value”, consisting of a verb string and noun string, the latter possibly empty, and return an imperative where both strings have been checked for being valid words, and returning a canonical error imperative otherwise.
It may seem that I am reinventing parsing here. Yes, that’s what’s going on. If I like this better, and so far, I am, we’ll see about actually using it.
- Pause
- I paused here to consider whether this is worth doing. Always good to think about it. I’m glad I decided to continue.
“Should” I be doing this? Would it be better to do some features? Well, maybe, but I’m new here, and some of the features we want require everything to be defined in our DSL, so we do need to get our parsing reasonably right. However, I could accept that we could instead push forward on features and probably be OK, perhaps with a more clunky parsing arrangement. If we were in a hurry to get done, this might not be the best use of our time.
But we’re not. We’re here to create this game as a way to learn and become adept with Kotlin. We’re learning to solve problems with Kotlin, by solving problems with Kotlin.
Let’s rename our ImperativeMap’s getValue
method (and I bet we rename the class soon). We want to accept two words, verb and noun.
We’ll go in tiny steps:
class ImperativeMap(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun getValue(verb:String, noun:String = ""): Imperative {
return map.getValue(synonyms.getValue(verb))
}
}
You know what? I don’t like that already. But first let’s go a bit further. Should be green. Commit: ImperativeMap getValue accepts optional and unused second arg, noun.
Rename the method to, I don’t know, create.
class ImperativeMap(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String, noun:String = ""): Imperative {
return map.getValue(synonyms.getValue(verb))
}
}
Green. Commit: rename getValue to create. IDEA’s rename handled the users of the function. We’ll see them next time we look at the tests.
Let’s do a test for a two word legal command.
@Test
fun `two word legal command`() {
val imperatives = ImperativeMap(impTable)
var imp = imperatives.create("go", "e")
assertThat(imp.act()).isEqualTo("went east")
}
That should be hard enough. Now to what I already don’t like: the optional argument in the method:
class ImperativeMap(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String, noun:String = ""): Imperative {
return map.getValue(synonyms.getValue(verb))
}
}
We can have functions with the same name and different arguments, so let’s break this out. It’ll mean that we don’t have to check anything about the noun. (I’m not entirely sure how this will work at the calling end. We’ll see …)
class ImperativeMap(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative {
return map.getValue(synonyms.getValue(verb))
}
fun create(verb:String, noun:String): Imperative {
return map.getValue(synonyms.getValue(verb), synonyms.getValue(noun))
}
}
This will not stand until the getValue
in our map can handle both. Let’s back off a bit.
OK, this is a bit nasty. What I’m trying to do here is to make it work, then make it right. Seeing what I have to go through to make it work should give me a clue. I started with this:
fun create(verb:String, noun:String): Imperative {
val v = synonyms.getValue(verb)
val n = synonyms.getValue(noun)
val imp = map.getValue(v)
return Imperative(imp.verb, n)
}
The issue is that the map only maps single word commands to Imperatives, as we’ll see in a moment. So I get the map give me an Imperative for the verb, and then return a new one with the correct noun plugged in. (I felt that was preferable to setting the noun in the returned one. Immutable is better when we can.)
The map was improved to understand “go”:
val impTable = mapOf(
"east" to Imperative("go","east"),
"go" to Imperative("go", "irrelevant")
).withDefault {
(Imperative("none", "none"))
}
The issue, of course, is that the impTable isn’t smart enough to deal with anything other than a verb, but it’s trying to return an imperative expanded from the verb if need be.
- Doubts
- Again I question my progress, but ultimately decide to push even further. A good decision, I think.
I’m feeling that this spike is about done teaching me things but I want to push it a bit further. I’m three hours in, so I’m nearing some kind of limit but I feel the need to understand a bit more. I want to pop up my stack and return to thinking about the basic idea, in the light of what I’ve done here so far.
Reflection
I like the Imperative object. It’s not very different from Command, except that Imperative knows inherently how to convert its verb to an action. We could easily take that learning and extend Command, or replace it at the right point with an Imperative.
I like the idea of an object, currently named ImperativeMap but probably should be named ImperativeFactory, with its two create
methods, one that takes a verb, and one that takes both a verb and a noun. I like that it applies synonym conversion to each. It’s possible that there should be more than one synonym table, depending on the kind of thing that has come in.
And I like the fact that the factory thing creates a ready-to-go Imperative.
And I’m beginning to like how much of it is done in table lookups from tables that I can at least imagine creating in the DSL.
Let’s rename ImperativeMap to …Factory for clarity. Test. Commit.
I don’t like the necessity of creating the new Imperative here:
fun create(verb:String, noun:String): Imperative {
val v = synonyms.getValue(verb)
val n = synonyms.getValue(noun)
val imp = map.getValue(v)
return Imperative(imp.verb, n)
}
Back to Code
I’ve not come to large conclusions about this spike yet. I have an idea that I’d like to try.
Let’s make a synonym method here:
class ImperativeFactory(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative {
return map.getValue(synonyms.getValue(verb))
}
IDEA extract function does this for me, nicely:
class ImperativeFactory(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative {
return map.getValue(synonym(verb))
}
fun create(verb:String, noun:String): Imperative {
val v = synonym(verb)
val n = synonym(noun)
val imp = map.getValue(v)
return Imperative(imp.verb, n)
}
private fun synonym(verb: String) = synonyms.getValue(verb)
}
Now I would like to inline the calls in the second method. Let’s see if it can do that too. I think it’s going to depend on just how I hold my mouse2. Ha! Select the variable, “Inline Property”, gives me this so far:
fun create(verb:String, noun:String): Imperative {
val n = synonym(noun)
val imp = map.getValue(synonym(verb))
return Imperative(imp.verb, n)
}
Do again (Opt-Cmd-n) to get:
fun create(verb:String, noun:String): Imperative {
val imp = map.getValue(synonym(verb))
return Imperative(imp.verb, synonym(noun))
}
I could inline some more but I think we won’t like the result:
fun create(verb:String, noun:String): Imperative {
return Imperative(map.getValue(synonym(verb)).verb, synonym(noun))
}
Let’s push this until it breaks. What if map.getValue()
was a method? Let IDEA extract map.getValue(synonym(verb))
and I get this:
class ImperativeFactory(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative {
return imperative(verb)
}
fun create(verb:String, noun:String): Imperative {
return Imperative(imperative(verb).verb, synonym(noun))
}
private fun imperative(verb: String) = map.getValue(synonym(verb))
private fun synonym(verb: String) = synonyms.getValue(verb)
}
What if Imperative understood the method setNoun()
?
fun setNoun(noun: String): Imperative {
return Imperative(verb,noun)
}
I just typed that in. Maybe IDEA could have helped me, I couldn’t think of what I’d even ask. Anyway now I can say this:
fun create(verb:String, noun:String): Imperative {
return imperative(verb).setNoun(synonym(noun))
}
This is rather nice and it preserves my immutable Imperative.
Let me push the limits more here.
class ImperativeFactory(val map:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative = imperative(verb)
fun create(verb:String, noun:String) = imperative(verb).setNoun(synonym(noun))
private fun imperative(verb: String) = map.getValue(synonym(verb))
private fun synonym(verb: String) = synonyms.getValue(verb)
}
Tests are green. Commit: ImperativeFactory refactored to almost terrible simplicity.
I say “almost terrible” because this is getting pretty idiomatic and while I’m pretty comfortable with it, I can imagine teams that would prefer things a bit more written out.
Reflection
I am now inclined to push this as far as I can, to see how nice I can make it. (By my standards of “nice”.)
The parameter map in my ImperativeFactory. What would be a better name for it? (And shouldn’t it be an object?) If I can think of a name I can probably use that to think about the object.
I think it’s a VerbTranslator, from verb to imperative. (It’s really returning a partial imperative. Maybe we need to know that some verbs are transitive and some intransitive3?) That thought’s a bit premature, but lets do the rename and then see if an object drops out.
class ImperativeFactory(val verbTranslator:Map<String,Imperative>, val synonyms:Map<String,String> = synMap) {
fun create(verb:String): Imperative = imperative(verb)
fun create(verb:String, noun:String) = imperative(verb).setNoun(synonym(noun))
private fun imperative(verb: String) = verbTranslator.getValue(synonym(verb))
private fun synonym(verb: String) = synonyms.getValue(verb)
}
Now I want an object, VerbTranslator, that takes a map and has a method, oh, translate
returning an Imperative.
Can IDEA help me? I could just type this in, it’s pretty easy, but I’ll see if IDEA has any help.
I can’t see anything. I’ll just quickly TDD it into submission. I think this might do:
@Test
fun `verbTranslator`() {
val vt = VerbTranslator(impTable)
val imp = vt.translate("e")
assertThat(imp.verb).isEqualTo("go")
}
And …
class VerbTranslator(val map:Map<String,Imperative>) {
fun translate(verb:String): Imperative {
return map.getValue(verb)
}
}
I expect green. I’m not but it’s a weakness in my tiny starting table. Change the test:
fun `verbTranslator`() {
val vt = VerbTranslator(impTable)
val imp = vt.translate("east")
assertThat(imp.verb).isEqualTo("go")
}
Green. Expand the test:
@Test
fun `verbTranslator`() {
val vt = VerbTranslator(impTable)
val imp = vt.translate("east")
assertThat(imp).isEqualTo(Imperative("go", "east"))
}
Tests are green. Commit: new VerbTranslator object.
Now the purpose of this is just to make a place for more reasonable method names and more powerful methods o our new object. Let’s change our factory to expect a VerbTranslator:
class ImperativeFactory(val verbTranslator:VerbTranslator, val synonyms:Map<String,String> = synMap) {
And of course now we have to supply one in our tests. A few quick substitutions and I’m green. The substitutions all look like this one:
@Test
fun `two word legal command`() {
val imperatives = ImperativeFactory(VerbTranslator(impTable))
var imp = imperatives.create("go", "e")
assertThat(imp.act()).isEqualTo("went east")
}
IDEA offered me the change to create an alternate constructor. I don’t know how to do that, so I’m going to change one of those calls back and see what it does.
This test now works:
@Test
fun `two word legal command`() {
val imperatives = ImperativeFactory(impTable)
var imp = imperatives.create("go", "e")
assertThat(imp.act()).isEqualTo("went east")
}
Because we have this IDEA-built constructor:
class ImperativeFactory(val verbTranslator:VerbTranslator, val synonyms:Map<String,String> = synMap) {
constructor(map: Map<String, Imperative>) : this(VerbTranslator(map))
fun create(verb:String): Imperative = imperative(verb)
fun create(verb:String, noun:String) = imperative(verb).setNoun(synonym(noun))
private fun imperative(verb: String) = verbTranslator.translate(synonym(verb))
private fun synonym(verb: String) = synonyms.getValue(verb)
}
We’re green. Commit: alternate constructor for ImperativeFactory casts map to VerbTranslator.
Given this style, don’t you think we should cover the synonym table with a Synonyms object instead of using the map directly? Let’s go for it. This time I’m just creating it, intending to plug it in to the tests, at which point we’ll see that it works (or that it doesn’t).
class Synonyms(val map: Map<String,String>) {
fun synonym(word:String) = map.getValue(word)
}
So far I think we’re only using the default synonyms table in our factory.
We want this to be:
class ImperativeFactory(val verbTranslator:VerbTranslator, val synonyms:Synonyms = Synonyms(synMap)) {
And this should be fine except for this method:
private fun synonym(verb: String) = synonyms.getValue(verb)
That needs to be:
private fun synonym(verb: String) = synonyms.synonym(verb)
I think I should be green again. I am. Commit: Synonyms object covers synonym table.
This has gone on a long time, and if you’re like me, even though the steps all seemed to make sense, you’re wondering what all this has done to the big picture. Let me tidy the file and we’ll take a look.
All the Code
data class Imperative(val verb: String, val noun: String) {
fun act():String {
val action:(Imperative) -> String = when(verb) {
"go" -> { i -> "went $noun" }
else -> { i -> "I can't $verb a $noun"}
}
return action(this)
}
fun setNoun(noun: String): Imperative {
return Imperative(verb,noun)
}
}
class ImperativeFactory(val verbTranslator:VerbTranslator, val synonyms:Synonyms = Synonyms(synonymTable)) {
constructor(map: Map<String, Imperative>) : this(VerbTranslator(map))
fun create(verb:String): Imperative = imperative(verb)
fun create(verb:String, noun:String) = imperative(verb).setNoun(synonym(noun))
private fun imperative(verb: String) = verbTranslator.translate(synonym(verb))
private fun synonym(verb: String) = synonyms.synonym(verb)
}
class VerbTranslator(val map:Map<String,Imperative>) {
fun translate(verb:String): Imperative = map.getValue(verb)
}
class Synonyms(val map: Map<String,String>) {
fun synonym(word:String) = map.getValue(word)
}
These classes are all tiny and are almost entirely driven by the data in the tables shown below. The exception is the Imperative itself, and we can see the table taking form already, in the when
clause. We’ll probably have another object, maybe Actions, to contain those.
We’ve followed a convention of covering even the elementary tables with classes, which gives us better names, like translate
instead of the common and uncommunicative getValue
.
I’m starting to like what we have here. I’m not sure if it is much like what I was originally thinking. You can see the point, way up above, where I suddenly flipped from early-morning fantasy about code to actual code. After that, I wasn’t guided so much by a vision as by individual steps that seemed sensible in themselves. Then I just kept pushing for more data focus, with meaningful methods provided by covering all the tables with classes.
So far, I think it’s going nicely. I’ll be interested to hear, if anyone gets through this, what you think.
All the Data
// language control tables. (prototypes)
val synonymTable = mapOf("e" to "east", "n" to "north").withDefault { it }
val imperativeTable = mapOf(
"east" to Imperative("go","east"),
"go" to Imperative("go", "irrelevant")
).withDefault { (Imperative("none", "none"))
}
All the Tests
class ImperativeSpike {
@Test
fun `create Imperative`() {
val imp = Imperative("go", "east")
assertThat(imp.verb).isEqualTo("go")
assertThat(imp.noun).isEqualTo("east")
}
@Test
fun `look up some imperatives`() {
val imperatives = ImperativeFactory(VerbTranslator(imperativeTable))
var imp: Imperative = imperatives.create("east")
assertThat(imp).isEqualTo(Imperative("go","east"))
imp = imperatives.create("e")
assertThat(imp).isEqualTo(Imperative("go","east"))
}
@Test
fun `imperative can act`() {
val imperatives = ImperativeFactory(VerbTranslator(imperativeTable))
var imp: Imperative = imperatives.create("east")
assertThat(imp.act()).isEqualTo("went east")
imp = imperatives.create("e")
assertThat(imp.act()).isEqualTo("went east")
imp = Imperative("forge", "sword")
assertThat(imp.act()).isEqualTo("I can't forge a sword")
}
@Test
fun `two word legal command`() {
val imperatives = ImperativeFactory(imperativeTable)
var imp = imperatives.create("go", "e")
assertThat(imp.act()).isEqualTo("went east")
}
@Test
fun `verbTranslator`() {
val vt = VerbTranslator(imperativeTable)
val imp = vt.translate("east")
assertThat(imp).isEqualTo(Imperative("go", "east"))
}
}
Summary
This was a long session, it’s just shy of 1300 hours and I started at 0722, so I’m about 5 1/2 hours in. I’ve got 90 lines of new tests and code, and the code seems to me to be quite nice, consisting almost solely of one-line methods.
At least three times along the way, I was moved to stop, but saw one more tiny step that I could take to improve things a bit more. And each time, I gained momentum and took several steps, making things generally smaller and better. Over the course of the time, I made 11 commits, about 2 per hour, a little slower than one might like but I’m not as quick with IDEA and Kotlin as I might be.
I learned a few IDEA tricks along the way and tried to use keystrokes where I could remember, or look them up and then use them, to help me start to remember. I’m getting the hang of where to put the cursor so that IDEA’s ideas will apply to the situation.
Next time, probably tomorrow given the time but you never know do you, I’ll push the action when
clause into a table-covering object and see about mating this idea up to the existing command parsing in the real game. I’ll try to do that with tests against the new code, but at some point, the game’s requirement for access to world and room and such may require a bit of a leap.
Let’s hope not. My feeling at the moment is that I may be able to get the leap down to a tiny jump, or maybe all the way to just another small step.
And I’m going to try to make and keep a new resolution:
Never, ever, use a native collection directly, even “just for a moment to save time”. Always, always, create an object of your own, with a sensible name and sensible get (and put) methods of its own.
We’ll see. I hope you’ll follow along.