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

I feel some tiny objects coming on. I plan to let them come. Tiny objects FTW.

I’m tentatively planning some hand objects for Denizens that say things. I have a few notions in mind:

  1. A sequence of sayings that are spoken once to the end and then the last one repeats if more requests are made.
  2. A sequence that cycles from beginning to end to beginning to end.
  3. A sequence that produces a random saying at each reference.
  4. A wrapper for the above that prefixes the string with a name and quotes the text.

Begin with a test.

class TestSaying:
    def test_say_sequence(self):
        seq = SequenceSayer(['a', 'b', 'c'])
        assert seq.saying == 'a'
        assert seq.saying == 'b'
        assert seq.saying == 'c'
        assert seq.saying == 'c'

And a class.

class SequenceSayer:
    def __init__(self, messages):
        self.messages = messages
        self.index = 0

    @property
    def saying(self):
        message = self.messages[self.index]
        self.index = min(self.index + 1, len(self.messages)-1)
        return message

And a test:

    def test_cycle(self):
        seq = CycleSayer(['a', 'b', 'c'])
        assert seq.saying == 'a'
        assert seq.saying == 'b'
        assert seq.saying == 'c'
        assert seq.saying == 'a'

And a class:

class CycleSayer:
    def __init__(self, messages):
        self.messages = messages
        self.index = 0

    @property
    def saying(self):
        message = self.messages[self.index]
        self.index = (self.index + 1)%len(self.messages)
        return message

We should be committing this. Do so. Let’s have an abstract class for these things.

class Sayer(ABC):
    @property
    def saying(self):
        return None

class SequenceSayer(Sayer):
    def __init__(self, messages):
        self.messages = messages
        self.index = 0

    @property
    def saying(self):
        message = self.messages[self.index]
        self.index = min(self.index + 1, len(self.messages)-1)
        return message

class CycleSayer(Sayer):
    def __init__(self, messages):
        self.messages = messages
        self.index = 0

    @property
    def saying(self):
        message = self.messages[self.index]
        self.index = (self.index + 1)%len(self.messages)
        return message

And a test:

    def test_random(self):
        sayings = ['a', 'b', 'c']
        seq = RandomSayer(sayings)
        for _ in range(20):
            assert seq.saying in sayings

And a class:

class RandomSayer(Sayer):
    def __init__(self, messages):
        self.messages = messages
        self.length = len(self.messages)

    @property
    def saying(self):
        return self.messages[randrange(self.length)]

Green. Commit.

And a test:

    def test_name_sayer(self):
        sayings = SequenceSayer(['a'])
        seq = NameSayer('Buzz', sayings)
        assert seq.saying == 'Buzz: "a"'
        assert seq.saying == 'Buzz: "a"'

And a class:

class NameSayer(Sayer):
    def __init__(self, name, sequence):
        self.name = name
        self.sequence = sequence

    @property
    def saying(self):
        return f'{self.name}: "{self.sequence.saying}"'

Green. Commit.

Now let’s have some convenience methods:

    def test_convenience_sequence(self):
        sayings = ['a', 'b', 'c']
        seq = NameSayer.sequence('Buzz', sayings)
        assert seq.saying == 'Buzz: "a"'
        assert seq.saying == 'Buzz: "b"'
        assert seq.saying == 'Buzz: "c"'
        assert seq.saying == 'Buzz: "c"'

And:

class NameSayer(Sayer):
    @classmethod
    def sequence(cls, name, sayings):
        return cls(name, SequenceSayer(sayings))

Commit. Another:

    def test_convenience_cycle(self):
        sayings = ['a', 'b', 'c']
        seq = NameSayer.cycle('Buzz', sayings)
        assert seq.saying == 'Buzz: "a"'
        assert seq.saying == 'Buzz: "b"'
        assert seq.saying == 'Buzz: "c"'
        assert seq.saying == 'Buzz: "a"'

And, of course:

class NameSayer(Sayer):
    @classmethod
    def cycle(cls, name, sayings):
        return cls(name, CycleSayer(sayings))

Commit. And one more:

    def test_convenience_random(self):
        sayings = ['a', 'b', 'c']
        long_sayings = [f'Buzz: "{s}"' for s in sayings]
        seq = NameSayer.random('Buzz', sayings)
        for _ in range(20):
            assert seq.saying in long_sayings

class NameSayer(Sayer):
    @classmethod
    def random(cls, name, sayings):
        return cls(name, RandomSayer(sayings))

Green. Commit. Move the sayings classes to src, all in one file, ‘sayer.py’. Green. Commit.

Now let’s use these new classes in the Denizen. First the tests, I guess.

I think the Denizen is allowed to know that its initial messages are a sequence. So:

class Denizen:
    def __init__(self, *, name,
                 initial_sayings=['nice flower'],
                 gift=None):
        self.name = name
        self.initial_sayings = NameSayer.sequence(self.name, initial_sayings)
        self.gift = gift
        self.say = 0
        self.gift = gift
        self.state = 'seeking'

ANd we need to fix up the seeking method, which is:

    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'
        else:
            saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
            self.say = min((self.say + 1), len(self.initial_sayings) - 1)
            interactor.announce(saying)

And now it can be:

    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'
        else:
            interactor.announce(self.initial_sayings.saying)

I see from the code that we had chosen to have Buzz cycle his messages. Let’s go with that.

class Denizen:
    def __init__(self, *, name,
                 initial_sayings=['nice flower'],
                 gift=None):
        self.name = name
        self.initial_sayings = NameSayer.cycle(self.name, initial_sayings)
        self.gift = gift
        self.say = 0
        self.gift = gift
        self.state = 'seeking'

Buzz works in the dungeon. The tests all run too, which makes me a little suspicious about them. Let’s take a quick look. Ah, one did fail when I changed ty cycle. With the test renamed and changed:

    def test_first_sayings_cycle(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 'Buzz: "Where is it?"' in interactor.kwargs['message']

So those little objects have improved Denizen just a bit, releasing it from the need to handle the indexing and the name-plugging. We can, at our leisure, make similar changes to the other messages, which are all presently ad-hoc, not provided as parameters at all.

Let’s sum up.

Summary

So that was a bit of a flyer, those four classes without a clear need for them in the code. I would argue that the need for two of them was in fact there in the code, in these lines:

saying = f'{self.name}: "{self.initial_sayings[self.say]}"'
self.say = min((self.say + 1), len(self.initial_sayings) - 1)
interactor.announce(saying)

We see here:

  1. An indication that there will be a lot of name-plugging, which should be centralized. Thus NameSayer.
  2. An indication that we need to cycle messages, thus CyclerSayer.
  3. We have switched back and forth from stopping at the last message to cycling. Thus SequenceSayer.
  4. The random one we’ve speculated about but if you want to call that one speculative, I’ll go along with you. And then I’ll use it just because I’m that kind of person.

So maybe not so speculative after all. And if it was … it’s not the worst thing I ever did.

More to a useful point, we see here how some very small objects, quite easy to produce, can make the work of other objects simpler, more clear, and a bit less procedural. And that’s a good thing.

See you next time!