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

I’m tired of this program, or at least the work we’ve been doing. What might be more interesting? Definitely contains bees. Possibly important summary.

A huge fraction of any program, even just pretend ones like our dungeon, is tedium. There’s just a lot of nitty-gritty repetitive not very interesting work that has to be done. In these exercises, I try to limit that as much as I can, both because the ideas to explore and techniques to write about don’t really have boredom as a priority, and because I am not doing this for pay and I get to work on whatever I want to.

I’m sure there are plenty of opportunities to refactor in the Dungeon, and some of those might be interesting. And we really ought to put the current zoomable dungeon view into a section. We can look at those and see if anything grabs me.

Dot has an inventory but there is no display of what she has. We could add a Section to the window showing her inventory. Looking further out, Dot will have attributes. Since there is no violence in this dungeon, her attributes might still include strength, but not hit points. Perhaps she will get tired, and if she gets too tired, she falls asleep and wakes up lost somewhere in the dungeon. Perhaps we’ll track wear and tear on her Louboutins. We’ll need some kind of bar graph or something like that for the attributes. Another Section, most likely. That idea doesn’t grab me.

We’ll need more treasures, which will be mostly tedious and repetitive. We need to spread it around the dungeon, mostly randomly. There are some interesting issues there: if we have doors, we need to keep the keys to the doors on the right side relative to Dot.

We surely want Dungeon Denizens, people and creatures to encounter. In a conventional dungeon most of these encounters come down to battle. We don’t want battle. So maybe the snake is afraid of the bird, the ghost runs from the bright lamp, the frog will turn into a prince if kissed, and so on. This area is somewhat interesting, in that we’ll surely start with specific Denizens and behaviors and then look for ways to generalize them, to make creating them easier and perhaps more declarative.

There might be Quests: Perhaps the Cat Girl has lost her kitten and,if Dot finds it, will give Dot a resurfacing kit (get it?) for the red soles of her Louboutins. Perhaps the Pirate will steal Dot’s things and she has to find his trove and steal it back. Perhaps the Engineer can be induced to provide the missing girder for the bridge across the chasm, if we offer him an interesting tool that we found somewhere.

Quests seem to be much like simple denizens, just with more state. (Assume lots of hand-waving right here.) There are at least two aspects to Denizens and Quests: the placement of the components, which is much like the placement of treasures, and the maintenance of state in the Denizen, recognizing and responding to Dot and her inventory and state.

OK, Denizens and Quests might be interesting. Let’s do a simple one and then see what we see.

To interact with a Denizen, let’s say that Dot’s driver types space. That will cause the system, I don’t know where the code will reside yet, to pick a denizen adjacent to Dot (hopefully only one) and interact with it. Let’s do something specific.

We’ll put some Denizen in a room somewhere. It wants some thing. If Dot interacts when she has the thing, the Denizen thanks her, and takes the thing and gives Dot some other thing. If Dot interacts when she does not have the thing, the Denizen says “don’t bother me, I’m looking for the thing”.

Let’s see what we can figure out about how to do that. Is a Denizen just a kind of Content, or is it a completely different kind of thing? Let’s review how Content works now.

The ContentFactory makes Content items. A simple example is a ReceivableContent: the things that you get when you interact:

class ContentFactory:
    def receivable(self, *, name, resource, scale):
        def interaction(self, interactor):
            interactor.receive_content(self)
            interactor.publish('announce', 'xx', message=f'You have received {self.name}!')
            return True
        return Content(name=name,
                       resources=[resource],
                       scale=scale,
                       interaction=interaction)

So a Content has an interaction that is called at appropriate times. It appears not to receive Dot: there is no Dot object, but does get to talk with an Interactor. Let’s first look at Content:

class Content:
class Content:
    def __init__(self, *, name, resources, scale,
                 interaction=lambda self, interactor: True,
                 info=None,
                 subs=None):
        if subs is None:
            subs = []
        self.name = name
        self.resources = resources
        self.scale = scale
        self.state = 0
        self.info = info
        self.subs = subs
        self.interact_with_player = types.MethodType(interaction, self)
        self.dungeon = None

    def run(self, pub_sub):
        for sub in self.subs:
            sub.callback = types.MethodType(sub.callback, self)
        pub_sub.subscribe_all(self.subs)

The Content has the interact_with_player function, and can subscribe to events. What about Interactor?

class Interactor:
    def __init__(self, dungeon, pub_sub, cell):
        self.dungeon = dungeon
        self.pub_sub = pub_sub
        self.cell = cell

    def interact(self):
        # explicit loop because python `all` short-circuits
        all_ok = True
        for item in self.cell.contents().copy():
            # copy because we may remove content in this loop
            all_ok = all_ok & item.interact_with_player(self)
        return all_ok

    def publish(self, event, caller_id, *args, **kwargs):
        self.pub_sub.publish(event, caller_id, *args, **kwargs)

    def receive_content(self, item):
        self.dungeon.receive_content_from_cell(item, self.cell)

OK … I think we create an interactor every time Dot moves (we might have to have her try to move onto a Denizen until we sort out the space bar idea). And the interactor loops over all content in the cell, calling its interact_with_player callback. It supports publish and receive_content for use by the Content. We’ll need to extend those methods as we do, I suspect.

It seems to me that a Denizen might be little more than a Content item that moves. I think we have the ability to provide state information, called info in content, such as the rather complex spikes:

class ContentFactory:
    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)

        def control(self, *, pub_sub, state):
            try:
                self.info.cycling, self.state = self.info.cases[state]
                pub_sub.publish('state_number', self.name, content=self, state=self.state)
            except KeyError:
                return
        control_sub = Subscription(event='control', caller_id=name, callback=control)

Let’s have a bee:

class ContentFactory:
    def bee(self, *, name):
        return Content(name=name, resources=['bee.png'], scale=0.5)

main.py
def add_content(layout, dungeon):
    factory = ContentFactory()
    item = factory.bee(name='Buzz')
    cell =  Cell(33, 25)
    cell.add_content(item)
    item = factory.receivable(name="a red key", resource='keyRed.png', scale=0.5)
    cell = Cell(29, 28)
    cell.add_content(item)
    ...

We get a bee:

dungeon with bee toward lower right

Perfect. Let’s slowly elaborate what happens when Dot tries to step on the bee.

class ContentFactory:
    def bee(self, *, name):
        def bee_behavior(self, interactor):
            interactor.publish('announce', 'xx', message='Buzz: "I wish I had a nice flower."')
            return False
        return Content(name=name, resources=['bee.png'],
                       scale=0.5, interaction=bee_behavior)

That should do the job, and it does.

dungeon with bee saying it wants a flower

Of course, every time Dot bumps the bee it says the same thing. We can elaborate that, but let’s reflect a bit about what we know and what we might do.

Reflection

Eight lines of code and we have a Denizen. Not a very interesting one, but a Denizen nonetheless. And he seems to be giving Dot a small Quest, to find a flower and, presumably, bring it back.

Now we could code up all the interaction right here in the ContentFactory, but that seems awkward. I think it’d be better to build a small class, Bee, put that into the Content’s info, and defer all the action to that class. Then we could more readily TDD that class’s behavior, and we might well find ourselves with a class that could be extended to other state-driven Denizens, such as Fish or Worms. Or Pirates.

So let’s do some Wishful Thinking now and gin up a tiny Bee class.

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

We stuff a Bee instance into the standard info and defer to it. We just need a bee class.

Before I even create it, I decide to name it Denizen. A bit optimistic but it feels better. We can change it: that’s our profession.

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

And that works, no surprise. The message comes out just like before, but now from the Denizen class, not the inline code in the Content.

Now, unless I miss my guess, we can put all the state information into Denizen and make it behave appropriately, at least for Denizens that expect you to bring them something. We’ll probably have to do something more elaborate for more complex behavior and we may regret using up that really good name Denizen, in which case we’ll perhaps call it SimpleDenizen.

I suspect that the setup for bee will be similar for any Denizen with simple quest behavior, and we’ll probably get some duplication and probably do something about it. This, too, is our profession.

Next time, I guess, we’d better put a flower in the dungeon and have the Bee gratefully receive it. Small steps, always. Three, five, eight, a dozen lines at a time, from not working to working. This, this, is our profession.

Summary Observation

In my earlier days, in fact for the bulk of my long career, faced with a problem like this one:

There must be Denizens in the Dungeon and they can offer Quests and carry on State-Driven Conversations with Dot, giving Hints and Allegations, taking Things from her and giving Other Things to her …

I would have insisted that we would have to figure out and design a General Scheme for implementing those Denizens and their perhaps Arbitrarily Complex Behavior, and we would have consumed much time and brainpower devising and implementing a DenizenFramework, upon which to build a Bee and other such things. And we’d have done a pretty good job of it too, although it would surely need some changes as the Rubber met the Road. And often, they didn’t even cancel the project and fire all of us because it wasn’t done and looked like never getting done.

Today, totally counter to the conventional wisdom, we just typed in a simple Bee, right in the content code. Then we refactored to have a still very simple Denizen class that only knows how to say “Buzz …”. The exact opposite of a General Scheme. And, as we continue, I predict with confidence, a General Scheme will start to emerge, bit by bit, each new capability being driven by actual need, and emerging just before, or often just after we need it.

This is completely backward from the way I was originally taught to do things … and in my experience with “Agile” ideas, it is far better. I try to demonstrate that difference in all the work I do here. Today, I just wanted to underline it. And in the days going forward, we’ll see whether we “get away with it”. I am confident that we will. It’s not getting away with anything: in it incremental design and development, a real thing, unlike the other thing, and, in my view, a better thing.

See you next time!