Kotlin 90 - Just Work
Converting the enums to Kotlin’s preferred rules. Published for the record, whatever that means. Probably means, scan or don’t read at all. Nothing to see here, move on …
The following doesn’t work out …
I really like having the room names in the R enum
, but I’d like for the tests to have their own enum, so that test room names can be unique to testing, yet do not show up in the game’s list. Today I want to see whether I can perhaps provide a top level interface for the rooms in the various methods and structures that accept R now, and separate implementations of that interface, probably private, in the tests and game.
This is more an experiment in what Kotlin can do than anything else: I can certainly just use game-level room names in the tests. Were it not for the experience of what I’m about to try, that’s what I’d do.
Let’s get started.
That didn’t take long. I tried this:
interface RR {
val name:String
}
enum class R: RR {
spring, wellhouse, woods, `woods toward cave`, `cave entrance`, first, second, palace, treasure
, clearing, Y2}
This compiles and runs, but to add a new go
, you have to type R.roomname
rather than just roomname
. I’m not willing to pay that price for a convenience in testing. I’ll use names from the game in the tests.
If some wise reader knows a way to provide two different enums as parameter type for a function, and to get the prompting that one would get with just a standard enum type, let me know. For now, I’ll change the tests.
So we go another way …
A small matter of name substitution and we’re good. However, I do notice a complaint from IDEA and Kotlin. They want my enum names to begin with an upper case character. I guess to avoid a hundred warnings, I can accommodate that notion. I’ll rename all of them and see what breaks. I suspect that something just might.
In the course of that, I find that Kotlin also dislikes my enum names that contain spaces. Curiously, they work just fine, as do the lower case names. It’s a style guide thing. Also, while the message suggests that I can use underbar in the names, it doesn’t really like them.
I’m going to go along with these rules, but I’m not sure I’m going to be happy. With luck, all I have to do is type an upper-case first character to get my prompts.
And get in trouble that’s not worth detailing …
Trouble. Too many changes the way I did it. Rolling back. I’m going to start again, smaller, and I’m going to leave the names for the room tests in the list for now.
Again
I’ve renamed all the R enum to be legal:
enum class R {
Spring, Wellhouse, Woods, WoodsNearCave, CaveEntrance,
First, Second, Palace, Treasure, Clearing, Y2
}
I think in order to do this right, we need to change all the things that think a room name is a string to understand that now they are Rs. First test to see what breaks.
@Test
fun roomsWithGo() {
val world = world {
room("woods") {
go(D.south,R.Clearing)
}
room("clearing") {
go(D.north, R.Woods)
}
}
assertThat(world.roomCount).isEqualTo(2)
assert(world.hasRoomNamed("clearing"))
val player = Player(world, "clearing")
player.command("go n")
assertThat(world.response.nextRoomName).isEqualTo("woods")
}
The room
function, up there at line 4, accepts a String. We should make it require an R.
fun room(name: String, details: Room.()->Unit) : Room {
val room = Room(name)
rooms.add(room)
room.details()
return room
}
I’m just going to change the signature here and deal with the explosion of errors.
The explosion was substantial. It took me perhaps half an hour to change all the references and to update a number of the data structures to expect R instead of String. These are the hazards of using unwrapped native objects. Even a typealias might have helped. All the changes to the actual code amounted to changing “foobar” to R.foobar
.
We’re green and the game plays properly. Commit: Converted to Room enum R throughout. Enums match Kotlin desires for format.
I see no real point to showing you all the changes. A typical test looks like this now:
@Test
fun `room go has optional block`() {
val myWorld = world {
room(R.First) {
desc("first room", "the long first room")
go(D.north, R.Second) { true }
go(D.south,R.Second) {
it.say("The grate is closed!")
false
}
}
room(R.Second){}
}
val myRoom = myWorld.unsafeRoomNamed(R.First)
val secondRoom = myWorld.unsafeRoomNamed(R.Second)
val cmd = Command("s")
val resp1 = myWorld.command(cmd, myRoom)
assertThat(resp1.nextRoomName).isEqualTo(R.First)
assertThat(resp1.sayings).isEqualTo("The grate is closed!\n")
val resp2 = myWorld.command(Command("s"), secondRoom)
assertThat(resp2.nextRoomName).isEqualTo(R.Second)
}
I think I prefer the R notation, since it is less prone to typographic errors.
Changing the directions, D.north
and so on to be legal enums would seem to be the next step. D.North
and such. I think that requires a recasting of the lexicon to get to the right answers.
In for a penny, I’ll change one and see what breaks. South is used a lot. When I change that to D.South
, I get errors saying
No enum constant com.ronjeffries.adventureFour.D.south
It’s coming from here:
val (targetName, allowed)
= moves.getValue(D.valueOf(imperative.noun))
So what’s happening here is that we’re looking into the moves table, searching for a direction named D.south, because the command translation produces all lower case elements.
I want a case-independent valueOf
equivalent. I think we can add a method to D somehow.
enum class D {
north, South, east, west,
northwest, southwest,northeast,southeast,
up, down, xyzzy;
companion object {
fun valueMatching(desired: String): D {
return values().filter {it.name.lowercase() == desired}[0]
}
}
}
I found a page that suggested that I could implement valueMatching
as a plain method on D, but that didn’t work. Conveniently, IDEA offered to create the companion object version, which I filled in as shown. D.South
now works. I’ll rename the others, one at a time.
Commit: All D converted to leading upper case character.
Reflection
It’s rather a bit of a hack to have done this:
enum class D {
North, South, East, West,
Northwest, Southwest,Northeast,Southeast,
Up, Down, XYZZY;
companion object {
fun valueMatching(desired: String): D {
return values().filter {it.name.lowercase() == desired}[0]
}
}
}
This is a sort of “weak” valueOf
, that will find a suitable enum value. Which reminds me, there is a better way to do this:
companion object {
fun valueMatching(desired: String): D {
return values().filter {it.name.equals(desired, ignoreCase = true)}[0]
}
}
Test, commit use ignoreCase in D.valueMatching.
As I was saying, this is a bit of a hack, allowing the command parsing to deal with things in lower case, which it wants to do. But that has great value, given that the user can type in whatever case they want, and the lexicon and translation code are set up to drive things to lower case.
I am also concerned that even with my new valueMatching
function, we could fail to find a direction and if we did, we’d get a runtime error. Yes. If I type “go blue”, we look for a direction “blue” and it isn’t there, and we get an exception. Don’t type “go blue” until we figure out a fix.
Overall, I think these changes have been worthwhile. In game definition, we get the advantage of pre-declared room and direction names, while retaining the ability of the user to type in any case. And … we avoided rewriting the Lexicon and Verbs and Actions tables to accommodate the CamelCase requirement on enum names.
Well, I have to get ready to go get a Covid jab, so we’ll wrap this here. Little of consequence from a learning viewpoint, just work that needed to be done.
See you next time!