Denizens—Too Soon
Hello, loves! Tout le monde déteste l’IA.
Even working incrementally, I think it’s too soon to be generalizing the Bee Denizen. Discuss, decide, do.
I don’t think I can clearly define why I think it’s too soon to start moving toward a general purpose multi-state Denizen, but it’s mostly that we don’t know enough, yet, about what they need to do. Building a general tool when we don’t yet know what the tasks will be is what gets you a multi-tool with a flint fire starter and a bottle opener when what you need turns out to be a box opener, a nail file, and a small screwdriver for your glasses.
So, what to do? I think two things. First, we’ll finish up the Bee, get him nice and spiffy. Then, quite likely, we’ll come up with another Denizen with roughly the same pattern of behavior and build that, discovering any additional generalization that we might need. Then—as usual I was mistaken about the two—we’ll probably do a Denizen with a different style of behavior, rinse repeat.
We’ll generalize when we have something needing it, in other words, not in preparation for possibly needing it.
Let’s proceed. Here’s the Denizen so far:
class Denizen:
def __init__(self, *, name):
self.name = name
self.state = 'seeking'
def interact(self, interactor):
if self.state == 'seeking':
self.seeking(interactor)
elif self.state == 'satisfied':
self.satisfied(interactor)
else:
interactor.publish('announce', 'xx', message='Buzz: "Impossible Situation"')
return False
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."')
def satisfied(self, interactor):
interactor.publish('announce', 'xx', message = 'Buzz: "I\'m just a happy little bee!')
We might rename the class, but there’s nothing here that is so tied to a Bee as to trouble me.
We have tests that make me think this works:
class FakeInteractor:
def __init__(self):
self.kwargs = {}
self.inventory = []
def has(self, item):
return item in self.inventory
def publish(self, action, caller_id, *args, **kwargs):
self.kwargs = kwargs
def receive_content(self, item):
self.inventory.append(item)
class TestDenizen:
def test_initial(self):
interactor = FakeInteractor()
bee = Denizen(name='Buzz')
bee.interact(interactor)
assert 'nice flower' in interactor.kwargs['message']
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'
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
I’d like to test this in the game pretty soon, just to see what we’ve missed. But let’s see what we might want to deal with in the tests.
Here are some improvements—yes, generalizations—that we might consider before releasing the Denizen to general use.
- In the
seekingstate, the Denizen should be given a list of things to say, not just a single hint. Maybe a little story, whatever. The list would probably be said in order, one saying per interaction. - We could parameterize what the Denizen is looking for, not just
nice flower. - We should parameterize the gift, so that a Denizen can give anything … or nothing. Perhaps a denizen just says something important after being satisfied.
- We should allow for multiple things to say after receiving the sought item.
- The sayings, especially after being satisfied, might be randomized, just to keep things interesting.
As I write the above, I wonder about a Quest object, which might have
- One or more objects that must be received. “Bring me the three Stones of Throwing.”
- Different dialog depending on what has been received. “Only one more stone to go.” or “Bring me now the Stick of Fetching.”
- An optional gift of set of gifts.
- A final speech to be given.
- A final set of sayings of no consequence.
Too soon to say if we need such a thing, but let’s keep it in mind.
Let’s work on providing parameters for things to say, in order or randomly.
def test_first_sayings(self):
interactor = FakeInteractor()
initial_sayings = ['Where is it?',
'Where can it be?',
'I lost my nice flower!']
bee = Denizen(name='Buzz', initial_sayings=initial_sayings)
bee.interact(interactor)
assert 'Buzz: "Where is it?"' in interactor.kwargs['message']
bee.interact(interactor)
assert 'Where can it be?' in interactor.kwargs['message']
bee.interact(interactor)
assert 'I lost my nice flower!' in interactor.kwargs['message']
Denizen expects keyword arguments. Let’s make this test run.
class Denizen:
def __init__(self, *, name, initial_sayings=['nice flower']):
self.name = name
self.initial_sayings = initial_sayings
self.say = 0
self.state = 'seeking'
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:
saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
self.say = (self.say + 1)%len(self.initial_sayings)
interactor.publish('announce', 'xx', message=saying)
I got a bit fancy there and added the Denizen name and put the saying in quotes, in the seeking method. That’s a bit more convenient and confirms the standard that a Denizen always speaks in quotes prefixed by its name.
I am more than tired of typing .publish('announce','xx'. Interactor should be more helpful.
class Interactor:
def announce(self, message):
self.publish('announce', 'xx', message=message)
And I’ll add that to the Fake one too. And then:
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:
saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
self.say = (self.say + 1)%len(self.initial_sayings)
interactor.announce(saying)
And, while we’re at it, change the other .publish calls to use .announce. Much nicer.
I set up a Bee in main and try it in world, and learn some things.
main.py
def add_content(layout, dungeon):
factory = ContentFactory()
initial_sayings = ['Where is it?',
'Where can it be?',
'I lost my nice flower!',
'Please help me find my nice flower!']
bee = factory.bee(name='Buzz', initial_sayings=initial_sayings)
cell = Cell(33, 25)
cell.add_content(bee)
...
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)
So, in use the Bee cycles through those four sayings over and over. That’s OK but odd. I think what would be better would be after the final hint, the bee would go into a new state where it has a different set of things to say, perhaps even randomly. Either that, or it should at least keep repeating the final message. Let’s settle for that right now. Improve the test.
def test_first_sayings(self):
interactor = FakeInteractor()
initial_sayings = ['Where is it?',
'Where can it be?',
'I lost my nice flower!']
bee = Denizen(name='Buzz', initial_sayings=initial_sayings)
bee.interact(interactor)
assert 'Buzz: "Where is it?"' in interactor.kwargs['message']
bee.interact(interactor)
assert 'Where can it be?' in interactor.kwargs['message']
bee.interact(interactor)
assert 'I lost my nice flower!' in interactor.kwargs['message']
bee.interact(interactor)
assert 'I lost my nice flower!' in interactor.kwargs['message']
That fails because it cycles back to the first message. Change Denizen:
def seeking(self, interactor):
if interactor.has('flower'):
interactor.announce(message='Buzz: "Oh thank you! Have some honey!"')
interactor.receive_content('honey')
self.state = 'satisfied'
else:
saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
self.say = min((self.say + 1), len(self.initial_sayings) - 1)
interactor.announce(saying)
Green. We should be committing this stuff, I think it’s getting there. Commit: improving Denizen, not yet ready for prod.
What else do we need? Well, we can’t be giving the real Dot a string that says ‘honey’, se need a content item. TDD in a new parameter, gift. It can be None.
def test_gifts(self):
interactor = FakeInteractor()
interactor.inventory.append('flower')
bee = Denizen(name='Buzz', gift='honeycomb')
bee.interact(interactor)
assert 'honeycomb' in interactor.inventory
And:
def seeking(self, interactor):
if interactor.has('flower'):
if self.gift:
interactor.announce(message='Buzz: "Oh thank you! Have some honey!"')
interactor.receive_content(self.gift)
self.state = 'satisfied'
else:
saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
self.say = min((self.say + 1), len(self.initial_sayings) - 1)
interactor.announce(saying)
Some other tests needed to be given a gift parameter, no real surprise there. Test runs. Commit.
Hm, now it seems that we need to parameterize the gift message, or, no, for now, let’s just make it more general.
def seeking(self, interactor):
if interactor.has('flower'):
if self.gift:
interactor.announce(message='Buzz: "Oh thank you!"')
interactor.announce(message='Buzz: "Here\'s something you may need."')
interactor.receive_content(self.gift)
self.state = 'satisfied'
...
A test fails, this one, now corrected:
def test_receives(self):
interactor = FakeInteractor()
bee = Denizen(name='Buzz', gift='anything')
bee.interact(interactor)
assert 'nice flower' in interactor.kwargs['message']
interactor.inventory.append('flower')
bee.interact(interactor)
assert 'may need' in interactor.kwargs['message']
assert bee.state is 'satisfied'
Reflection
I felt the need to slow down a bit with all this parameter-adding. Clearly we’ll want a sequence to be said upon receiving the quest item, and a sequence to be said, perhaps randomly, after we are satisfied. But plugging all these things into Denizen is just adding complexity to it, and it is beginning to break tests, and we need to edit ContentFactory and so on.
Earlier I mentioned the notion of a Quest object. Maybe a Denizen’s job is to know a Quest and to present, first the hints as to what Dot is expected to provide, then a little speech upon receiving it, with a gift to Dot, and then some generalized chatter after all is said and done. But if we did that, there isn’t much left for the Denizen itself to do.
We’ve also touched on the notion that some message collections might cycle, some might stick at the end, and maybe some would randomly select something to say each time. So maybe there is an abstract class Messages, or Sayings, with a collection of strings, a name to interpolate in, and concrete classes providing a production order, sequential, cycling, random.
I think we’re not ready to invent a Quest object yet, and may never be. In any case we need to better understand what Denizens need. I do think we could work on the Sayings idea, as we can see quite clearly that we want to have a sequential list and a random list, and we can do the cycling list now or later as may make sense.
As things stand now, we can just about say that we can create any number of Denizens who want Dot to bring them something. We might see that if we want her to do something or go somewhere, we could slip her an inventory item that she can’t see but the Denizen can, so we could do quests like “Find and kiss the Frog of Intrepid Osculation”, and the Frog would slip a note into Dot’s inventory that the quest giver Denizen could detect.
All those quests, right now, are limited to a simple sequence, giving the quest info, getting the result, providing the gift, entering the satisfied but still chatty state. If we want something more complex, we’ll need to figure it out.
Let’s not generalize until we have the actual cases in hand.
And there’s another issue, which is that I need artwork for a flower and a honeycomb. I’ll see what I can find or create.
Summary
We’re inching forward, and for me, today’s work verifies for me that I wasn’t ready to generalize to some kind of magical state machine or the like. Denizen does implement a simple state machine. I don’t see a useful generalization right now, so perhaps it was wise to hold off.
It could happen. I could do something somewhat wise. It could happen.
See you next time!