Language-oriented programming

,

There’s a conventional piece of software development wisdom, to the effect that writing your own programming language is an absurd and wasteful activity, a hideous time-sink, and some existing language is almost certain to satisfy your needs perfectly well.

There’s a good deal of truth in this advice, in the case where you find yourself trying to develop a general-purpose programming language, and high performance is critical. The vast range of functionality needed to be general purpose overwhelms the capacities of a small team, and it is well known that making an optimizing compiler targeting general-purpose hardware requires an investment of many years of effort.

But if you’re not facing that situation, then I think that writing your own language has a lot to be said for it:

  1. Languages are not just for describing algorithms to computers: they are for communication with other people. Other programmers, of course, but also non-programmers (or less expert programmers) like designers and managers. And your future self, who has forgotten the details of the implementation. If the syntactic form of the language is familiar to its readers, they have a better chance of being able to understand the code and review it for correctness (even if they cannot necessarily write it).

    For example, in Mathematica, mathematical expressions are basic objects: thus, the polynomial x2 + 3x + 2 is represented by the code:

    x^2 + 3 x + 2

    Compare, for example, with the corresponding representation in Numpy:

    Polynomial([2, 3, 1])

    In the Mathematica case it’s possible for someone completely unfamiliar with the language to verify that the correct polynomial has been entered, but this is not the case in Numpy: for example, which number is the units coefficient?1

    Or consider the meaning of the following snippet of code in Inform 7. Even if you don’t know the first thing about Inform 7, you can probably guess what it means:

    The player is in Longwall Street. The player knows detect trap, memorise, fashion staff, know nature and exorcise undead. The player wears a leather jerkin. The player carries a dagger.

  2. Implementing your own language allows you to impose modelling constraints on programs, making it impossible to express certain kinds of common errors, like dereferencing of null pointers, out-of-bounds array access, or the addition of numbers representing measurements in different units.

  3. You can develop an execution model that’s completely different from the execution model of the language that it’s embedded in. It’s often much clearer and more convenient to be able to express concurrent programs in coroutine style, rather than in the form of state machines embedded in a single-threaded program (even if that’s actually how they are implemented).

  4. A higher-level language aids portability by abstracting away details of the underlying platform. You can port to new platforms by reimplementing parts of the runtime.

  5. It would be an absurd and wasteful activity to study lexers and parsers and code generators but then never actually going on to make use of them!

The name language-oriented programming is sometimes given to the technique of designing a language as the first step in modelling a problem domain. The term was coined by Martin Ward in his 1994 paper “Language Oriented Programming”, where he made some quite expansive claims for the technique:

Our experience with FermaT, and the experiences from other projects, indicate that a system implemented using the language oriented method, as a series of language levels, ends up much smaller than an bottom up or top down implementation of the same system. This is due to the fact that with a problem-specific very high level language, a few lines of code are suffcient to implement highly complex functions. The implementation of the language is also kept small since only those features which are relevant to the particular problem domain need to be implemented.

The small size of the final system means that the total amount of development work required is reduced, without increasing the complexity of the system, and for the same or higher functionality. This leads to improved maintainability, fewer bugs, and improved adaptability

To demonstrate the power of the technique, the program below is written in a language that I’ve just invented but which you most likely already know how to read, because, like me, in your mispent youth you read The Warlock of Firetop Mountain and similar choose-your-own-adventure books.

                                    PAGE 1

It is only the autumn night that makes you shiver, you tell yourself, but you
know better.

The castle stands on the hill in the gathering dusk. Wrapped in your grey cloak
against the cold, you crouch on the edge of the counterscarp. Eyes peer down
from the battlements, but you need not fear discovery: they are only the empty
sockets in the skulls of the adventurers who have come before you to harrow the
fortress of the wizard Gwydion.

Soon it will be dark enough to make your move.

You have boots.

You have a cloak.

You have a dagger.

To scale the wall, turn to page 27.

To swim the moat, turn to page 24.


                                    PAGE 2

The dagger makes no impression on the chain. “Ah, you wish to free me,” says
the monk. “You have a good heart, but no blade forged of iron can cut a chain
forged with magic.”

Suddenly he stands up. “Only Gwidion’s death can undo his magic.  But if you
wish to defeat him, take this.” He selects a tattered scroll from the shelf
above the desk and hands it to you.

Defeat Gwydion? You only meant to rob him. But you nod and take the scroll.

You have a tattered scroll.

To leave the monk and follow the stone passageway, turn to page 13.


                                    PAGE 3

You are in a long stone passageway.

To go through the doorway on the right, turn to page 33.

To follow the passageway, turn to page 13.


                                    PAGE 4

You push the door. It opens noiselessly on well-oiled hinges and you peer
through the gap. What you see inside takes your breath away. Golden coins in
heaps! Tapestries in silk and damask! Kingly crowns studded with polished
stones. This is what you came for.

To fill your pockets and make a run for it, turn to page 34.

To try the other door instead, turn to page 10.


                                    PAGE 5

To tag onto the back of the troop, turn to page 6.

To stay hidden until they pass, and then try your key in the door of the keep,
turn to page 40.


                                    PAGE 6

You follow the troop, imitating their shambolic walk as best you can. No one
seems to notice your presence. The door to the keep swings open, squealing on
its rusty hinges like a stuck pig, and you pass through one by one. The massive
door swings shut behind you.

If you have a cloak, turn to page 36.

Test your luck. If you succeed, turn to page 23.

Otherwise, turn to page 21.


                                    PAGE 7

The courtyard is quiet in the moonlight, but you remain on your guard. Who
knows what traps Gwydion has set for unwary intruders?

To approach the keep, turn to page 41.


                                    PAGE 8

Your boots echo loudly on the tiles. Too loud. You freeze, but the sound of
footsteps does not stop. You turn to see an armoured knight step emerging from
an alcove. Its visor is down, but somehow you doubt that there is a face behind
it.

        suit of armour (the, its) SKILL 10 STAMINA 10

If you win, turn to page 30.


                                    PAGE 9

It looks as if the goblin dropped a key in the struggle. You pick it up.

You gained a key.

To leave the scullery by the door, turn to page 7.


                                    PAGE 10

You push the door. It opens noiselessly on well-oiled hinges and you peer
through the gap. Inside is a study lined with tapestries, and at a desk sits a
tall white-haired man, bent in concentration over a grimoire. It is the wizard
Gwydion. His staff rests at his side.

If you have a tattered scroll, turn to page 47.

To attack Gwydion, turn to page 45.

To try the other door instead, turn to page 4.


                                    PAGE 11

You leap through the window.

Test your luck. If you succeed, turn to page 12.

Otherwise, turn to page 32.


                                    PAGE 12

It is a long fall, but the moat is deep. You pull yourself out and slink away
empty-handed into the night. Is that distant laughter you can hear? No
matter. You will be back.


                                    PAGE 13

The dark passageway enters a large hall lit by a candelabra. Suits of armour
stand silently in alcoves. The floor is tiled in a black and white checkerboard
pattern, and at the far side a wide staircase ascends. Something about the room
makes you suspicious.

To cross the hall, stepping only on the white tiles, turn to page 20.

To cross the hall, stepping only on the black tiles, turn to page 20.


                                    PAGE 14

You dart into the room and snatch the staff from the wizard’s side. He snaps
awake, and makes a grab for it, but you draw back from his reach.

“Very good, {player.name}!” he says. “But with or without my staff, I am still
the wizard Gwydion!”

To fight him, turn to page 29.

To run away, turn to page 25.


                                    PAGE 15

Two steps are all it takes to cover the distance, and then your dagger goes in
between his cervical vertebrae. The monk barely make a sound as he collapses
forward onto the desk, bleeding onto his half-copied page.

You search his cassock efficiently but find nothing. It is only then that you
notice the heavy iron chain from his leg to the desk. He was a prisoner here,
not an enemy.

To go back and follow the stone passageway, turn to page 13.


                                    PAGE 16

You place your foot on a loose cobble and it gives way. For a moment you hang
from your fingertips, but with lightning speed you find a new foothold. Soon
you swing up through a machicolation and onto the parapet walk.

To quickly descend the stairs to the courtyard, turn to page 37.


                                    PAGE 17

You take a ladleful and sip. Pfaughh! This isn’t soup, it’s laundry! There are
underclothes boiling here… and not overly clean ones.

A scraping from behind you makes you drop the ladle in alarm.  Someone is
coming through the door.

To try to sneak out past the newcomer, turn to page 22.

To stand your ground, turn to page 26.

To hide, turn to page 49.


                                    PAGE 18

You cough. The monk turns his head. “It’s nearly done,” he says, “I’m writing
as fast as I can. But with this light…” He turns back to his work. It is only
then you notice the heavy iron chain linking his leg to the desk. He is a
prisoner here!

To cut the chain with your dagger, turn to page 2.

To leave the monk to his fate and follow the stone passageway, turn to page 13.


                                    PAGE 19

You place your foot on a loose cobble and it gives way. For a moment you hang
from your fingertips, cursing the mason. And then you are gone.


                                    PAGE 20

If you have boots, turn to page 8.

Otherwise, turn to page 30.


                                    PAGE 21

You are still wet with water from your swim, and in the silence a drip echoes
in the stone passageway. The three shambling figures turn to look at you. You
wish that you had never seen the rotting flesh beneath their hoods.

          first zombie (the, its) SKILL 4 STAMINA 6

The second zombie is even more hideous than the first. It reaches for you with
the claws on its one good hand.

         second zombie (the, its) SKILL 5 STAMINA 6

The third zombie is more horrible than the first two put together.  It glares
at you with one eye dangling from its socket.

         third zombie (the, its) SKILL 6 STAMINA 6

If you win, turn to page 3.


                                    PAGE 22

Test your luck. If you succeed, turn to page 42.

Otherwise, turn to page 26.


                                    PAGE 23

The troop continues down a long stone passageway, but you scuttle through a
doorway on the right, glad to be rid of their unsettling company.

Turn to page 33.


                                    PAGE 24

You wrap your boots in your cloak and slip silently into the water. There is a
water gate, barred with iron, but the iron is rusted and crumbling below the
waterline. You take a deep breath and squeeze through into a dark underwater
passage.

Test your stamina. If you succeed, turn to page 35.

Otherwise, turn to page 46.


                                    PAGE 25

Clutching the wizard’s staff, you run out onto the landing.  Gwydion
follows. You run down the stairs. Gwydion follows. You run across the tiled
floor. Gwydion follows, his hobnailed boots ringing on the stone.

A suit of armour steps down from an alcove and swings its sword at Gwydion. He
shatters it with a blast of lightning, but a second suit of armour is at the
wizard’s back, swinging its sword. A third and a fourth step into the fray
until you can no longer see the wizard. Caught in his own trap!

Turn to page 50.


                                    PAGE 26

The newcomer is green-skinned and ugly as sin. He lunges at you with a
staff. You have no choice but to defend yourself.

            goblin (the, his) SKILL 7 STAMINA 6

If you win, turn to page 9.


                                    PAGE 27

The masonry has not been pointed in many years, and the stones are rough. You
climb swiftly and silently: an owl, perched in an arrow slit, is not disturbed
as you pass. But the wall is high.

Test your skill. If you succeed, turn to page 16.

Otherwise, turn to page 19.


                                    PAGE 28

You flourish your dagger, but Gwydion taps you with his staff and you find
yourself unable to move.

Turn to page 44.


                                    PAGE 29

Gwydion raises his hands, lightning flickering from his fingers.

            Gwydion (-, his) SKILL 10 STAMINA 12

If you win, turn to page 50.


                                    PAGE 30

You cross the hall and ascend the stairs. On the landing there are two
doors. One has a stuffed owl on the lintel, the other a lizard in a jar.

To enter the door with the stuffed owl, turn to page 4.

To enter the door with the lizard, turn to page 10.


                                    PAGE 31

This page intentionally left blank.


                                    PAGE 32

It is a long fall, and the ground is hard.


                                    PAGE 33

This is a scriptorium, with shelves stacked haphazardly with scrolls and
codices. By the light of a candle, a tonsured man sits at a writing desk,
copying a manuscript. His back is turned to you and he does not appear to have
heard him enter.

To slip back out again and follow the stone passageway, turn to page 13.

To kill the monk, turn to page 15.

To talk to the monk, turn to page 18.


                                    PAGE 34

Just one of the crowns would set you up for life. But you close your fingers on
it and it vanishes.

You hear a mocking laugh behind you, and whirl around to see the wizard Gwydion
standing there.

“Your head will make a fair adornment for my battlements, {player.name},” he
says.

If you have a tattered scroll, turn to page 48.

To jump out of the window, turn to page 11.

To attack Gwydion, turn to page 28.


                                    PAGE 35

You feel your way along the passage in the dark. Nothing but cold stone. Your
lungs are bursting: you must find a way out or drown.  With a last desperate
burst of energy you kick upwards and surface. Blessed air! You breathe it deep
into your lungs. You seem to have left your cloak and boots in the passage, but
at least you are alive.

You lost your boots.

You lost your cloak.

To haul yourself out of the water and look around, turn to page 38.


                                    PAGE 36

Are you scrutinized by unseen eyes as you pass the threshold? With your grey
cloak pulled over your head, you cannot tell.

Your luck went up by 1.

Turn to page 23.


                                    PAGE 37

The courtyard is quiet in the moonlight, but you remain on your guard. Who
knows what traps Gwydion has set for unwary intruders?

To enter a low stone building in a corner of the curtain wall, turn to page 38.

To approach the keep, turn to page 41.


                                    PAGE 38

By the flickering light of a fire, you can see that this is a scullery. Buckets
of dirty dishes stand by the water’s edge, and a giant cauldron bubbles over
the flame. There is a door in the west wall.

To drink from the cauldron, turn to page 17.

To leave the scullery by the door, turn to page 7.


                                    PAGE 39

You start to unroll the scroll, but Gwydion dashes it out of your hand with his
staff.

Turn to page 28.


                                    PAGE 40

The unsettling shambolic figures disappear into the keep and the door swings
shut behind them. You stay hidden for a quarter of an hour. Nothing moves. You
step silently across the drawbridge and approach the door.

The key grates in the lock but turns. You pull on the massive brass ring, and
the door opens, squealing on its rusty hinges like a stuck pig. You freeze, but
no one seems to have heard: or maybe they are used to the noise. You step
through and leave the door ajar.

Turn to page 3.


                                    PAGE 41

Massive blocks of well-fitted grey stone make up the walls of the keep: you can
see no way to scale the walls. Nor is there a moat with unguarded water gate:
just a ditch filled with chevaux de frise.

But what’s this? You crouch behind a pigsty as a troop of guards marches
towards the keep. Though maybe “marches” is the wrong word: the first one is
limping, the second has a hunched back, and the third drags his foot along the
ground.

If you have a key, turn to page 5.

This could be your only chance. To tag onto the back of the troop, turn to page
6.


                                    PAGE 42

You flatten yourself against the wall beside the door, and hold your breath as
the newcomer enters. He is green-skinned, ugly as sin, and carrying a staff. He
approaches the cauldron and gives it a stir, sniffing the fumes.

To sneak out while his back is turned, turn to page 7.


                                    PAGE 43

Quietly, so as not to disturb the wizard, you unroll the scroll and whisper the
words. Gradually the wizard’s head nods forward until his beard is on the desk
and you can hear him snoring.

To run in and stab him, turn to page 45.

To steal his staff, turn to page 14.


                                    PAGE 44

The wizard studies your face as you stand there paralyzed. “A most respectable
visage. I think it will look best on the western wall.”


                                    PAGE 45

Two swift steps and you plunge your dagger into his back, only for it to
shatter into pieces. Gwydion jumps up from the desk with staff in hand and taps
you with it. Suddenly you find yourself unable to move.

Turn to page 44.


                                    PAGE 46

You feel your way along the passage in the dark. Nothing but cold stone as far
as you can swim. Your lungs are bursting: you must turn back or drown. Back to
the water gate. Where is the gap? In panic you wrench at the bars, but it is
too late….


                                    PAGE 47

To read your scroll, turn to page 43.

To attack Gwydion, turn to page 45.

To try the other door instead, turn to page 4.


                                    PAGE 48

To read your scroll, turn to page 39.

To jump out of the window, turn to page 11.

To attack Gwydion, turn to page 28.


                                    PAGE 49

You crouch down behind the cauldron and watch as a goblin enters the laundry
room. He is green-skinned, ugly as sin, and carrying a staff. He approaches the
cauldron and gives it a stir, sniffing the fumes.

You leap up from your hiding place and push the goblin into the cauldron, where
he expires horribly in the boiling water and a tangle of dirty clothing.

Turn to page 9.


                                    PAGE 50

Gwydion is dead. His staff and his books are in your hands. All you have to do
now is find his gold, subdue his minions, free his slaves, and get the treasure
safely away. It should be easy now…

And here’s an interpreter for this language, written in Python.

# -*- coding: utf-8 -*-

import re
import random
import sys
import textwrap

# For compatibility between Python 2 & 3.
if sys.version_info.major >= 3:
    raw_input = input

class GameOver(Exception):
    pass

class Roll(object):
    """The object Roll('2d6+1') represents a roll of two six-sided dice,
    plus one. When converted to a string it describes the roll in
    English.

    """
    DICE_RE = re.compile(r'([1-9]\d*)d([1-9]\d*)([+-]\d+)?$')

    def __init__(self, dice):
        m = self.DICE_RE.match(dice)
        if not m:
            raise ValueError('bad dice description: {}'.format(dice))
        n, k, bonus = (int(g or '0') for g in m.groups())
        self.roll = [random.randint(1, k) for _ in range(n)]
        self.bonus = bonus
        self.total = sum(self.roll) + self.bonus

    def __str__(self):
        if len(self.roll) == 1:
            s = '{}'.format(self.roll[0])
        elif len(self.roll) == 2:
            s = '{} and {}'.format(*self.roll)
        else:
            s = '{}, and {}'.format(', '.join(self.roll[:-1]), self.roll[-1])
        if self.bonus:
            s += ', plus {}'.format(self.bonus)
        if len(self.roll) > 1 or self.bonus:
            s += ', making {}'.format(self.total)
        return s

class Player(object):
    STATS = [('skill', '1d6+6'), ('stamina', '1d6+6'), ('luck', '1d6+6')]

    def __init__(self, game):
        self.game = game
        self.name = self.game.input("What is your name? ")
        self.equipment = set()
        self.initial_stats = dict()
        self.w = lambda s: game.write(s, 4)

    def rollup(self):
        self.w("Welcome, {player.name}. Let’s roll up your character.")
        for stat, dice in self.STATS:
            r = Roll(dice)
            self.initial_stats[stat] = r.total
            setattr(self, stat, r.total)
            self.w("{}: you rolled {}.".format(stat.capitalize(), r))

    def test_stat(self, stat):
        w = self.w
        value = getattr(self, stat)
        w("Testing your {} ({}).".format(stat, value))
        r = Roll('2d6')
        if r.total > value:
            w("You rolled {}. You failed the test.".format(r))
            return False
        elif r.total == value:
            w("You rolled {}. You just made it.".format(r))
        else:
            w("You rolled {}. You passed the test.".format(r))
        if stat == 'luck':
            self.adjust_stat(stat, -1)
        return True

    def adjust_stat(self, stat, change, report=True):
        old_value = getattr(self, stat)
        new_value = max(0, min(self.initial_stats[stat], old_value + change))
        change = new_value - old_value
        setattr(self, stat, new_value)
        if report and change != 0:
            self.w("Your {} just went {} by {}. It is now {}."
                   .format(stat, 'down' if change < 0 else 'up',
                           abs(change), new_value))

class Paragraph(object):
    """`Paragraph(game, **kwargs)` represents a paragraph of instructions
    on a page. Call it to execute the instructions. The class property
    `re` is a regular expression matching all paragraphs of this type.
    The `kwargs` dictionary passed to the constructor must contain all
    the named groups in `re`.

    """
    def __init__(self, game, **kwargs):
        self.game = game
        self.w = lambda s: self.game.write(s, 4)
        for k, v in kwargs.items():
            setattr(self, k, v)

    GROUP_RE = re.compile(r'\(?P<(\w+)>')

    @classmethod
    def group(cls):
        """Return the name of the first named group in the `re` property."""
        return cls.GROUP_RE.search(cls.re).group(1)

class Fight(Paragraph):
    re = (r'(?P<monster_name>\S.*\S) \((?P<monster_article>the|an?|-),'
          r' (?P<monster_pronoun>their|his|her|its)\)'
          r' +SKILL +(?P<monster_skill>\d+) +STAMINA +(?P<monster_stamina>\d+)')

    def __call__(self):
        w = self.w
        player = self.game.player
        name = self.monster_name
        article = self.monster_article
        if self.monster_article == '-':
            fullname = name
        else:
            fullname = '{} {}'.format(self.monster_article, name)
        pronoun = self.monster_pronoun
        skill = int(self.monster_skill)
        stamina = int(self.monster_stamina)
        w('{0.name}: SKILL {0.skill} STAMINA {0.stamina}'.format(player))
        w('{}: SKILL {} STAMINA {}'.format(name.capitalize(), skill, stamina))
        while True:
            player_roll = Roll('2d6+{}'.format(player.skill))
            w("You rolled {}.".format(player_roll))
            monster_roll = Roll('2d6+{}'.format(skill))
            w("{} rolled {}.".format(fullname.capitalize(), monster_roll))
            if monster_roll.total == player_roll.total:
                w("You skirmish without damage on either side.")
                continue
            luck_adjustment = 0
            if self.game.input("Test your luck? ").lower() in ('y', 'yes'):
                luck_adjustment = 1 if player.test_stat('luck') else -1
            if monster_roll.total > player_roll.total:
                player.adjust_stat('stamina', -2 + luck_adjustment, False)
                w("{} hits you! Your stamina is now {}."
                  .format(fullname.capitalize(), player.stamina))
                if player.stamina <= 0:
                    w("You die.")
                    raise GameOver()
            else:
                stamina = max(0, stamina - (2 ** (luck_adjustment + 1)))
                w("You hit {}! {} stamina is now {}."
                  .format(fullname, pronoun.capitalize(), stamina))
                if stamina <= 0:
                    w("You killed {}.".format(fullname))
                    return

class NextPage(Paragraph):
    re = r'(?:If you win, t|Otherwise, t|T)urn to page (?P<next_page>\S+)\.'

    def __call__(self):
        return self.next_page

class TestEquipment(Paragraph):
    re = (r'If you have(?: (?:an?|your|the|some))? (?P<equip_test>\S.*\S),'
          r' turn to page (?P<equip_page>\S+)\.')

    def __call__(self):
        if self.equip_test in self.game.player.equipment:
            return self.equip_page

class TestStat(Paragraph):
    re = (r'Test your (?P<test_stat>\S+)\. If you succeed,'
          r' turn to page (?P<test_page>\S+)\.')

    def __call__(self):
        if self.game.player.test_stat(self.test_stat):
            return self.test_page

class ChangeStat(Paragraph):
    re = (r'Your (?P<stat_change>\S+) went (?P<stat_dir>up|down) by'
          r' (?P<stat_amount>\d+)\.')

    def __call__(self):
        amount = int(self.stat_amount) * (-1 if self.stat_dir == 'down' else 1)
        self.game.player.adjust_stat(self.stat_change, amount)

class GainEquipment(Paragraph):
    re = (r'You (?:gained|have)(?: (?:an?|your|the|some))?'
          r' (?P<gain_equipment>\S.*\S)\.')

    def __call__(self):
        self.game.player.equipment.add(self.gain_equipment)

class LoseEquipment(Paragraph):
    re = r'You lost(?: (?:an?|your|the|some))? (?P<lose_equipment>\S.*\S)\.'

    def __call__(self):
        self.game.player.equipment.remove(self.lose_equipment)

class Choice(Paragraph):
    re = r'(?P<choice>\S.*\S), turn to page (?P<choice_page>\S+)\.'

    def __call__(self):
        n_choices = len(self.choices)
        if n_choices == 1:
            self.w("{}, press return.".format(self.choices[0]['choice']))
        else:
            for i, c in enumerate(self.choices):
                self.w("{}, choose {}.".format(c['choice'], i + 1))
        prompt = "? "
        while True:
            inp = self.game.input(prompt)
            if n_choices == 1:
                return self.choices[0]['choice_page']
            try:
                choice = int(inp)
                if 1 <= choice and choice <= n_choices:
                    return self.choices[choice - 1]['choice_page']
            except ValueError:
                pass
            prompt = "Please enter a choice from 1 to {}> ".format(n_choices)

class Description(Paragraph):
    re = r'(?P<description>\S.*\S)'

    def __call__(self):
        self.game.write(self.description)

class Page(object):
    def __init__(self, game):
        self.game = game
        self.paras = []
        self.choice = None

    def add(self, cls, **kwargs):
        if cls == Choice:
            if not self.choice:
                self.choice = Choice(self.game, choices=[])
                self.paras.append(self.choice)
            self.choice.choices.append(kwargs)
        else:
            self.paras.append(cls(self.game, **kwargs))

    def visit(self):
        for para in self.paras:
            page = para()
            if page:
                return page
        raise GameOver()

class Game(object):
    width = 72
    para_re = re.compile(r'^(?:PAGE (?P<page>\S+)|{})$'
                         .format('|'.join(cls.re for cls in Paragraph.__subclasses__())))

    def __init__(self, text):
        self.pages = dict()
        page = None
        for para in re.split(r'\n(?:[ \t]*\n)+', text.strip()):
            para = textwrap.dedent(para).strip().replace('\n', ' ')
            m = self.para_re.match(para).groupdict()
            if m['page']:
                if page is None: self.initial_page = m['page']
                page = self.pages[m['page']] = Page(self)
                continue
            for cls in Paragraph.__subclasses__():
                if m[cls.group()]:
                    page.add(cls, **m)
                    break

    def input(self, prompt):
        answer = raw_input(prompt)
        print('')
        if answer.lower() in ('q', 'quit'):
            raise GameOver()
        return answer

    def write(self, para, left_margin=0):
        indent = ' ' * left_margin
        for line in textwrap.wrap(para.format(player=self.player),
                                  self.width, initial_indent=indent,
                                  subsequent_indent=indent):
            print(line)
        print('')

    def centre(self, text):
        self.write(text, (self.width - len(text)) // 2)

    def play(self):
        self.player = Player(self)
        self.player.rollup()
        self.centre('*')
        page = self.initial_page
        try:
            while True:
                page = self.pages[page].visit()
        except GameOver:
            pass

if __name__ == '__main__':
    Game(open(sys.argv[1], encoding='utf-8').read()).play()

  1.  Thus SymPy would be a better choice if you needed to manipulate polynomial expressions in Python.