Hello, loves!

Before working out that validation idea, let’s commit the good bits. Surprise Idea: The search for joy for everyone must never be abandoned.

Note
Down toward the end, musing about how we can always improve code, I followed my nose into more philosophical terrain. If you wish, skip directly there.

Despite the need to be awake, or the inability not to be, I’m not really at my best before dawn. Not sure when I am at my best, but I think the sun has to be up. And although it is cloudy, my watch assure me that the sun has been up for a few hours. I’m not sure what a watch knows about astronomy but the general level of light outside supports the idea.

Anyway, I like the new classes Machine, State, and Event, at least in the way they are used, so let’s get them committed in that form, and then perhaps look further into declaring the specific state and event names we expect.

We ignore two tests, and here are the classes:

class Event:
    def __init__(self, *, to:str, hook):
        self.to = to
        self.hook = hook

class State:
    def __init__(self, **kwargs):
        self.info:dict[str, Event] = dict()
        for k, v in kwargs.items():
            self.info[k] = v

    def __getitem__(self, item:str) -> Event:
        return self.info[item]

class Machine:
    states = []
    def __init__(self,*, states=(), **kwargs):
        self.info:dict[str,State] = dict()
        for k, v in kwargs.items():
            self.info[k] = v

    def __getitem__(self, item:str) -> State:
        return self.info[item]

The dunder method __getitem__ is the one that lets us “subscript” into those two objects, as we do in our QGDenizen’s event method:

class QuestGiverDenizen:
    def event(self, event, interactor):
        transition = self.transitions[self.state][event]
        transition.hook(interactor)
        self.state = transition.to

I’ll move the classes from their current home in the test, to a file in the ‘src’ section. We are green. Commit: moving Machine, State, and Event to src side.

Let’s take a moment to consider the names. Here are the classes in actual use:

class QuestGiverDenizen:
        self.state= 'seeking'
        self.transitions=Machine(
            seeking=State(
                has_item=Event(to='satisfied', hook=self.giving_action),
                no_item=Event(to='seeking', hook=self.seeking_action),
            ),
            satisfied=State(
                has_item=Event(to='satisfied', hook=self.satisfied_action),
                no_item=Event(to='satisfied', hook=self.satisfied_action),
            ),
        )

I included the setting of state because it’s clearly part of what’s going on here, but you’d be forgiven for thinking that the Machine should know its initial state. And you’d also be forgiven, for thinking right after that, that the Machine should know its state. And, if you thought right after that that the Machine should maintain state, I’d be right there with you.

We’re only partly done here (and the system is working perfectly). It seems to me that we should send events to the Machine, it should maintain the state, and it should call the hooks as appropriate. As things stand we ask the machine for the stake and the hooks and we call them. This is Feature Envy and a violation of the Law of Demeter.

Back to the tests to put this in.

ARRGH!

I went kind of random. Tests are broken, I don’t know why but we’ll look in a moment. But Buzz is working correctly in the game, with this code:

class QuestGiverDenizen:
    def __init__(self, *, name,
                 knowledge,):
        self.name = name
        self.knowledge = knowledge
        self.machine=Machine(
            initial_state='seeking',
            seeking=State(
                has_item=Event(to='satisfied', hook=self.giving_action),
                no_item=Event(to='seeking', hook=self.seeking_action),
            ),
            satisfied=State(
                has_item=Event(to='satisfied', hook=self.satisfied_action),
                no_item=Event(to='satisfied', hook=self.satisfied_action),
            ),
        )

    def interact(self, interactor):
        event = self._which_event(interactor)
        self.machine.event(event, interactor)
        return False

(And the various hook methods, of course.) Note the new initial_state parameter to Machine. Note also that we just send the event to the machine: we’re not maintaining state in Buzz any more. The Machine does that.

Now those tests. Ah they are checking bee.state. No such thing any more but we’ll let them probe the machine. Now they’re green.

The ‘arrgh’ above was because between “Back to the tests” and now, I did write a test, which is working, but then Buzz didn’t work and I kind of felt that I was thrashing around rather than calmly and coolly assessing the situation and dealing with it. Fortunately, thrashing worked, but that’s not really the way to bet.

Anyway, I think we’re in pretty good shape now. Let’s commit: Machine now has initial state and maintains state with event method.

I think this is good, but the tests are now way off the mark. Most of the tests in TestStateClasses are evolutionary and while they reflect the order of building the Machine classes, they no longer reflect how the classes are used and how they work. I think we’ll find that we should remove some of them, and possibly recast others, and maybe even write a couple more.

Additionally, I am not entirely comfortable with handling interact inside Denizen. It seems to me that possibly the conversion of interact to an event should be done in the ContentItem, closer to the “UI”. There is an interact function at that level, I believe:

class ContentFactory:
    def quest_giver(self, *, name, quest_item, seeking_sentences, giving_sentences, satisfied_sentences, gift,
                    resources, scale):
        def interaction(self, interactor):
            return self.info.denizen.interact(interactor)

        knowledge = self.create_knowledge(name, seeking_sentences, giving_sentences, satisfied_sentences, gift,
                                          quest_item)
        denizen = QuestGiverDenizen(name=name, knowledge=knowledge)
        info = SimpleNamespace(denizen=denizen)
        return Content(name=name, resources=resources,
                       scale=scale, interaction=interaction,
                       info=info)

    def create_knowledge(self, name, seeking_sentences, giving_sentences, satisfied_sentences, gift, quest_item):
        seeking_sayer = NameSayer.cycle(name, seeking_sentences)
        giving_sayer = NameSayer.once(name, giving_sentences)
        satisfied_sayer = NameSayer.random(name, satisfied_sentences)
        return SimpleNamespace(
            quest_item=quest_item,
            seeking_sayings=seeking_sayer,
            giving_sayings=giving_sayer,
            satisfied_sayings=satisfied_sayer,
            gift=gift,
        )

I’m not sure what is best. How should QuestGiverDenizen, Machine, and ContentFactory.quest_giver be dividing up the responsibilities? We could (probably) dump the machine and knowledge into the Contents info and do the whole job there, without the Denizen class existing at all. If I’m not mistaken, the Denizen is the first and only smart class that is embedded inside a Content. All the other ones, even things that change state, like the lever are done entirely with callbacks inside Content.

That said, the Denizen idea is on its way to being more than just a thing that stands around handing out quests. It seems likely that we’ll turn it into a Non-Player Character (NPC) that can wander around and perhaps do other things.

Reflection, a Bit Deeper

It is fascinating to me how many reasonable-seeming designs are possible for one product capability, in this case a somewhat general-purpose ability to say things to Dot, sending her off to find things and bring them back, in order to receive something that she needs. We have surely committed at least four significantly different versions of the bee already, and here we are thinking about another one, still fuzzy but taking shape.

Now my real purpose here is to entertain myself, and perhaps some readers, but showing how I think about design, and how I respond to those thoughts by changing the code in small steps, keeping it working all along. If that has any practical use, it is in demonstrating that often—I want to say ‘always-ε’—we can bring a design that has become unruly back under control without suffering major delays while we rewrite the universe from first principles.

But, unlike what one might think, I believe there is no one “right” design for most things. More likely, there are many “close enough” designs, and quite likely, any real design we have in place can be improved. The challenge, in actual software development, is in how far to go and where to stop. We are told that Leonardo DaVinci said:

Art is never finished, only abandoned.

Presumably he really said:

L’arte non è mai finita, solo abbandonata.

He didn’t speak English as far as we know. I think the quotation applies to all our human work: nothing is ever finished, but often we stop, say “good enough” and move on to the next feature or the next floor to scrub.

If we are fortunate, as I have been, we’ll find work that we enjoy, that lets us build skill and knowledge, so as to improve ourselves and the work. If we are less fortunate, our work may be boring or tiring, and our joy comes more from a hobby or avocation. And, sadly, there are many who are so tired and worn down as to find little joy at all.

And that is a tragedy that should never happen. This world has enough resources, and enough wonder, to permit everyone to find time for joy. We need to recognize that the most important work that needs to be done today is to bring that world into being.

Perhaps all that you can do, perhaps all that I can do, is to let our little voices be heard, and to vote. Perhaps we can find ways to influence a few people. Perhaps some of us will join protests, perhaps others will run for office or make themselves heard in other ways.

Wow! Where did this come from? State machine to never giving up the search for joy. I don’t know, but I know this:

The search for joy for everyone must never be abandoned.