I’m psyched1 after two consecutive sessions with some success and no big troubles. What could possibly go wrong?

Last night, Tuesday, at the meeting of the Friday Night Coding Zoom Ensemble, Bryan had a game spike running under libGDX, including a little avatar moving around on a floor populated by various elements. The game didn’t do anything, but it looked very nice. He had some advantages, including that he had a pile of assets, he had a libGDX build, and he moves rapidly. I wasn’t jealous, honestly. Really. Not a bit. Well, sort of.

Fact is, we go as fast as we can go, or too fast. We rarely if ever go much slower than we are capable of. Me, I’m new to this IDE, new to this language, and, for all I know, Bryan and Hill are better programmers than I am. Someone has to be, why not them?

That said, Hill had hosed his entire effort and reverted it, so he had nothing to show. Take that, Hill.

As you know, if you’ve been following along, I have very little and yet I have some good stuff. I have a rudimentary structure for a Dungeon Specification Language (DSL), with tests. I have the ability to dispatch commands. I have a Game with a World with Rooms, and I have each room knowing its connections to other rooms, via somewhat magical terms like “n” and “s”. And it’s all under test, and written up in articles. Take that, Bryan.

Seriously, though, while I like to take inspiration wherever I can find it, I’ve been doing this long enough to know that my pace is what it is. I do feel badly in this new system and language, because I’m clumsy and don’t yet reach reflexively for the right keystroke or menu item. I feel frustrated and afraid that if I step in just the wrong spot, my whole build / Gradle framework will come crumbling down around me, and I won’t have any idea how to fix it.

But overall, with two sessions very successful (and the preceding one ending well after hours of horror), I feel that I’m in a good place and moving appropriately. One nice thing about not knowing much is that I am more careful than usual to take small steps, and that’s a very good thing. It’s very easy to make too sweeping a change and break things. It’s not easy at all to take steps that are too small: in fact, I don’t know how to do that in any sensible way.

I suppose you could write x = a + instead of x = a + b, and that would be too small. But no one’s going to do that, I hope.

Anyway I was handed some ideas and I’m generally psyched (q.v.), so let’s get to it.

init

Hill objects to the use of the name init for my function parameter, because init is a keyword and it squicks him out to see it used as I’m using it. That’s in code like this:

    fun room(name: String, init: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.init()
        return room
    }

He suggested details and I’m good with that. Let’s see if IDEA will help me with this. Yes. I can click in the word, type Shift F6 (obviously) and I’m renaming. Type “details”, enter, and voila!

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.details()
        return room
    }

Nice. I think there are a few other occurrences of the same thing. Kotlin has a search thing that might be helpful. Urrgh! Whatever that thing is, it’s not useful for finding “init” in my program. I see why Hill might want me not to use it, it does occur lots of places. How do I search in my own program? OK, there’s a Find. It doesn’t find any either. I was sure there was one. In that world function, I thought. Yes, here it is:

fun world(init: World.()->Unit): World{
    val world = World()
    world.init()
    return world
}

I should move that elsewhere anyway, it’s in the test file now. But why didn’t it find it? OK, Find in files, maybe. Anyway …

fun world(details: World.()->Unit): World{
    val world = World()
    world.details()
    return world
}

Test. Green. Move the function to the World.kt. Still green. Commit: rename init functions to details. Move world function to World.

So, that’s nice. What else did we do last night? Well, Ken suggested that my DSL world go suggests action to him and needs a better word. Hill suggested that go is an action and seems to want not to differentiate one kind of action from another. And everyone agreed, including YT, that my rooms collection should be an object of my own invention, not a naked collection. Let’s review the World class: it’s small:

class World {
    val name = "world"
    private val rooms = mutableMapOf<String,Room>()

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.details()
        return room
    }

    val roomCount get() = rooms.size

    fun hasRoomNamed(name: String): Boolean {
        return rooms.containsKey(name)
    }

    fun roomNamed(name:String) :Room {
        return rooms[name]!!
    }
}

I think we’ll TDD up a bit of a Rooms class next, but first I want to get clear about what should happen. There are two kinds of references to rooms, by name. When we define a room, we provide its name. But when we specify a room connection with go, we refer to a room name … which might not exist yet … or ever.

        val world = world {
            room("woods") {
                go("s","clearing")
                go("xyzzy","Y2")
            }
            room("clearing") {
                go("n", "woods")
            }
            room("Y2") {
                go("xyzzy","woods")
            }
        }

If I were to decide that south of Y2 we could find Y3, and say

go("s", "Y3")

We’d have a problem. I’m not at all clear just what would happen. Let’s find out, I’ll enhance that test.

        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("Y3")

Test, holding breath. Somewhat surprisingly, the test passes. Presumably we are now in room Y3. This cannot be a good thing, there’s surely no way out. But how did that even happen? Let’s check the code:

    fun command(cmd: String) {
        when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            "xyzzy" -> move("xyzzy")
            else -> {println("unknown cmd $cmd")}
        }
    }

    fun move(dir:String) {
        currentRoomName = currentRoom().moves.firstOrNull{it.first==dir}?.second ?:currentRoomName
    }

Ah. We successfully set the room name. The next move might surprise us. I’ll move again.

        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("Y3")
        game.command("s")

And when I run, I get a fail:

java.lang.NullPointerException
	at com.ronjeffries.adventureFour@1.0-SNAPSHOT/com.ronjeffries.adventureFour.World.roomNamed(World.kt:27)

Right, I bet that’s my “!!” line:

    fun roomNamed(name:String) :Room {
        return rooms[name]!!
    }

Right. No such room. Now we have enough information to plan.

I think in the fullness of time, we would like for the go("S","Y3") to create a room just as room("y3") would, but that the room should be somehow identified as uninitialized. And when we are finished with the DSL defining a world, we should have a verification stage that checks for rooms that have never been truly defined, that is, for which no room ever happened.

Alternatively—it’s good to have options—we might build a symbol table of room references and check them against defined rooms.

Another change comes to mind. (Yes, this is chaotic. The world is young and full of chaos.) I think I’d like for the Game class to have a room instance. Presently it has the name:

class Game(val world: World, startingName: String) {
    var currentRoomName = startingName

This brings me to another troubling issue, one that I seem always to encounter in these games. Observe this DSL code:

        val world = world {
            room("woods") {
                go("s","clearing")
                go("xyzzy","Y2")
            }
        }

The function room is a method on World, returning a Room instance, and go is a method on Room. Suppose that we did want go to create a Room if it didn’t exist. How could it even do that? It has no access to World, where the rooms collection exists. Well, when we create the Room, which is done in World:

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.details()
        return room
    }

We could pass the World instance, this down to the Room constructor. Then any Room method, like go, could reference World methods. Maybe we would make that a general rule, that any nested object is given its parent. I don’t usually like object connections that go both ways, but it would certainly be convenient.

We have no current need for this solution, but we do have a problem, the possibility that a go command may reference a room that doesn’t exist. One important question is what should happen if the Game encounters this situation. It’s all well and good to imagine that the verification phase, which doesn’t even exist yet, will handle this, but we need to be sure that the game won’t crash. Let’s fix that.

We have the crash in our test, and the code that fires the NPE is this:

    fun roomNamed(name:String) :Room {
        return rooms[name]!!
    }

Let’s send you back to the starting room if you ever wind up somewhere impossible. Can’t do that directly, because the World instance doesn’t know which room is the starting room. We gave that info to Game.

I feel that I’m a maze of twisty little passages all alike. Whichever way I turn, I’m stymied and confused.

Cortical-Thalamic Pause
I’m thinking about too many things at once. If I were someone else, perhaps I could do that, but then I wouldn’t live in this perfect house with my perfect wife, perfect car, and, it goes without saying, perfect cat. I need to settle down and do things one at a time.

I think I’m afraid that I’ll make a change and then have to reverse it out. My lack of comfort with IDEA and Kotlin makes me a bit fearful of making a bad decision. Generally, I don’t worry about that much. Here, I feel the need to be more careful, and since being careful is a good thing, I’ll go with the feeling.

Let’s start by looking at Game and thinking about what it wants. Game looks like this:

class Game(val world: World, startingName: String) {
    var currentRoomName = startingName

    fun command(cmd: String) {
        when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            "xyzzy" -> move("xyzzy")
            else -> {println("unknown cmd $cmd")}
        }
    }

    fun move(dir:String) {
        currentRoomName = currentRoom().moves.firstOrNull{it.first==dir}?.second ?:currentRoomName
    }

    fun currentRoom(): Room {
        return world.roomNamed(currentRoomName)
    }
}

Now if we look at this carefully at all, we can see that while Game currently knows the current room name, it really wanted the current room, because it immediately fetches it from the world. So let’s convert Game to have the room, not the name.

I somewhat suspect that a test may be inquiring about the name. We’ll deal with that when it happens.

class Game(val world: World, startingName: String) {
    var currentRoom = world.roomNamed(startingName)

    fun command(cmd: String) {
        when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            "xyzzy" -> move("xyzzy")
            else -> {println("unknown cmd $cmd")}
        }
    }

    fun move(dir:String) {
        val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
        currentRoom = world.roomNamed(name)
    }
}

That seems nearly good. Is that expression just too long? Yes, but it was fun to fold it up like that. Let’s see if this works. Test.

I get lots of errors saying that currentRoomName isn’t good, such as here:

        assertThat(game.currentRoomName).isEqualTo("woods")

Let’s make the property:

class Game(val world: World, startingName: String) {
    var currentRoom = world.roomNamed(startingName)
    val currentRoomName get() = currentRoom.name

That gets me compiled and running, with this error:

Expecting:
 <"Y2">
to be equal to:
 <"Y3">
but was not.

That’s this:

        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("Y3")
        game.command("s")

We didn’t go to Y3, which is a good thing. There is no south exit in Y2:

            room("Y2") {
                go("xyzzy","woods")
            }

I mean for there to be an illegal one:

            room("Y2") {
                go("xyzzy","woods")
                go("s", "Y3")
            }

Test again. I don’t know quite what to expect. Could read the code and figure it out but why not ask the computer?

Ah, good, we have the NPE bacK:

java.lang.NullPointerException
	at com.ronjeffries.adventureFour@1.0-SNAPSHOT/com.ronjeffries.adventureFour.World.roomNamed(World.kt:27)

And that code is the same as it ever was:

    fun roomNamed(name:String) :Room {
        return rooms[name]!!
    }

What should we return? We don’t know. We’re called from here:

    fun move(dir:String) {
        val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
        currentRoom = world.roomNamed(name)
    }

This guy could tell us the default he wants if there is no such room as he names. We could allow a null to come back and check it, or we could pass in currentRoom and have it come back if there is no such name.

I’m starting to wish the go commands were linked to rooms by now, but I think deferring the linkage is generally better, so I won’t move that way unless I have to. We’ll pass the default.

I decided to make a new method, because I don’t have a default to pass in the World creation. So:

class World {
    val name = "world"
    private val rooms = mutableMapOf<String,Room>()

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.details()
        return room
    }

    val roomCount get() = rooms.size

    fun hasRoomNamed(name: String): Boolean {
        return rooms.containsKey(name)
    }

    fun roomNamedOrDefault(name: String, default: Room) :Room {
        return rooms.getOrDefault(name, default)
    }

    fun roomNamed(name: String): Room {
        return rooms[name]!!
    }
}

Now we return the current room if we can’t find the room, just as we would if we can’t find the direction in our go table. So the test needs to change:

    @Test
    internal fun gameCheck() {
        val world = world {
            room("woods") {
                go("s","clearing")
                go("xyzzy","Y2")
            }
            room("clearing") {
                go("n", "woods")
            }
            room("Y2") {
                go("xyzzy","woods")
                go("s", "Y3")
            }
        }
        val game = Game(world, "woods")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("clearing")
        game.command("n")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("s") // points to Y3 erroneously
        assertThat(game.currentRoomName).isEqualTo("Y2")
        // mp such room as Y3, defaults to stay in Y2
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("woods")
    }
}

Test runs. Green. Commit: Game keeps current Room, not current Name. Derives name if needed.

Now what? Oh. Let’s think about a Rooms object with more reasonable behavior. And should we have a startAt command in World DSL? I don’t know.

Let’s TDD a Rooms. It should act like a map, with a put and get, and should support defaults, perhaps a built-in one, and one passed in in the way we are now.

I think I’m going to just elaborate a single test until it tells the story of Rooms. I start:

    @Test
    fun roomsCanAddAndGet() {
        val rooms = Rooms()
    }

This demands a Rooms class:

class Rooms {
    private val rooms = mutableMapOf<String,Room>()
}

I’ve already gone beyond my remit but I like to type in my thoughts when I have them. Now the test needs to test something.

    @Test
    fun roomsCanAddAndGet() {
        val rooms = Rooms()
        val room = Room("abc")
        rooms.add(room)
        val found = rooms.getOrDefault("abc", Room("zzz"))
        assertThat(found).isEqualTo(room)
    }

That is red all over, so I need:

class Rooms {
    private val rooms = mutableMapOf<String,Room>()

    fun add(room: Room) {
        rooms.put(room.name, room)
    }

    fun getOrDefault(name: String, default: Room): Room {
        return rooms.getOrDefault(name, default)
    }
}

This seems to work a treat. The plan, of course, is to use it in World, which presently thinks it has a map. Let’s just plug in Rooms and see what happens. First, though, let’s commit. We are green. Commit: Rooms class with initial tests.

Now plug in:

class World {
    val name = "world"
    private val rooms = Rooms()

This is underlined with red squiggles:

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms[name] = room
        room.details()
        return room
    }

Before I fix that, I make a yellow sticky: Rooms second add of same name?? That’ll remind me of the issue. Change this code:

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms.add(room)
        room.details()
        return room
    }

This is squiggled:

    val roomCount get() = rooms.size

Rooms needs to know size.

class Rooms {
    private val rooms = mutableMapOf<String,Room>()
    val size get() = rooms.size

What else is squiggled?

    fun hasRoomNamed(name: String): Boolean {
        return rooms.containsKey(name)
    }

We need that method.

    fun containsKey(name: String): Boolean {
        return rooms.containsKey(name)
    }

We might be nearly good. I’ll test and see. No, missed this:

    fun roomNamed(name: String): Room {
        return rooms[name]!!
    }

We have the default issue here. For now … let’s first defer to Rooms class:

    fun roomNamed(name: String): Room {
        return rooms.roomNamed(name)
    }

And then there, maybe leave the !! solution in place? I hate that but don’t as yet have a better idea.

    fun roomNamed(name: String): Room {
        return rooms[name]!!
    }

Test. All green. Commit: World uses new Rooms class rather than a map.

You may be wondering what has been gained here. We used to have a map in World, now we have a Rooms, and the Rooms is pretty much just a cover for a map. Let me clearly state that the main reason I did this is because I have found it generally better to have my own collections rather than system native ones, so this is done speculatively. I did it now because it will only get harder to do if I put it off. And, frankly, I wanted an easy refactoring exercise as part of gaining facility with Kotlin and IDEA.

Now let’s reflect, review, and regroup.

Re(flect, view, group)

We did a simple change of init to details. Then some additional tests to build understanding, and finally a TDD’s Rooms class to replace the naked map in World. Not much actual work, but that’s fine, I’m trying to fill my mind with understanding and ideas.

We have two yellow sticky “cards” on the desktop:

  • Rooms second add of same name??
  • Missing name in roomNamed NPE

That second one is left over from last night’s discussion. We still have that problem, pushed down to Rooms class:

    fun roomNamed(name: String): Room {
        return rooms[name]!!
    }

What should happen if we do get a reference to a room name that doesn’t exist? It can now only happen here:

class Game(val world: World, startingName: String) {
    var currentRoom = world.roomNamed(startingName)

That’s the only reference to the method. Honestly, I think it’s just too soon to say quite what should happen. Let’s rename the method to unsafeRoomNamed. Test. Green. Commit: Rename unsafe roomNamed to unsafeRoomName throughout.

It would be nice to have some new feature to crow about. But what? I don’t know. How about if a Room could return its room references, the names of all the rooms it, well, references. That’s speculative but clearly plays into validation and verification.

We don’t even have any Room tests per se, we have tests at the world and game level. But we can see that if we are to do it at all, we’ll have to do it in Rooms, so let’s improve our test for game.

        val refs = game.roomReferences
        assertThat(refs).contains("Y3")

This method just pushes right on down:

class Game(val world: World, startingName: String) {
    val roomReferences: Set<String> get() = world.roomReferences

class World {
    val roomReferences: Set<String> get() = Rooms.roomReferences

Arrgh, it wants me to do a companion object for some reason. Oh, the upper case. I meant:

class World {
    val roomReferences: Set<String> get() = rooms.roomReferences

And now it’ll let me write a method.

class Rooms {
    val roomReferences: Set<String> get() {
        val result = mutableSetOf<String>()
        for (room in rooms.values ) {
            result += room.roomReferences
        }
        return result
    }

I think there is a better way to do this, but this ought to do what I need. Now:

class Room(val name: String) {
    val roomReferences: Set<String> get () {
        return moves.map { it.second}.toSet()
    }

Now enhance the test:

class GameTest {
    @Test
    internal fun gameCheck() {
        val world = world {
            room("woods") {
                go("s","clearing")
                go("xyzzy","Y2")
            }
            room("clearing") {
                go("n", "woods")
            }
            room("Y2") {
                go("xyzzy","woods")
                go("s", "Y3")
            }
        }
        val game = Game(world, "woods")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("s")
        assertThat(game.currentRoomName).isEqualTo("clearing")
        game.command("n")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("woods")
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("Y2")
        game.command("s") // points to Y3 erroneously
        assertThat(game.currentRoomName).isEqualTo("Y2")
        // mp such room as Y3, defaults to stay in Y2
        game.command("xyzzy")
        assertThat(game.currentRoomName).isEqualTo("woods")
        val refs = game.roomReferences
        assertThat(refs).contains("Y3")
        val expected = setOf("Y3", "Y2", "clearing", "woods")
        assertThat(refs).isEqualTo(expected)
    }
}

Long test, but it tells the story, and it is, after all, a game test. And we get the correct set of references at the end. Nice.

It’s 1130, I need a short break.

What Now?

Let’s look at that for thing and see if we can do better.

    val roomReferences: Set<String> get() {
        val result = mutableSetOf<String>()
        for (room in rooms.values ) {
            result += room.roomReferences
        }
        return result
    }

This may or may not be better:

    val roomReferences: Set<String> get() {
        val result = mutableSetOf<String>()
        rooms.forEach { _, room -> result += room.roomReferences }
        return result
    }

I don’t see a good way to do it all in one swell foop. I’ll ask my betters. If you’re out there, message me or something.

Meanwhile: Commit:

One more little thing. Kotlin has placed a lot of these methods differently from how I would. I’m going to tidy these classes. I’ll list them in the appendix below, after the summary. Let’s sum up.

Summary

Subject to the tidying below, which is done, we’ve cleaned up the code a fair amount. We still have that one !! to worry about, and a sticky to remind us. And a sticky about adding rooms of the same name more than once. I think what we want is that a second reference to the same room name should be the same room, which would permit you to reopen a room and add things to it. There’s a risk, though, which is that you might have thought you were creating a new room.

Maybe we need two commands, room and reopenRoom or something. Anyway we have a stick for that, so we can think about it in due time.

Kotlin has extends or some word like that. Maybe we need the same. Whatever we decide needs more brain power than I have at my disposal just now.

We have that nice new Rooms class covering the collection of Rooms, so we can make it behave however we see fit.

I just made a new sticky: Should Room know parent ( world)? Something to consider, but we still have no need of it just now.

Another sticky, “Validation”, clearly needed.

According to the History thing, we have ten commits this morning, not bad. The History thing looks very powerful, I’ll have to make a point of exploring it.

Anyway, a good morning and only one significant stumble there when I messed up a tidying. I think I cut something and pasted it into the middle of something else. Maybe there’s a better way to move chunks of code around in IDEA?

I’m cruising through yellow light bulbs. IDEA suggests this change:

    fun add(room: Room) {
        rooms[room.name] = room
    }

I had a put there. Test. Commit: Accept IDEA advice. It finds a “useless trailing comma”. Test, Commit: Ditto.

It suggested this rather than Pair(…):

    fun go(direction: String, roomName: String) {
        moves += direction to roomName
    }

Idiomatic. Might as well. Test, Commit ditto.

I guess it’s happy. I’m happy. Let’s get out of here. See you next time!


Appendix: Tidying Results

class World {
    val name = "world"
    private val rooms = Rooms()

    val roomCount get() = rooms.size
    val roomReferences: Set<String> get() = rooms.roomReferences

    fun room(name: String, details: Room.()->Unit) : Room {
        val room = Room(name)
        rooms.add(room)
        room.details()
        return room
    }


    fun hasRoomNamed(name: String): Boolean {
        return rooms.containsKey(name)
    }

    fun roomNamedOrDefault(name: String, default: Room) :Room {
        return rooms.getOrDefault(name, default)
    }

    fun unsafeRoomNamed(name: String): Room {
        return rooms.unsafeRoomNamed(name)
    }
}

I like my properties at the beginning, in an order that has the things referenced ahead of the references. Test, commit: Tidy World.

class Game(val world: World, startingName: String) {
    var currentRoom = world.unsafeRoomNamed(startingName)
    val currentRoomName get() = currentRoom.name
    val roomReferences: Set<String> get() = world.roomReferences

    fun command(cmd: String) {
        when(cmd) {
            "s" -> move("s")
            "n" -> move("n")
            "xyzzy" -> move("xyzzy")
            else -> {println("unknown cmd $cmd")}
        }
    }

    fun move(dir:String) {
        val name = currentRoom.moves.firstOrNull{it.first==dir}?.second ?:currentRoom.name
        currentRoom = world.roomNamedOrDefault(name,currentRoom)
    }
}

Test. Commit: Tidy Game.

class Room(val name: String) {
    val moves = mutableListOf<Pair<String,String>>()

    val roomReferences: Set<String> get () {
        return moves.map { it.second}.toSet()
    }
    fun go(direction: String, roomName: String) {
        moves += Pair(direction, roomName)
    }

    fun move(direction: String) :String {
        return moves.first { it.first == direction}.second
    }
}

Test, commit: Tidy Room.

class Rooms {
    private val rooms = mutableMapOf<String,Room>()
    val size get() = rooms.size

    val roomReferences: Set<String> get() {
        val result = mutableSetOf<String>()
        rooms.forEach { _, room -> result += room.roomReferences }
        return result
    }

    fun add(room: Room) {
        rooms.put(room.name, room)
    }

    fun getOrDefault(name: String, default: Room): Room {
        return rooms.getOrDefault(name, default)
    }

    fun containsKey(name: String): Boolean {
        return rooms.containsKey(name)
    }

    fun unsafeRoomNamed(name: String): Room {
        return rooms[name]!!
    }
}

Test, commit: Tidy Rooms. Tidying done.

When I first started tidying here, I did something that broke the tests. After a bit of flailing, I just reverted and did the tidying one class / file at a time. Much better. Strongly encouraged.

Added in post:

    val roomReferences: Set<String> get() {
        return (rooms.flatMap { it.value.roomReferences }).toSet()
    }

That replaces this:

    val roomReferences: Set<String> get() {
        val result = mutableSetOf<String>()
        rooms.forEach { _, room -> result += room.roomReferences }
        return result
    }

Arguably better. Test, commit: Used flatmap in roomReferences.



  1. P.S. When you fake someone out, the correct exclamation is “Psych!”, not “Sike!”. The latter isn’t trendy or cute: it’s just wrong.