Overview
World Generator
API Documenation
The Graph that Generates Stories
StoryGraph is a library that allows you to generate a narrate based on random interactions between actors in a world. StoryGraph provides classes for actors, types, and rules that can model interactions between different classes of entities. Rules can create new actors, remove actors from the world, and move actors between locations. StoryGraph will match actors to rules with any combination of randomness and specificity for a given number of steps and render the result in an English narrative.
Story graph is inspired by programming interactive worlds with linear logic by Chris Martens although it doesn't realize any of the specific principles she develops in that thesis.
My own worlds are available in the /examples directory. You can run them directly with node.js:
$ node examples/forest.js
You will see output something like this:
The river joins with the shadow for a moment. The river does a whirling dance with the shadow. A bluejay discovers the river dancing with the shadow. A bluejay observes the patterns of the river dancing with the shadow. A bluejay dwells in the stillness of life. A duck approaches the whisper. A duck and the whisper pass each other quietly.
This project is licensed under the terms of the MIT license.
The StoryGraph world generator translates a plain english description of a StoryGraph world into a working StoryGraph program. StoryGraph objects, especially rules, can be cumbersome to write out by hand, so this program allows you to easily define your world and generate the code automatically. The program generated by the World Generator may be somewhat dull, but since the boilerplate is all there it will be easy to go in and make modifications and additions to add color to your StoryGraph world.
First, write a description of your world using the grammar below and save it to disk. Go to the root directory of StoryGraph in your console and use the following command:
node generateWorld path/to/description.txt myWorld.js
The second parameter is the output file name and it is important that it has a .js extension. Now you can modify your world as you see fit. Generated worlds automatically console.log a four step story so you can immediately test you world like this:
node myWorld.js
Here is the grammar of the world generator. Note that the formats provided here are not flexible. Only the parts inside curly braces may be replaced with your custom text.
FORMAT: There is a type called {typename}.
There is a type called person. There is a type called ghost.
FORMAT: A {new type} is a {base type}.
A woman is a person. A cat is an animal. A skeleton is a ghost.
FORMAT: Some actors are {typename}.
OPTIONAL FORMAT: Some actors are {type one} and some are {type two}.
Some actors are smart and some are stupid. Some actors are scary.
FORMAT: There is a place called <{place name}>.
There is a place called <the red house>.
There is a place called <the field of wheat>.
Note that the placeholder {type} here may be a basic type or extended type preceded by any number of type decorators. After the actor's description there is a second clause listing out what locations this actor may enter. You may list any number of locations separated by the word "or". Note that only actors with multiple locations can enter into transitions that describe their change of location.
FORMAT: There is a {type} called {name}, he/she/it is in/on <{place}>. OPTIONAL FORMAT: There is a {type} called {name}, he/she/it is in/on <{place}> or <{place two}>.
There is a ghost called Slimer, it is in <the red house>.
There is a smart kind man named Joe, he is in <the red house> or <the field of wheat>.
There is a beautiful woman named Angelina, she is in <the red house> or <the law office>.
Again, the placeholder {type} may be preceded by any number of decorators.
FORMAT: If a {type one} <{encounter text}> a {type two} then the {type one||two} <{result text}>.
OPTIONAL FORMAT: If a {type one} <{encounter text}> a {type two} then the {type one||two} <{result text}> the {type one||two}
If a boy <is startled by> a ghost then the boy <starts to cry>. If a man <sees> a ghost then the man <stares in disbelief at> the ghost.
FORMAT: From <{place one}> to <{place two}> the {type or actor} <{does something}>.
From <the red house> to <the field of wheat> the man <goes out to>.
From <the sky> to <the field of wheat> the bird <swoops down into>.
For a full working example look at example.txt in the root directory of this repository.
All of the StoryGraph classes and constants are accessible via the file in dist/story.js
which is generated by webpack. To generate this file run the following commands:
$ npm install
$ webpack
##World
The story is generated by the main World
class. To create a story, you must populate your world with actors, rules, and optionally locations. First, instantiate a world:
const World = require('story-graph').World;
const world = new World({
logEvents: true // log rule matches as they happen
excludePrevious: true // prevent matching on the same rule twice in a row
})
Next create types, actors and rules to populate your world.
The first step is to define types. Types are how the story graph engine determines whether or not an actor matches a given rule. While it is possible to make rules that apply to specific actors, you'll probably want to make general rules that apply to classes of actors, and for that you use types. First the basics:
const Type = require('story-graph').Type;
// we can start with a basic type and extend it with more specific types
const person = new Type('person');
// An actor with the adult type will match rules for either "person" or "adult"
const adult = person.extend('adult');
// An actor with the man type will match rules for "person", "adult" and "man"
const man = adult.extend('male');
const smart = new Type('smart');
const cunning = new Type('cunning');
const spy = new Actor({
type: smart.extend(cunning).extend(person),
name: 'the spy'
});
Making actors is straightforward: they take a type which defines what rules they can match, and a name which is used to create the narrative output. Actors can also be given a lifetime; after the graph goes through a number of time steps equal to the lifetime of an actor, the actor will be removed from the graph. Here are some basic examples:
const Actor = require('story-graph').Actor
const bob = new Actor({
type: smart.extend(man),
name: 'Bob'
});
const sally = new Actor({
type: smart.extend(woman),
name: 'Sally'
});
You can add actors to the world one by one or as an array of actors. You can save the id
of an actor for later reference by adding them individually and saving the return value.
const World = require('story-graph').World
const world = new World()
const bobId = world.addActor(bob)
const sallyId = world.addActor(sally)
world.addActor([tim, larry, david, moe])
Rules are added via the addRule method on the world, and have the following structure:
world.addRule({
cause: { type: [A, B, C], template: [A, B, C] },
consequent: { type: [A, B, C], template: [A, B, C] } | null,
isDirectional: boolean,
locations: [],
mutations: function(source, target){
// type mutations for example:
target.type.add('newType');
},
consequentActor: {
type: type,
name: "name"
}
});
Cause
Cause is a description of the event that triggers the rule. The cause type has the following structure:
[ (id | Type), Event, (id | Type | undefined ) ]
where id is the id of an actor in the world, event is one of the events described in ./src/constants.js, and type is an instance of Type. Note that the first position in the cause type is referred to as the "source" and the third position as "target". It helps to think of it as describing vertex-edge->vertex structure in a directed graph.
The cause template is an array that can be any mix of strings and references to the source and target that triggered the rule. Constants are provided that allow you to refer to source and target. Here is an example cause:
const c = require('story-graph').constants;
const cause = {
type: [ dancer, c.ENCOUNTER, dancer ],
template: [ c.SOURCE, 'dances with', c.TARGET]
}
When the cause is matched with actors StoryGraph will replace the source and target constants in the template portion of the cause with the name of the actors. That means if actors named "Bob" and "Sally" are matched with the rule above, StoryGraph will add "Bob dances with Sally." to the output.
consequent
The type property of consequent is different from the type property of cause: it is the type of event that is triggered by the rule as a consequence of the rule being matched, like a chain reaction. If you want a rule that results in an actor being removed from the world you can use constants.VANISH in the consequent type, but all other action types will trigger a search for a matching rule. The template of the consequent is any mix of strings and references to the actors that triggered the rule. The consequent template will produce a string describing the outcome of the interaction when the rule is triggered.
This distinction is important: The consequent type describes an event that will trigger a rule after the current rule is done, the consequent template describes a sentence that will be rendered by the current rule as part of its execution. This means that a rule can render both the cause and effect of an event as output but it can also serve as the cause of another rule. Here are two examples, one to match the cause given above and one to demonstrate the vanish constant:
consequent: {
type: [], // this rule doesn't trigger anything else
template: ['The crowd admires the dancing skill of', c.SOURCE, ' and ', c.TARGET]
}
consequent: {
type: [ c.SOURCE, c.VANISH ], // this rule removes the source from the graph
template: [ c.SOURCE, 'disappears into thin air' ]
}
directionality
The isDirectional property must be set to tell the graph how to match rules. If this property is set to false then the order of the actors will be ignored when finding a match. When the source and target are qualitatively different actors and the action is truly directional you should set this property to true.
mutations
If you want to mutate the actors involved in an event you can add a mutations function to your rule. The mutations function takes the source actor and target actor as parameters. This allows you to alter the types of actors as a part of the consequence of the rule being activated, for which you can use the remove, add, and replace helpers on the actors type:
{
cause: [ handsome.extend(boy), meets, pretty.extend(girl) ],
consequent: [ c.SOURCE, 'starts dating', c.TARGET ],
isDirectional: false,
mutations: function(source, target){
source.type.replace('single', 'dating');
target.type.replace('single', 'dating');
}
}
consequent actor
We've already seen how a rule can trigger another event, but a rule can also create a new actor in the world. If you want a rule to produce an actor add the actor's definition in the consequentActor property of the rule. Consequent actors have a couple of special properties: first they can have members. Because the consequent actor is a product of some other set of specific actors triggering a rule it makes sense that those actors might merge or compose to create the consequent actor. Moreover, the name of the consequent actor might involve the names of the actors that triggered the rule, so there is an initializeName function that takes the instance of the new actor and the world instance as parameters and returns a string that will be set as the name of the new actor instance. This allows you to use the members of the new actor to set the name. Here is a full rule example:
world.addRule({
cause: {
type: [smart(person), c.ENCOUNTER, smart(person)],
value: [c.SOURCE, 'meets', c.TARGET]
},
consequent: {
type: [],
value: [c.SOURCE, 'and', c.TARGET, 'start chatting']
},
isDirectional: false,
consequentActor: {
type: casual(discussion(gathering)),
name: 'having a discussion with',
members: [c.SOURCE, c.TARGET],
lifeTime: Math.floor(Math.random()*3),
initializeName: (actor, world) => `${this.members[0]} ${this.name} ${this.members[1]}`
}
}
});
If this rule was matched with two actors "Bob" and "Tom" it would produce the following output:
"Bob meets Tom. Bob and Tom start chatting."
And it would create a new actor with the name:
"Bob having a discussion with Tom"
locations Locations are an optional feature of StoryGraph that add complexity and narrative possibilities. I think of locations as named graphs. When you add a location to your StoryGraph world, you are saying that there is a specific named place where actors can reside and where specific rules may apply.
Add locations like this:
const world = new StoryGraph.World();
world.addLocation({ name: 'the house'});
world.addLocation({ name: 'the garden'});
When creating actors you can provide a list of possible locations for that actor and an optional starting location.
const Bob = world.addActor(new Actor({
type: human,
name: 'Robert',
locations: ['the house', 'the garden'],
location: 'the house' // defaults to first location in the locations array
}));
There are three ways to use locations in your StoryGraph rules. First of all, if you have an actor that can exist in multiple different locations you can create rules to model movement between those locations.
world.addRule({
cause: {
type: [human, c.MOVE_OUT, 'the house'],
template: ['']
},
consequent: {
type: [c.SOURCE, c.MOVE_IN, 'the garden'],
template: [c.SOURCE, 'walks out into the garden']
},
})
world.addRule({
cause: {
type: [human, c.MOVE_OUT, 'the garden'],
template: ['']
},
consequent: {
type: [c.SOURCE, c.MOVE_IN, 'the house'],
template: [c.SOURCE, 'enters the house']
},
})
As you can see from these examples we have special constants MOVE_OUT
and MOVE_IN
for modeling location transitions. An actor will only match a location transition if it is currently in the location indicated in the rule type with MOVE_OUT
, and if the location indicated in the type with MOVE_IN
is contained in the possible locations of that actor. So, for the first example above, an actor will match the rule if its current location is "the house" and if "the garden" is contained in its potential locations.
The second way to use locations is to localize your rules. Some rules might describe events that make sense if they happen in one location but not in another. To configure this, simply add a locations property to the rule with an array of locations where that rule can occur. Here is an example of a rule I wrote that is localized:
world.addRule({
cause: {
type: [human, c.ENCOUNTER, ghoul],
value: [c.SOURCE, 'sees', c.TARGET]
},
consequent: {
type: [],
value: [c.SOURCE, 'turns pale and runs away']
},
locations: ['the graveyard'],
isDirectional: true,
mutations: null,
});
Finally, the event constant REST
can be used to make rules to render an actor's response to being in a location. Below is an example of a REST
rule, notice how there is no target in the cause type because the source is interacting with their location in this instance. There is also no consequent, which is optional:
world.addRule({
name: 'rest:house',
cause: {
type: [person, c.REST],
template: [c.SOURCE, 'paces around the house anxiously'],
},
locations: ['house'],
consequent: null,
isDirectional: true,
});
To generate a narrative use the runStory
method on the world object. This method takes a number which determines how many time steps the graph will run for and an optional array of events that happen at specific time steps. Events have the following structure:
{
step: 1 // this is the time step when this event will be rendered
event: [ (id || Type), Event, (id || Type) ] //this is the type of the rule to be rendered
}
At each time step the runStory method will check to see if there is an event set for that step, and if not it will generate a random event. The result will be stored on the output property of the world object.
world.runStory(4, [
{ step: 1, event: [ bob, c.ENCOUNTER, tom ]},
{ step: 4, event: [ bob, c.MOVE_OUT, cafe ]}
]);
console.log(world.output); // "Bob meets Tom..."