by
tundish
2021 November 16
Tuesday 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.

Recap

Last time we built 10 Green Bottles in the form of a Balladeer screenplay. The structure we used is scalable to much larger projects.

It wasn't interactive, though. And that's what we will implement today. We will put control in the hands of the reader. It is they who get to control the action.

Drama faces both ways

Up until now, our Drama class has served a supporting role. It has generated the Ensemble for the screenplay, and it has defined properties which can be used in the Dialogue.

Now it has another partner to play with; the User. The real job of a drama is to mediate between user and dialogue.

The change is actually fairly simple. We will remove from the dialogue the responsibility of modifying ensemble state. We will add two interactive commands by which the user can achieve that instead.

Methods of control

The first thing to do is add a look command. That will be a way for the user to find out how many bottles are left. That command is implemented by this method on the Bottles class:

def do_look(self, this, text, presenter, *args, **kwargs):
    """
    look

    """
    self.prompt = ">"

What? This is a method whose only action is to set an attribute. It doesn't even return anything. So how by that is look achieved?

It happens in the dialogue. The criteria for singing the number of bottles have changed to this:

Song
====

Many
----

.. condition:: DRAMA.count ([^01]+)
.. condition:: DRAMA.history[0].name do_look

|BOTTLES| green bottles, hanging on the wall.

One
---

.. condition:: DRAMA.count 1
.. condition:: DRAMA.history[0].name do_look

|BOTTLES| green bottle, hanging on the wall.

Drama objects maintain a history of user commands. They are stored most recent first. The condition DRAMA.history[0].name do_look means 'was my most recent command called do_look?' If so, the dialogue tells you how many bottles there are.

Chains of command

But how is the method do_look associated with the command look? That occurs by placing the string 'look' in the docstring of the do_look method.

Docstrings are a Python mechanism for explaining the purpose of a piece of code. Balladeer extends this approach to define the syntax of user commands.

This system is enormously powerful. You will be able to parse highly complex phrases and interpret them reliably every time. That's an advanced topic for a later date though. We are going to keep it simple today.

Breaking changes

Here's the second method to add. This one breaks a bottle when the user enters 'bottle' or 'break'.

def do_bottle(self, this, text, presenter, *args, **kwargs):
    """
    bottle
    break

    """
    try:
        self.unbroken[0].state = Fruition.completion
    except IndexError:
        pass

And here's the scrap of dialogue which reacts to that:

All
---

.. condition:: DRAMA.history[0].name do_bottle

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

.. property:: DRAMA.prompt Type 'look' to check the damage >

Notice how the dialogue can modify the user prompt. It's also possible to change the descriptions of ensemble objects this way too.

Keeping active

You can control which commands are available to the user at any time. One command can activate or deactivate another. This is achieved by populating the active set of the drama object with the methods allowed.

So we have two or three more lines to initialize our drama object now:

class Bottles(Drama):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ...

        self.active.add(self.do_bottle)
        self.active.add(self.do_look)
        self.prompt = ">"

All that's required to process user commands is a couple of lines to prompt for input, and then to take the appropriate action. And that's all handled for you by the deliver method of the Drama class.

cmd = input("{0} ".format(story.context.prompt))
text = story.context.deliver(cmd, presenter=presenter)

Summary

In Balladeer, the Drama and the Dialogue each have an important role. The design goal is to keep logical functionality within Python code, and maintain character dialogue as screenplay markup.

At this point you should study the example directory so that this approach becomes familiar to you.

Try running the example:

cd examples/04_drama_parser
~/balladeer-app/bin/python drama.py

Here's the output from a typical session:

> look
3 green bottles, hanging on the wall.

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

Type 'look' to check the damage > look
2 green bottles, hanging on the wall.

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

Type 'look' to check the damage > look
1 green bottle, hanging on the wall.

> break
No green bottles hanging on the wall.

In the next article, we'll take this interactive sing-song, and perform it over the Web.

Comments hosted at Disqus