Procedural Narrative
Stories emerge from graphs when weighted choices walk the structure.
A procedural narrative system generates stories by traversing a directed graph of narrative beats. Each node is a moment; each edge is a possible continuation with a weight. When you walk the graph making weighted random choices at each branch point, distinct stories emerge — sometimes tens of thousands from a graph you could draw on a napkin. The combinatorics of branching structures is where small systems hide enormous possibility spaces.
The Story Graph
A story graph is a directed acyclic graph where nodes represent narrative beats and edges represent possible transitions. Each edge carries a weight — the probability that a random walk will follow that branch. The graph structure encodes what can happen; the weights encode what tends to happen.
const graph = {
nodes: {
'call': { text: 'The call arrives', x: 0.2, y: 0.2 },
'refuse': { text: 'The hero refuses', x: 0.1, y: 0.45 },
'accept': { text: 'The hero accepts', x: 0.35, y: 0.45 },
// ...
},
edges: [
{ from: 'call', to: 'refuse', weight: 3 },
{ from: 'call', to: 'accept', weight: 7 },
// ...
]
};
Weighted Random Walks
When generating a story, the system walks from node to node, choosing which edge to follow based on weights. A node with three outgoing edges of weights 3, 5, and 2 has a 30%, 50%, and 20% chance of each branch. Higher weights make certain paths more probable, shaping the distribution of generated stories.
function weightedChoice(edges) {
const total = edges.reduce((sum, e) => sum + e.weight, 0);
let r = Math.random() * total;
for (let edge of edges) {
r -= edge.weight;
if (r <= 0) return edge;
}
return edges[edges.length - 1];
}
Counting All Paths
The total number of possible stories is the number of distinct paths from start to end. For a directed acyclic graph, this can be computed efficiently with dynamic programming or recursion: the number of paths from any node is the sum of paths from each of its successors.
function countPaths(node) {
if (!graph.adj[node].length) return 1; // Terminal node
let total = 0;
for (let edge of graph.adj[node]) {
total += countPaths(edge.to);
}
return total;
}
Combinatorial Explosion
Small graphs often hide enormous story spaces. The mystery graph above has 14 nodes and 16 edges, but produces dozens of distinct narratives. The romance graph generates hundreds. The hero's journey, despite being a simple template, branches enough that adding just a few more nodes or edges can multiply the story count by orders of magnitude. Each new branch point is multiplicative.
The DAG Constraint
Story graphs must be directed acyclic graphs — no cycles allowed. A cycle would permit infinite-length stories and break the path-counting logic. The DAG structure ensures every story eventually terminates, and that we can count all possible stories by working backward from terminal nodes. This is why procedurally generated narrative tends toward linear paths with occasional merges, not genuinely open worlds.
What This Reveals
Procedural narrative exposes a fundamental tension in generative systems: structure versus freedom. A graph encodes what can happen, but the combinatorics of branching means even modest structures contain surprises. The designer controls the skeleton; the randomness controls the instantiations. Most generated stories feel similar (they follow high-weight paths), but rare combinations produce genuinely novel sequences. The same mechanism that generates repetitive content also occasionally generates magic — it's the same mechanism, just different dice rolls.