Let’s push the Spike in a little deeper. I’ve got some ideas for how it could be quite nice. Good ideas? We’ll find out.

I have a small idea, and a big one. The small one is that the various methods on my Status objects could be properties rather than methods, eliminating the need for extra parens. That should be easy. The larger idea is that I might be able to use a strategy kind of idea to create a status object that morphs to an IntStatus or BooleanStatus depending on what kind property is first used. It could happen. Good idea? I honestly don't know, and won't, until I try it.

Here goes.

Small Idea: Properties

I’ll enhance my test a bit, changing it to expect properties, and then make it run. We begin with this:

class StatusTest {
    @Test
    fun `status isTrue`() {
        val status = BooleanStatus()
        assertThat(status.isTrue()).isEqualTo(false)
        status.set(true)
        assertThat(status.isTrue()).isEqualTo(true)

    }

    @Test
    fun `Status map`() {
        StatusMap().apply {
            assertThat(item("unlocked").isFalse()).isEqualTo(true)
            item("unlocked").not()
            assertThat(item("unlocked").isTrue()).isEqualTo(true)
        }
    }
}

class StatusMap {
    val map = mutableMapOf<String, BooleanStatus>()

    fun item(name:String): BooleanStatus {
        return map.getOrPut(name) { BooleanStatus() }
    }
}

class BooleanStatus(var value: Boolean = false) {

    fun set(truth: Boolean) {
        value = truth
    }

    fun isTrue(): Boolean {
        return value
    }

    fun isFalse(): Boolean {
        return !value
    }

    fun not(): Unit {
        value = !value
    }
}

I’ll start with the first test:

class StatusTest {
    @Test
    fun `status isTrue`() {
        val status = BooleanStatus()
        assertThat(status.isTrue).isEqualTo(false)
        status.set(true)
        assertThat(status.isTrue).isEqualTo(true)
        assertThat(status.isFalse).isEqualTo(false)
    }

And the BooleanStatus:

class BooleanStatus(var value: Boolean = false) {

    val isTrue: Boolean get() = value
    val isFalse: Boolean get() = !value

    fun set(truth: Boolean) {
        value = truth
    }
    
    fun not(): Unit {
        value = !value
    }
}

I expect this test to run and the others probably fail because I haven’t updated them. Indeed we might fail the compile, now that I remember where we are, in Kotlin-land.

Right, compiler requires me to change this to use the properties:

    @Test
    fun `Status map`() {
        StatusMap().apply {
            assertThat(item("unlocked").isFalse).isEqualTo(true)
            item("unlocked").not()
            assertThat(item("unlocked").isTrue).isEqualTo(true)
        }
    }

Tests are green. Sweet. I’m a little irritated by not() needing to be a function but such is life. Doesn’t make sense as a property, but the others do.

Big Idea: Mighty Morphing Objects

Now what about creating a new kind of Status thing that starts out undecided and then turns into either an IntStatus or BooleanStatus (in behavior) when first accessed. So, if the first thing you said was isTrue, it’ll act like a boolean but if the first thing you say is value or increment, it acts like an int. (Assuming that you can only ask an IntStatus for value. We don’t even have a need for them yet, but I’m really trying to learn a good way to do this kind of thing.)

Let’s try to write a new test that addresses what we need. And what is that? Well, let’s suppose we create an instance of GameStatus (I want to avoid using the word Status because there’s a famous one in Java.) If we first say to it, say, isTrue, it’ll reply ‘false and thereafter respond as a BooleanStatus, but if we say increment, it’ll behave as an IntStatus.

I think I need a strategy variable that implements an interface. I’m not at all sure about this: I haven’t built a Strategy in years, oddly enough.

    @Test
    fun `strategy converts to boolean`() {
        val bStatus = GameStatus()
        assert(!bStatus.isTrue)
        bStatus.not()
        assert(bStatus.isTrue)
    }

Now, obviously a BooleanStatus would just do this, but we’re looking for bigger fish.

Implementing …

Hmm. I’m trying to take too big a step, creating an interface and a class with a strategy variable, and so on. Let me try this a different way. I’ll implement my GameStatus with some conditional behavior.

Hmm again. I think this may be possible, or may not, but I’m now quite sure that the additional complication isn’t worth it.

Abandon Ship

There’s probably an hour between the last two headings. Didn’t pan out. You gotta know when to abandon ship.

I think I’ll go with the notion of a flags variable and, when we need it, an integers one or numbers or something like that.

But I will create actual little classes with convenience methods.

Reverting to where I was this morning. I’ll create my class as a BooleanStatusMap, leaving open the possibility for others, and then use it.

Oh darn, I didn’t save the property change. It’ll take literally moments to do over.

OK, that’s done:

class StatusTest {
    @Test
    fun `status isTrue`() {
        val status = BooleanStatus()
        assertThat(status.isTrue).isEqualTo(false)
        status.set(true)
        assertThat(status.isTrue).isEqualTo(true)
        assertThat(status.isFalse).isEqualTo(false)

    }

    @Test
    fun `Status map`() {
        BooleanStatusMap().apply {
            assertThat(item("unlocked").isFalse).isEqualTo(true)
            item("unlocked").not()
            assertThat(item("unlocked").isTrue).isEqualTo(true)
        }
    }
}

class BooleanStatusMap {
    val map = mutableMapOf<String, BooleanStatus>()

    fun item(name:String): BooleanStatus {
        return map.getOrPut(name) { BooleanStatus() }
    }
}

class BooleanStatus(var value: Boolean = false) {
    val isTrue: Boolean get() = value
    val isFalse: Boolean get() = !value

    fun not(): Unit {
        value = !value
    }

    fun set(truth: Boolean) {
        value = truth
    }
}

I think those classes will stick. I’ll move them into the right section but I think I’ll keep all the status object in one file, if that’s OK with you.

OK, that’s done, let’s commit: Status objects moved to main/StatusObjects.kt.

Now let’s see about using it in the game code.

We have code like this:

    private fun castSpell(spell: String, world: World): String {
        world.status.put("unlocked", true)
        world.response.say("The magic wd40 works! The padlock is unlocked!")
        return roomName
    }

  go("e","treasure") {
      if (status.getOrDefault("unlocked", false))
          true
      else {
          say("The room is locked by a glowing lock")
          false
      }
  }


class World {
    val status = mutableMapOf<String,Boolean>()

We convert to:

class World {
    val flags = BooleanStatusMap()

    private fun castSpell(spell: String, world: World): String {
        world.flags.item("unlocked").set(true)
        world.response.say("The magic wd40 works! The padlock is unlocked!")
        return roomName
    }


  go("e","treasure") {
      if (flags.item("unlocked").isTrue)
          true
      else {
          say("The room is locked by a glowing lock")
          false
      }
  }

Tests remain green.

Probably the item method should be get. Refactor. Green. Commit: World uses BooleanStatusMap for flags. Use get(name) to get a BooleanStatus item. Defaults to false.

Well, I’m not entirely loving this, but it seems pretty decent to me, and a bit better than the raw map from yesterday.

Let’s sum up and call it an afternoon.

Summary

I messed up slightly by failing to commit my first change, over to the properties instead of methods, but of course it was just moments to fix it. Perhaps there was even a refactoring to make IDEA do it. Even by hand it was quick.

My idea of making a magical morphing be whatever you want object did not pan out. I’m not sure whether there’s something clever that I could do, but looking at the things I tried, it would be too cool to be allowed to live. Another member variable numbers will work just fine when we need it, and maybe I’ll get a new idea after that.

I think that probably the next thing to try will be a cut at inventory, which might be a pretty big deal. I think we’ll need:

  • Better parsing of commands, to all take axe, drop rod kind of syntax.
  • A player inventory collection. Should be pretty simple.
  • Simple internal commands and queries like inventory.has("axe") and inventory.drop("axe").

How will the inventory know what room to put the axe in when you drop it? Maybe we have to have inventory.move("axe","palace")?

I think we’ll likely run into other issues, and I expect it’ll take a few cuts to come up with something in the DSL and associated lambdas that will serve the world designer decently while still being possible to implement.

We’ll find out. Lots of fun. See you next time!