• Stars
    star
    468
  • Rank 93,767 (Top 2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 9 years ago
  • Updated almost 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The Graph that Generates Stories

Build Status

Index

Overview
World Generator
API Documenation

StoryGraph

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.

StoryGraph World Generator

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.

How to Use

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

Grammar

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.

Basic Types

FORMAT: There is a type called {typename}.

There is a type called person. There is a type called ghost.

Type Extensions

FORMAT: A {new type} is a {base type}.

A woman is a person. A cat is an animal. A skeleton is a ghost.

Type Decorators

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.

Locations

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>.

Actors

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>.

Rules

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.

Transitions

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>.

Full Example

For a full working example look at example.txt in the root directory of this repository.

StoryGraph API Documentation

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.

Types

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'
});

Actors

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

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,
});

Generate Stories

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..."

More Repositories

1

Graph-Reply

A graph based REPL that saves to and loads from disk
C
44
star
2

nebulaDB

NebulaDB Graph Database
JavaScript
39
star
3

DiamondDB

Node.js Database
JavaScript
31
star
4

JedLang

my own language written by me
JavaScript
22
star
5

graph-friend

A simple node.js social network using Neo4js
JavaScript
21
star
6

OpalDB

A straightforward zero-dependency in-memory database
JavaScript
16
star
7

MindGraph

The (pro && inter)active graph datastore
JavaScript
15
star
8

NodePOS

A Node.js Module for Analyzing Parts of Speech
JavaScript
13
star
9

clustering-graph

Graph datastructure that automagically manages clusters
JavaScript
9
star
10

musical-graph

An experiment in animation and sound
JavaScript
7
star
11

js-combinators

Parser Combinators in JavaScript
JavaScript
6
star
12

rethink-chat

A simple chat app using rethinkDB changefeed and Pub Nub
JavaScript
5
star
13

predict-likes

Content-based recommender system for Node.js
JavaScript
4
star
14

castle-and-village

An open source text based kingdom development game
JavaScript
4
star
15

reactive-html

Experimental reactive framework
JavaScript
3
star
16

gravel-pit-music

Electronic Music Production Center
JavaScript
3
star
17

parts-of-speech

Instantaneous parts of speech analysis with Angular.js
JavaScript
3
star
18

Jump-Into-JavaScript

Change your Career, Become a Software Engineer
JavaScript
3
star
19

radix

Compact Prefix Tree in JavaScript
JavaScript
3
star
20

conceptual-chains

Chains of Concepts using Neo4j-Node-Express and Angular.js
JavaScript
3
star
21

languages

A Website for Creating and Recording Human Language
JavaScript
2
star
22

classical-chinese

Classical Chinese texts for Node.js
JavaScript
2
star
23

weirdo

A normal language that transpiles to JavaScript
JavaScript
2
star
24

classification

A short program to demonstrate the basics of document classification
JavaScript
2
star
25

adventure

Adventure: a fantasy RPG for ages 4+
2
star
26

xml-to-js

A Node.js module that translates xml into a JavaScript object
JavaScript
2
star
27

software-sutras

website for software sutras
HTML
2
star
28

perceptron-js

A simple, readable artificial neural network in JS
JavaScript
2
star
29

algo-exercises

A set of exercises for practicing algorithms inspired by real world problems
JavaScript
2
star
30

matrix-classifier

Uses matrices of markov-style predictor object to predict types of matrices
JavaScript
2
star
31

step-game-skeleton

Skeleton for a step-based game using react with hot-loading and single source data flow
JavaScript
2
star
32

edwards-book

Edwards family book
1
star
33

Trie

Trie data structure
JavaScript
1
star
34

configurable-form

React Redux Configurable Form
JavaScript
1
star
35

grid-mutations

A library for mutating grids in cool ways
JavaScript
1
star
36

teamwork

Demonstrates the power of groups using Node.js and Neo4j
JavaScript
1
star
37

MixUp

A hilarious text manipulator for FB
JavaScript
1
star
38

visual-smith

A utility for writing, saving, and displaying graphs using Node.js and D3
JavaScript
1
star
39

adventure-book

JavaScript
1
star
40

story-parser

A parser for choose your own adventure stories with optional combat system
TypeScript
1
star