Hello, loves!

As I dozed off, I had an Idea. We’ll think about that and then find something to do. LLM note. Important(?) closing. Tout le monde déteste l’IA.

When I go to bed, I often think about whatever I’m programming, as such thoughts are rarely negative or troubling, so it’s not hard to get to sleep. There is a small risk that I’ll get back up to try them, but usually I just drop off.

Last night I was thinking about what to do next, and what Denizen is like, with its different snatches of conversation, the hint-giving phase, the receiving phase, and the satisfied nothing to see here move along phase. It came to me that there might be more phases, perhaps triggered by time, something else in the dungeon, etc.

It came to me that I was thinking about the personality of a Denizen. It seemed to me that there might be at least two aspects to the Denizen, a general object that executes the different states, and the states and associated data themselves. And it seems to me now that we can almost see that division even now. Here’s Denizen. Pardon the over-long and messy init: it’s part of the issue.

class Denizen:
    def __init__(self, *, name,
                 initial_sayings=None,
                 gift=None):
        self.name = name
        if initial_sayings is None:
            initial_sayings = ['Hmm...']
        self.initial_sayings = NameSayer.cycle(self.name, initial_sayings)
        gift_sentences = [
            'Oh, thank you!',
            'I am most grateful!',
            'Here\'s something you may need.',
        ]
        self.gift_sayings = NameSayer.once(self.name, gift_sentences)
        satisfied_sentences = [
            'I\'m just a happy little bee!',
            'Hmmm, hmmm, just buzzin along.',
            'Nothing to see here, just a bee'
        ]
        self.satisfied_sayings = NameSayer.random(self.name, satisfied_sentences)
        self.gift = gift
        self.say = 0
        self.gift = gift
        from content import ContentFactory
        self.gift = ContentFactory().receivable(name='delicious honeycomb',
                                                resource='honeycomb.png',
                                                scale=1)
        self.state = self.seeking

    def interact(self, interactor):
        self.state = self.state(interactor)
        return False

    def seeking(self, interactor):
        if interactor.has('a flower'):
            if self.gift:
                for saying in self.gift_sayings:
                    interactor.announce(saying)
                interactor.receive_content(self.gift)
            return self.satisfied
        else:
            interactor.announce(next(self.initial_sayings))
            return self.seeking

    def satisfied(self, interactor):
        interactor.announce(next(self.satisfied_sayings))
        return self.satisfied

The general bit of course includes the interact method, which just executes a state, getting a new state. And the seeking and satisfied states are specific to a simple Denizen that sends Dot on a quest, from which she returns with some key object in her inventory, after which the Denizen gives her some presumably desirable item.

And the init is a bit of a mess. I think we’ll probably improve that as we create other denizens, but it is quite clear that the only thing having to do with the state machine part is the self.state='seeking' bit, and that should be a parameter. I also notice this clever sequence:

        self.gift = gift
        self.say = 0
        self.gift = gift

I guess the author (Ron) wanted to be sure that gift was set. As for say, it is never used. I’ll remove a couple of lines from that. Commit: tidying.

As I think about how we might get all that init sorted, I’m reminded of the info instance variable we use in the Content object. Different instances of Content need more information than others. So, in addition to a few standard instance variables, Content accepts a parameter info, typically a SimpleNamespace, allowing code like this in a more intelligent Content item. Here’s part of ContentFactory making the spikes:

    def spikes(self, *, name):
        resource1 = 'trap/1.png'
        resource2 = 'trap/2.png'
        resources = [resource1, resource2]
        scale = 0.75
        cases = {
            0: (True, 0),
            1: (False, 1),
            2: (False, 0),
            3: (True, 0),
        }
        info = SimpleNamespace(cycling=True, cases=cases, time=0)

        def cycle(self, pub_sub, delta_time):
            if not self.info.cycling: return
            self.info.time += delta_time
            if self.info.time >= 1:
                self.info.time = 0
                self.state = (self.state+1)%len(self.resources)
                pub_sub.publish('state_number', self.name, content=self, state=self.state)
        cycle_sub = Subscription(event='on_update', caller_id='view', callback=cycle)

In that object, info contains three variables that the code needs. So, in effect, info extends the knowledge of a Content item, but only when the particular kind of Content needs it.

In the case of a Personality, if we were to have one (always hoped for a better one, but here we are), we could do something similar, tucking all the and other variables away in a single package that the state-execution part just tucks away.

I’m glad we had this little chat. I had no idea what we might do this morning, but now I have a possibly useful idea: let’s modify Denizen so that we pass in all the sayings and such in a single package.

The bee is created using the bee method of ContentFactory.

class ContentFactory:
    def bee(self, *, name, initial_sayings):
        the_bee = Denizen(name=name, initial_sayings=initial_sayings)
        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)

All this does is set up the bee Content item to forward the interaction to the Denizen. Our concern here is improving how we build the Denizen. Let’s posit a new Denizen parameter, which we’ll call knowledge. You could argue that it should be named info by analogy …

No, wait. If we believe that the only code in Denizen that is general is the interact method:

    def interact(self, interactor):
        self.state = self.state(interactor)
        return False

Maybe we don’t need the Denizen class at all. We might be able to do it all with Content and embedded methods, as we do elsewhere. No, that’s too big a bite. Let’s continue with the knowledge idea. Add a parameter to Denizen:

    def __init__(self, *, name,
                 initial_sayings=None,
                 gift=None,
                 knowledge=None,):
        ...
        self.knowledge = knowledge if knowledge is not None else dict()
        self.state = self.seeking

I think we’ll break some Denizen tests here. We’ll find out. This code works. Commit it: we’re going to do this. We can always back way up if we have to. adding knowledge to Denizen.

Let’s posit that the gift messages are in knowledge. That means we change this:

    def seeking(self, interactor):
        if interactor.has('a flower'):
            if self.gift:
                for saying in self.gift_sayings:
                    interactor.announce(saying)
                interactor.receive_content(self.gift)
            return self.satisfied
        else:
            interactor.announce(next(self.initial_sayings))
            return self.seeking

To this:

    def seeking(self, interactor):
        if interactor.has('a flower'):
            if self.gift:
                for saying in self.knowledge.gift_sayings:
                    interactor.announce(saying)
                interactor.receive_content(self.gift)
            return self.satisfied
        else:
            interactor.announce(next(self.initial_sayings))
            return self.seeking

So we remove the setting of those messages from Denizen and move them to bee:


class ContentFactory:
    def bee(self, *, name, initial_sayings):
        gift_sentences = [
            'Oh, thank you!',
            'I am most grateful!',
            'Here\'s something you may need.',
        ]
        sayer = NameSayer.once(name, gift_sentences)
        knowledge = SimpleNamespace(gift_sayings=sayer)
        the_bee = Denizen(name=name, initial_sayings=initial_sayings, knowledge=knowledge)
        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)

Create the NameSayer and put it in gift_sayings. The bee works. Some test is failing, check it out. The test needs to provide some sayings is all.

    def test_receives(self):
        interactor = FakeInteractor()
        gift_sayings = NameSayer.once('Buzz', ['may need'])
        knowledge = SimpleNamespace(gift_sayings=gift_sayings)
        bee = Denizen(name='Buzz', gift='anything', knowledge=knowledge)
        bee.interact(interactor)
        assert 'Hmm' in interactor.kwargs['message']
        interactor.inventory.append('a flower')
        bee.interact(interactor)
        assert 'may need' in interactor.kwargs['message']
        assert bee.state == bee.satisfied

Green, commit.

Reflection

It seems that we are just moving messiness from Denizen to the bee ContentFactory method, and that is true. However, we are also removing bee-specific messiness to bee, leaving just Denizen things in Denizen. Putting mess where it belongs is (slightly) better than having it all over.

Back to Denizen. Let’s do the initial sayings:

    def seeking(self, interactor):
        if interactor.has('a flower'):
            if self.gift:
                for saying in self.knowledge.gift_sayings:
                    interactor.announce(saying)
                interactor.receive_content(self.gift)
            return self.satisfied
        else:
            interactor.announce(next(self.knowledge.initial_sayings))
            return self.seeking

That demands a similar change to bee.

class ContentFactory:
    def bee(self, *, name, initial_sayings):
        gift_sentences = [
            'Oh, thank you!',
            'I am most grateful!',
            'Here\'s something you may need.',
        ]
        initial_sayer = NameSayer.sequence(name, initial_sayings)
        gift_sayer = NameSayer.once(name, gift_sentences)
        knowledge = SimpleNamespace(
            gift_sayings=gift_sayer,
            initial_sayer=initial_sayer,)
        the_bee = Denizen(name=name, initial_sayings=initial_sayings, knowledge=knowledge)

Three tests break. They all need to be upgraded to provide knowledge and some initial messages, similar to this one:

    def test_initial(self):
        interactor = FakeInteractor()
        sayer = NameSayer.sequence('Buzz', ['Hmm'])
        knowledge = SimpleNamespace(initial_sayings=sayer)
        bee = Denizen(name='Buzz', knowledge=knowledge)
        bee.interact(interactor)
        assert 'Hmm' in interactor.kwargs['message']

Two similar changes and we are green. I think I’ll test in the game, for security and because I enjoy seeing it work. Works just fine. Commit.

Let’s remove the initial_sayings parm from Denizen. It’s looking more clean:

class Denizen:
    def __init__(self, *, name,
                 gift=None,
                 knowledge=None,):
        self.name = name
        satisfied_sentences = [
            'I\'m just a happy little bee!',
            'Hmmm, hmmm, just buzzin along.',
            'Nothing to see here, just a bee'
        ]
        self.satisfied_sayings = NameSayer.random(self.name, satisfied_sentences)
        self.gift = gift
        from content import ContentFactory
        self.gift = ContentFactory().receivable(name='delicious honeycomb',
                                                resource='honeycomb.png',
                                                scale=1)
        self.knowledge = knowledge if knowledge is not None else SimpleNamespace()
        self.state = self.seeking

Now the satisfied ones, over to bee.

class Denizen:
    def satisfied(self, interactor):
        interactor.announce(next(self.knowledge.satisfied_sayings))
        return self.satisfied

And:

class ContentFactory:
    def bee(self, *, name, initial_sayings):
        gift_sentences = [
            'Oh, thank you!',
            'I am most grateful!',
            'Here\'s something you may need.',
        ]
        initial_sayer = NameSayer.cycle(name, initial_sayings)
        gift_sayer = NameSayer.once(name, gift_sentences)
        satisfied_sentences = [
            'I\'m just a happy little bee!',
            'Hmmm, hmmm, just buzzin along.',
            'Nothing to see here, just a bee'
        ]
        satisfied_sayer = NameSayer.random(self.name, satisfied_sentences)
        knowledge = SimpleNamespace(
            gift_sayings=gift_sayer,
            initial_sayings=initial_sayer,
            satisfied_sayings=satisfied_sayer,)
        the_bee = Denizen(name=name, knowledge=knowledge)
        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)

There seems to be no test breaking, which suggests to me that we’re missing a test. Anyway, try the game.

Shouldn’t have said self.name in the random. I blame PyCharm.

Important Observation re LLM-AI

PyCharm did in fact prompt that self.name, and I glanced at it and accepted it. My bad, of course. And that was in less than a line of code, not a big code blurt from some cursed text-extruding “AI”. What are my chances of spotting an error that small in a wall of mangled code from the probabilistic parrot? Slim and none, I’d say.

“AI” is not your friend, whether you are a programmer or a user of programs.

Back To Work

I think we’re just left with the gift. Remove from the Denizen, leaving just this:

class Denizen:
    def __init__(self, *, name,
                 knowledge=None,):
        self.name = name
        self.knowledge = knowledge if knowledge is not None else SimpleNamespace()
        self.state = self.seeking

    def interact(self, interactor):
        self.state = self.state(interactor)
        return False

    def seeking(self, interactor):
        if interactor.has('a flower'):
            if self.knowledge.gift:
                for saying in self.knowledge.gift_sayings:
                    interactor.announce(saying)
                interactor.receive_content(self.knowledge.gift)
            return self.satisfied
        else:
            interactor.announce(next(self.knowledge.initial_sayings))
            return self.seeking

    def satisfied(self, interactor):
        interactor.announce(next(self.knowledge.satisfied_sayings))
        return self.satisfied

And in bee, of course:

class ContentFactory:
    def bee(self, *, name, initial_sayings):
        gift_sentences = [
            'Oh, thank you!',
            'I am most grateful!',
            'Here\'s something you may need.',
        ]
        initial_sayer = NameSayer.cycle(name, initial_sayings)
        gift_sayer = NameSayer.once(name, gift_sentences)
        satisfied_sentences = [
            'I\'m just a happy little bee!',
            'Hmmm, hmmm, just buzzin along.',
            'Nothing to see here, just a bee'
        ]
        satisfied_sayer = NameSayer.random(name, satisfied_sentences)
        gift = ContentFactory().receivable(name='delicious honeycomb',
                                                resource='honeycomb.png',
                                                scale=1)
        knowledge = SimpleNamespace(
            gift=gift,
            gift_sayings=gift_sayer,
            initial_sayings=initial_sayer,
            satisfied_sayings=satisfied_sayer,)
        the_bee = Denizen(name=name, knowledge=knowledge)
        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)

Some tests need fixing up, no surprise there. Easily done, same kinds of changes as before. Commit: individual Denizens keep all their info in knowledge parameter.

Speaking walls of text, this article is long, although it’s mostly slightly-changed code bits. Let’s sum up.

Summary

Denizen has been transformed in a series of small commits, from a very specialized bee-oriented thing to a general purpose thing with two states, seeking and satisfied, with a gift-giving transition between those to states. By filling in different text, and a different gift, we can have a bee, an ancient wizard, a hookah-smoking caterpillar to give you the call, whatever we want.

That’s a good thing. It could be even better, if we could devise a way to provide a more complex state diagram perhaps with as many as three states! Or even more!! Maybe we can devise such a thing. We’re certainly better positioned to do so.

Meanwhile, however, our ContentFactory bee method has become rather messy. When we next make a Denizen, perhaps a creepy pair of twin girls, we’ll probably find ways to clean that up, again separating the common elements from the specialized ones. Probably we wind up with a denizen ContentFactory method, fed by a bee method and a twins method.

Underlining

If I had to boil down the over five million words in this web site to one message, it would be something like this:

No matter how bad our design gets, we can “always” bring it back to good condition in a series of small steps, each of which can be made independently, over whatever period of time it takes. We “never” have to rewrite big swathes of code.

For large values of “always” and small values of “never”.

See you next time!