Hello, loves! Tout le monde déteste l’IA.

Working on Denizens seems interesting enough to keep me going. Let’s see what we might do. Some discussion of how we generalize here chez Ron.

Despite the way I seem to program, sort of failing forward with poor ideas that then get refactored into less poor ones, I do know a bit about programming. And, in fact, I strongly suspect that if one did not know quite a bit, one would be unlikely to have (even) the level of success that I have in turning early ad hoc solutions into rather decently-designed code. One would be less able to detect poor design, less able to sense the direction of a better design, and, quite probably, one would be well short of useful approaches to the problems that arise.

We are facing such a concern right now: our first Denizen of many: Buzz, the bee.

When Dot first encounters Buzz, he will be feeling an intense need to pollinate something. (We’ve all been there, haven’t we?) When interacting with Dot, we want him to give various hints, until Dot finally figures it out and brings him a flower that she will find somewhere in the dungeon. When interacting with Dot when she has the flower. Buzz will gratefully take the flower and reward Dot, perhaps with a nice comb of nourishing honey. (We do not have to work out what the honey is good for, but perhaps there is a bear in the dungeon, or perhaps Dot gets peckish from time to time.)

So Buzz basically has at least two states, pining for the fjords flower, and having received the flower and given the honey to Dot. Maybe a third state of just buzzing about, depends what we decide later.

So, a thing has a few states and does different things depending on those states. That suggests what we in the trade call a Finite State Machine, FSM for short. We in the trade may even have a decent sense of how one implements an FSM, often with tables indexed by state, containing conditions to be checked and actions to be taken. More elaborate FSMs often include different conditions and actions for first entering the state, re-entering the state after something has happened, leaving the state, and so on. We here at my house (chez Ron) vaguely recall having done such things in our shadowed past. And we are quite sure that we can make our Denizens do most anything we may need using a Finite State Machine mechanism.

If only we had one. We could, of course, divert briefly, whip up a nice FSM, give it some excellent features, and then use it to program Buzz. But you know, and I know, we’re not going to do that. Instead, we’re going to code up Buzz, and then, either working just with Buzz or perhaps working on a second Denizen, we’re going to evolve to the FSM. Because that’s how we roll.

Let’s review Buzz as he exists right now.

Buzz exists, from the Dungeon’s viewpoint, as an item of Content. He’s created by a ContentFactory method:

class ContentFactory:
    def bee(self, *, name):
        the_bee = Denizen(name=name)
        info = SimpleNamespace(bee=the_bee)
        def bee_behavior(self, interactor):
            result = self.info.bee.interact(interactor)
            return result
        return Content(name=name, resources=['bee.png'],
                       scale=0.5, interaction=bee_behavior,
                       info=info)

Buzz’s only behavior, so far, is that when Dot interacts with him, bee_behavior will be called, passing the the standard instance of Interactor that content gets. We see that bee_behavior looks up the bee member in the provided info packet, and sends it interact(interactor). The bee member is an instance of Denizen, named, in this case, Buzz, because in main:

main.py
def add_content(layout, dungeon):
    factory = ContentFactory()
    item = factory.bee(name='Buzz')
    cell =  Cell(33, 25)
    cell.add_content(item)
    ...

Finally, as it stands now, Denizen:

class Denizen:
    def __init__(self, *, name):
        self.name = name

    def interact(self, interactor):
        interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')
        return False

Let’s begin by just coding up a bit more behavior in Denizen. We’ll assume for now that the class is just for Bees - it was premature of me to call it Denizen. I apologize: I got carried away with the word.

I think it would be nice if states were just strings. We might want to make them an enum or something later but strings will be just fine for now, it’s early days. Bees have two states, seeking (something to pollinate) and satisfied.

Ah, forgive me, I started coding when I shouldn’t have. Here’s what I did:

class Denizen:
    def __init__(self, *, name):
        self.name = name
        self.state = 'seeking'

    def interact(self, interactor):
        if self.state == 'seeking':
            interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')
        else:
            interactor.publish('announce', 'xx', message='Buzz: "Impossible Situation"')
        return False

No harm done but what I meant to do was to write some tests for Buzz, the Denizen Bee.

class TestDenizen:
    def test_initial(self):
        assert False

OK, now in order to test this object, we need an interactor, and I think we’ll be wanting one that verifies that the right things have happened. So far it just needs a publish. First I’ll write a simple test and then we’ll deal with the fake interactor.

class TestDenizen:
    def test_initial(self):
        bee = Denizen(name='Buzz')
        bee.interact(None)

This likely fails on publish:

>           interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')
            ^^^^^^^^^^^^^^^^^^
E           AttributeError: 'NoneType' object has no attribute 'publish'

Yep. So we’ll have a simple interactor:

class FakeInteractor:
    def __init__(self):
        self.kwargs = {}

    def publish(self, action, caller_id, *args, **kwargs):
        self.kwargs = kwargs


class TestDenizen:
    def test_initial(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        bee.interact(interactor)

Test passes but let’s have an assertion:

    def test_initial(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        bee.interact(interactor)
        assert 'nice flower' in interactor.kwargs['message']

Test passes.

What we really want, however, is a bit more action. Let’s extract a method in Denizen and elaborate:

class Denizen:
    def interact(self, interactor):
        if self.state == 'seeking':
            self.seeking(interactor)
        else:
            interactor.publish('announce', 'xx', message='Buzz: "Impossible Situation"')
        return False

    def seeking(self, interactor):
        interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')

Now in seeking state, we want Buzz to give that message if Dot has no flower but if she has the flower he should say something different and enter a new state, one of satiety. I think he’ll say something on the way out, at least for now.

class Denizen:
    def seeking(self, interactor):
        if interactor.has('flower'):
            interactor.publish('announce', 'xx', message='Buzz: "Oh thank you! Have some honey!"')
        else:
            interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')

This breaks the test, because our interactor does not understand has. (Nor does the real one. We are working out here what the real one needs.)

class FakeInteractor:
    def __init__(self):
        self.kwargs = {}
        self.inventory = []

    def has(self, item):
        return item in self.inventory

Test passes. Expand the test. No, let’s write a new one, just because.

    def test_receives(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        bee.interact(interactor)
        assert 'nice flower' in interactor.kwargs['message']
        interactor.inventory.append('flower')
        bee.interact(interactor)
        assert 'honey' in interactor.kwargs['message']

Well how about that! If the real Interactor understood has, and if there was a flower in the Dungeon, Buzz would actually work, at least to the extent of saying the right thing. Let’s test further.

    def test_receives(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        bee.interact(interactor)
        assert 'nice flower' in interactor.kwargs['message']
        interactor.inventory.append('flower')
        bee.interact(interactor)
        assert 'honey' in interactor.kwargs['message']
        assert bee.state is 'satisfied'

This fails, but readily fixed:

class Denizen:
    def seeking(self, interactor):
        if interactor.has('flower'):
            interactor.publish('announce', 'xx', message='Buzz: "Oh thank you! Have some honey!"')
            self.state = 'satisfied'
        else:
            interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')

But what does he do when satisfied? Well, not much, but he should say something:

    def test_satisfied(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        interactor.inventory.append('flower')
        bee.interact(interactor)
        assert bee.state is 'satisfied'
        bee.interact(interactor)
        assert 'happy' in interactor.kwargs['message']

And …

class Denizen:
    def satisfied(self, interactor):
        interactor.publish('announce', 'xx', message = 'Buzz: "I\'m just a happy little bee!')

So this is all quite good. It is, arguably, a hand-coded Finite State Machine. Let’s make Denizen actually give the content. I think the real Interactor has that method, so we’ll use that name. receive_content. I don’t like the name but there it is. For now, we’ll just pass in the string:

    def test_satisfied(self):
        interactor = FakeInteractor()
        bee = Denizen(name='Buzz')
        interactor.inventory.append('flower')
        bee.interact(interactor)
        assert bee.state is 'satisfied'
        bee.interact(interactor)
        assert 'happy' in interactor.kwargs['message']
        assert 'honey' in interactor.inventory

Test fails looking for honey. We code:

    def seeking(self, interactor):
        if interactor.has('flower'):
            interactor.publish('announce', 'xx', message='Buzz: "Oh thank you! Have some honey!"')
            interactor.receive_content('honey')
            self.state = 'satisfied'
        else:
            interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')

And the test passes. Commit: elaborating Denizen into FSM.

That was a mistake. Interactor doesn’t have has. Let’s quickly fix that.

class Interactor:
    def has(self, content_name):
        return self.dungeon.dot_has(content_name)

And … it turns out that dot_has is already a method, used to see if she has the brilliant torch! What a lovely discovery. The game now runs, but there is no flower for Dot to find. That’s fine, we can do that soon. For now, it’s getting to be time for bacon bacon bacon!

Summary

We have a trivial and fragile Finite State Machine in Denizen, which can currently only preform as Buzz the Denizen Bee, saying one thing, and, if we had a flower to give him, probably breaking the world by giving Dot a string instead of a content. We’ll fix that, I think, by having another receive method for strings.

We should probably talk about the strings, and their fragility and what we should use instead. Perhaps we’ll do that next time.

Of course our real work will not be done until we “generalize” Denizen to support at least something other than Buzz, at least other single-step Denizens that want you to bring them things. And, of course, that is exactly what we’ll do, moving from this single-purpose object to more and more generally useful objects. Because that’s what we do: wee move from simple seemingly ad-hoc code to generally useful code, step by step.

See you next time!