Denizen Review
Hello, loves!
We’ll begin with a look at the new Denizen-related code. Refactor a bit. Add a flower. Tout le monde déteste l’IA.
Yesterday I had a thought and for some reason tooted it on Mastodon:
Hmm. What if the messages were indexed by the state name?
Well, but the first ones and last ones are said one per interaction, and the middle ones are said all at once. And anyway, there are three sets of messages and only two states. So in principle, they’re indexed by state+event. One state+event combo never occurs.
Maybe the one vs all distinction is in the Sayer, not the hook, and there are two actions in the seeking to satisfied hook? A standard hook that is always run, plus the gift one?
I have the luxury, of course, that I can spend a week polishing three lines of code if I care to. Folx working for a living generally do that have that privilege, but I think they’d be wise to hone their skills by spending some available time doing something similar. That’s how we learn, in our bones or wherever we learn such things, what good code is like, what options are available, and how to move from here to there.
- Note
- Imagine a rueful note here about the “Age of AI”, and how programming by humans will soon be a quaint thing of the past like building your own furniture or churning your own milk. Or whatever one does with milk. Anyway, imagine that note, I’m not in a mood to write it.
As I think I mentioned yesterday, I have the vague feeling that there are too many components making up the QuestGiverDenizen, with its state machine, and the Content instance that contains it. Or maybe it’s just that the components aren’t all quite where we might want them. So we’ll look, think, and speculate.
Let’s start with the Denizen:
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
def _which_event(self, interactor):
return 'has_item' \
if interactor.has(self.knowledge.quest_item) \
else 'no_item'
def seeking_action(self, interactor):
interactor.announce(next(self.knowledge.seeking_sayings))
def giving_action(self, interactor):
for saying in self.knowledge.giving_sayings:
interactor.announce(saying)
interactor.receive_content(self.knowledge.gift)
def satisfied_action(self, interactor):
interactor.announce(next(self.knowledge.satisfied_sayings))
Ah. I think I’m starting to see something. A few things.
First, we do not use the name parameter or instance variable. Remove those. Back up, that didn’t work. Remove the setter, commit. Now Change Signature, to fix up the callers, which didn’t happen when I just edited the init like some kind of barbarian. Tests run. Commit.
OK, now the structure of this thing. Its members are knowledge, and machine. Three of its methods are referenced in the Machine: seeking_action, giving_action, and satisfied_action. They are executed on the various transitions of the machine, and only then. They reference the knowledge.
So it seems to me that knowledge, machine, and those three methods are a thing. And what they are, I think, are the variable parts of a specific event-driven finite state machine. We could readily create a QGDenizen and, from the outside, send event messages to its machine part, and the QGDenizen would do its thing.
Round Tuit
Let’s make some changes: it’s what we do.
class ContentFactory:
def quest_giver(self, *, name, quest_item, seeking_sentences, giving_sentences, satisfied_sentences, gift,
resources, scale):
def interaction(self, interactor):
event = 'has_item' if interactor.has(quest_item) else 'no_item'
self.info.denizen.event(event, interactor)
return False
...
This doesn’t break any tests, and I really think it should. We’ll hold off on that and make the necessary change to the denizen, which will probably break things:
class QuestGiverDenizen:
def event(self, event, interactor):
self.machine.event(event, interactor)
I also removed the interaction and _which_event, and that does break five tests. Here’s the first one:
class TestDenizen:
def test_initial(self):
interactor = FakeInteractor()
sayer = NameSayer.cycle('Buzz', ['Hmm'])
knowledge = SimpleNamespace(quest_item='anything', seeking_sayings=sayer)
bee = QuestGiverDenizen(knowledge=knowledge)
bee.interact(interactor)
assert 'Hmm' in interactor.kwargs['message']
I think the other tests will mostly be like this one, needing to be sent a suitable event, since now the Denizen object doesn’t concern itself with what Dot has: that is up to the Content-level interact. This passes:
def test_initial(self):
interactor = FakeInteractor()
sayer = NameSayer.cycle('Buzz', ['Hmm'])
knowledge = SimpleNamespace(quest_item='anything', seeking_sayings=sayer)
bee = QuestGiverDenizen(knowledge=knowledge)
bee.event('no_item', interactor)
assert 'Hmm' in interactor.kwargs['message']
This is actually better, because we have uncoupled the Denizen from Dot’s inventory, and the tests can probably be simpler. Here’s the next one:
def test_receives(self):
interactor = FakeInteractor()
initial_sayings = NameSayer.cycle('Buzz', ['Hmm'])
gift_sayings = NameSayer.once('Buzz', ['may need'])
knowledge = SimpleNamespace(quest_item='a flower', gift='anything', seeking_sayings=initial_sayings, giving_sayings=gift_sayings)
bee = QuestGiverDenizen(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.machine.state == 'satisfied'
We can avoid adding to inventory, because we don’t have to parse inventory, we just send the right event. This passes:
def test_receives(self):
interactor = FakeInteractor()
initial_sayings = NameSayer.cycle('Buzz', ['Hmm'])
gift_sayings = NameSayer.once('Buzz', ['may need'])
knowledge = SimpleNamespace(quest_item='a flower', gift='anything', seeking_sayings=initial_sayings, giving_sayings=gift_sayings)
bee = QuestGiverDenizen(knowledge=knowledge)
bee.event('no_item', interactor)
assert 'Hmm' in interactor.kwargs['message']
bee.event('has_item', interactor)
assert 'may need' in interactor.kwargs['message']
assert bee.machine.state == 'satisfied'
I think some of these tests are redundant, but for now I’ll just fix them up. Three more to go. I’ll just do them and only report back when done or if something interesting arises. BRB.
Back. We’re green. Let’s check the game, just to be certain. We’re good. Commit. Here’s QuestGiverDenizen now, a bit simpler:
class QuestGiverDenizen:
def __init__(self, *, knowledge):
self.knowledge = knowledge
self.machine=Machine(
initial_state='seeking',
seeking=State(
has_item=Event(to='satisfied', hook=self._giving_hook),
no_item=Event(to='seeking', hook=self._seeking_hook),
),
satisfied=State(
has_item=Event(to='satisfied', hook=self._satisfied_hook),
no_item=Event(to='satisfied', hook=self._satisfied_hook),
),
)
def event(self, event, interactor):
self.machine.event(event, interactor)
def _seeking_hook(self, interactor):
interactor.announce(next(self.knowledge.seeking_sayings))
def _giving_hook(self, interactor):
for saying in self.knowledge.giving_sayings:
interactor.announce(saying)
interactor.receive_content(self.knowledge.gift)
def _satisfied_hook(self, interactor):
interactor.announce(next(self.knowledge.satisfied_sayings))
One of these things is not like the others: the only public method here is event. I took the liberty of marking the three action methods as private, and renaming them _hook. (I’m not sure where I got the word ‘hook’. Some article about FSMs. Possibly ‘action’ was better but since the key is ‘hook’, I think this is better for now.) Commit.
Reflection
As has been the case for quite a while now, the QuestGiverDenizen is usable as is for any Denizen who makes cryptic statements to Dot, until she brings something, at which point it makes additional remarks and gives her something, thereafter just mumbling useless phrases. (Or useful ones if we wished.)
If that’s all we even do, I think the Machine/State/Event state machine is more than we needed, but I rather like it for its declarative style and flexibility. If we ever devise a more complicated denizen, we can follow this pattern with different states and knowledge and private hooks, and build pretty much anything we might need.
If we ever need it. If we never do need it, this investment in the code will not pay off, although the investment in our knowledge and understanding will likely pay off at some point.
I would not recommend that a team do this kind of investment without a clear product need, understood and expressed by the product owner, customer, requirements giver, boss. Even then, if the team is good at refactoring, I’d suggest doing cases and then generalizing rather than the other way around, but of course every individual, every team, gets to make their own decisions. I’m just reflecting my history and my fears.
Future
When the next smarter kind denizen comes along, if it ever does, we can see that there will be a class like this one, with different Machine/State/Event structure, and different private hook methods. And it will have the same event method. Duplication. Its __init__ will almost certainly be duplicated as well. The solution, should we decide to deal with it, will probably be some kind of container object into which one plugs a knowledge, a machine, and somehow, the associated private methods.
We could work out how to do that now. But, heeding my advice above, we won’t. We’ll burn that bridge when we come to it.
I think we’re really done for the morning, but I want to do a little thing. In main:
item = factory.receivable(name="a flower", resource='flower.png', scale=0.5)
cell2 = Cell(29, 30)
cell2.add_content(item)
I have personally hand-crafted a flower texture. I don’t think it’s quite perfect but we’ll see how it looks:

I call that nearly good. the flower has a stem and leaves, which do not show up well on the dark background. I’ll go back to the drawing board (haha) but it’s better than the star that used to represent the flower.
Summary
I’ve wanted a flower there for a long time and finally got around to drawing one. I drew two, in fact, and a honeycomb that should show up in inventory when we display inventory.
Maybe that should be next. I think we’re done, for now, with the QuestGiver notion. Next time, something completely different. Or somewhat different.
See you then!