by
tundish
2021 November 10
Wednesday morning

Note

This page explains Balladeer's classic syntax, which is no longer maintained.

Refer to a more recent article for its replacement, Balladeer lite.

Pour implementation

In the last article we took a very simple approach to 10 Green Bottles. Those bottles were just a number to us.

Balladeer wants you to create a complex world of believable characters. So from now on, I think we should all start to care a bit more about the bottles.

In this tutorial we are going to promote those bottles into the Ensemble, as if they were characters in our play.

Model behaviour

Once you begin to take a fantasy world that grew in your head, and implement it Python code, you have some problems to solve. How do you fit the one into the other?

Which idiom of Python best delivers the behaviour you need in your narrative? When I first went down this path, I was beguiled by Python's powerful multiple-inheritance system. Imagine all the ways you can mix in classes to simulate human behaviour:

class Player(Saxon, Warrior):
    ...

One very important detail defeats this approach. Python objects cannot change their type at runtime. The Frog in Act One cannot become the Prince in Act Three. There are some tricks you can play, but if you go down that route you will forever feel the friction of fighting the language.

In Balladeer, behaviour is governed by state. You can create any number of state types and store them on stateful objects:

class Actor(DataObject, Stateful):
    ...

player = Actor(name="Hengist").set_state(Ethnicity.saxon, Caste.warrior)

States are implemented as Python enumerations. Access to state is overloaded by type. The default type is integer:

>>> player.state
0
>>> player.state = 3
>>> player.state = Caste.warrior
>>> player.get_state(int)
3

Because enums can have methods, you can delegate functional behaviour to an object's state, rather than its type.

>>> caste = player.get_state(Caste)
>>> caste
<Caste.warrior: Build(luck=(3, 6), magic=(5, 8), skill=(7, 10))>

>>> caste.roll()
Build(luck=4, magic=6, skill=9)

The Glass Ensemble

Back to our bottles, then (you can see this entire code example online).

drama.py

We are going to throw away our interlude function. Instead we'll populate the Ensemble with some stateful objects to represent the bottles. Our Drama class looks like this:

class Bottles(Drama):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.population = [
            Stateful().set_state(Fruition.inception),
            Stateful().set_state(Fruition.inception),
            Stateful().set_state(Fruition.inception),
        ]

    @property
    def ensemble(self):
        return self.population

The Fruition type is built in to Balladeer. It comes into its own when used with the Gesture state machine, to which it's closely related. Gestures are an advanced topic. Your first screenplay almost certainly ought not to use them. For now, all we need to know is that Fruition.inception is an initial state, and Fruition.completion a terminal state.

We now need a method of counting the unbroken bottles, which we can implement as a property:

@property
def count(self):
    return len(
        [i for i in self.population if i.get_state(Fruition) == Fruition.inception]
    )

song.rst

The entity declarations need to change to match one unbroken bottle from the Ensemble.

.. entity:: DRAMA
    :types: balladeer.Drama

.. entity:: BOTTLE
    :types:     balladeer.Stateful
    :states:    balladeer.Fruition.inception

.. |BOTTLES| property:: DRAMA.count

The breaking of the bottle can now be achieved in the dialogue, by setting its Fruition state to completion.

Song
====

Many
----

.. condition:: DRAMA.count ([^01]+)
.. condition:: DRAMA.state 0

|BOTTLES| green bottles, hanging on the wall.

.. property:: BOTTLE.state balladeer.Fruition.completion
.. property:: DRAMA.state 1

In addition, this frees up the drama integer state for use by the dialogue as well. In fact, this is its most common application; to sequence a loop of varied dialogue until a more meaningful transition takes us elsewhere:

One
---

.. condition:: DRAMA.count 1
.. condition:: DRAMA.state 0

|BOTTLES| green bottle, hanging on the wall.

.. property:: BOTTLE.state balladeer.Fruition.completion
.. property:: DRAMA.state 1

All
---

.. condition:: DRAMA.state 1

And if one green bottle should accidentally fall,
There'll be...

.. property:: DRAMA.state 0

end.rst

We hit a slight snag at the end of the song. When there are no unbroken bottles left, this dialogue won't match the ensemble. We'll add a second scene of dialogue for that case.

.. entity:: DRAMA
    :types: balladeer.Drama

End
===

None
----

.. condition:: DRAMA.count 0

No green bottles hanging on the wall.

We don't need specific criteria in the entity declaration. It's sufficient to define the folder in this order:

drama.folder = ["song.rst", "end.rst"]

Then when the first scene is unmatched, we fall back to the second.

Bin Ends

In just a couple of tutorials, we've built a scalable structure for our Interactive Screenplay. We can add more dialogue to our folder, and more Python modules (and unit tests) as we normally would do in a coding project.

There's just one element we haven't mentioned yet, and it's an important one.

It's the parser. We'll talk about that next time.

Comments hosted at Disqus