Saga is an experimental programming language for interactive fiction. Think "choose your own adventure" but with the dynamism of a Turing machine.
Stories (Saga programs) are sets of passages with preconditions, consequences, choices, and links.
Playing a story (running a Saga program) starts with an empty bag of facts, and the story progresses from passage to passage through links, user choices and satisfied preconditions.
As we'll see as you read on -- preconditions, choices and deterministic consequences govern declarative, constraint-based control-flow, whereas links and probabilistic consequences are used for probabilistic control flow (and thus can also be used to model imperative, deterministic control flow too).
A precondition is a fact that must be true for the passage to occur -- if a
passage has n
preconditions, all of them must be true for it to occur (unless
there is a link to it).
(-> (s/passage :taking-the-car "I took the car and drove off.")
(s/requires (s/indeed "I have a car")))
Expressed in its lispy intermediate representation, the expression above means
that there is a passage with id :taking-the-car
, that describes how I took the
car and drove off, and it requires for me to have a car.
Unless another passage has a link to it, the only way for this passage to occur
is to acquire the fact "I have a car"
at some prior point in the story.
A consequence is a fact that may be true after a passage occurs depending on a probability, and thus may be accumulated into the player's bag of facts.
When no probability is given, it defaults to certainty:
(-> (s/passage :in-the-market "Wandering through the market, I found an apple.")
(s/entails (s/indeed "I have an apple")))
This means that after the :in-the-market
passage happens, the player will have
a new fact in their bag, namely "I have an apple"
. This may enable future
passages that might otherwise be inaccesible (such as eating an apple).
Consequences can have independent probabilities up to 100% each:
(-> (s/passage :in-the-market "Wandering through the market, I found an apple.")
(s/entails (s/indeed "I have an apple"))
(s/entails (s/indeed "Someone saw me in the market") :p 0.2))
A choice is a request that the player needs to decide on. Each of its branches entails a set of consequences.
(-> (s/passage :at-home "I was getting read to get out of the house, and...")
(s/choices
(s/when-chose "Forget the umbrella, this is Barcelona. Sunglasses time!"
(s/then (s/not "I have an umbrella"))
(s/then (s/indeed "I have sunglasses on")))
(s/when-chose "I took an umbrella, you never know."
(s/then (s/indeed "I have an umbrella")))))
This means that, when this passage occurs, the player will be presented with a choice between taking an umbrella or sunglasses. These facts might determine the availability of future passages.
Consequences in choice branches can also have independent probabilities, just like normal consequences:
(-> (s/passage :at-the-crossroads "I came to a crossroads.")
(s/choices
(s/when-chose "I decided to go left."
(s/then (s/indeed "I went left"))
(s/then (s/indeed "A spy saw me.") :p 0.2))
(s/when-chose "I took an umbrella, you never know."
(s/then (s/indeed "I went right")))))
A link is a probability to jump from one passage to another when the first occurs.
(-> (s/passage :at-the-store "That store was so full of items, that I was getting hungry.")
(s/leads-to :at-the-mall-restaurant))
After the :at-the-store
passage runs, it is guaranteed that
:at-the-mall-restaurant
will be the next passage. That is because, being the
only link in the passage and having no probability assigned, its probability is
100%.
More generally, as long as no probabilities are assigned, the probability of each link to occur is 1 divided between the number of links:
(-> (s/passage :at-the-store "That store was so full of items, that I was getting hungry.")
(s/leads-to :at-the-mall-restaurant)
(s/leads-to :outside)))
In this case above, 50% of the time we'll end up at the mall restaurant, and the other 50% of the time we'll wind up outside.
One can weight links on purpose, as long as the defined probabilities won't go over 1:
(-> (s/passage :at-the-store "That store was so full of items, that I was getting hungry.")
(s/leads-to :at-the-mall-restaurant :p 0.8) ;; 80% of the time we'll go here
(s/leads-to :outside))) ;; ... and 20% of the time here
If probabilities go under 1, the remaining probability will be assigned to no link at all:
(-> (s/passage :at-the-store "That store was so full of items, that I was getting hungry.")
(s/leads-to :at-the-mall-restaurant :p 0.2) ;; 20% of the time we'll go to the mall restaurant
(s/leads-to :outside :p 0.2))) ;; another 20% of the time we'll go outside
;; and the remaining 60% of the time no link will occur.
In the most complex case, we can assign defined probabilities to some links and implicitly divide the remaining probabilities among the rest of the links:
(-> (s/passage :at-the-store "That store was so full of items, that I was getting hungry.")
(s/leads-to :at-the-mall-restaurant :p 0.2)
(s/leads-to :outside :p 0.2)
(s/leads-to :the-basement)
(s/leads-to :the-directors-office)))))
In the example above the probabilities will be:
- 20% of the time we'll go to the mall restaurant
- 20% of the time we'll go to outside
And the remaining 60% will be divided equally among the other two links:
- 30% of the time we'll end up in the basement
- 30% of the time we'll end up in the director's office
This repo contains two separate projects that share some common code:
The interpreter used by the player lives in the saga.engine
namespace.
Saga programs (stories) are exported and imported as EDN files. More about this to come.
You'll need Boot (brew install boot-clj
).
To build the IDE:
boot package -t ide
open target-ide/index.html
To build the player:
boot package -t player
open target-player/index.html
To deploy each part to Netlify, go to deploys/ide
or deploys/player
respectively and run netlify deploy
.
This project is built entirely in ClojureScript. The IDE and the Player are both
separate Om Next apps. You'll need Boot (brew install boot-clj
).
To work on the IDE:
boot ide-dev
open localhost:3000/ide.html
To work on the player:
boot player-dev
open localhost:3000/player.html
There are currently no automated tests. To run them anyway:
boot test
The Saga IDE includes a debugger to play your stories as you develop them:
You can use the time-travelling feature on the top right to go to past points in the story.
- Basic IDE working
- Basic Player working
- Exporting stories from IDE as EDN files
- Loading stories from Player as EDN files
- Saving / restoring app state automatically from local storage (IDE & Player)
- HTML5 Offline capability
- Material design in the IDE
- Material design in the Player
- Probabilistic passage links
- Probabilistic consequences
- Basic time-travel across all IDE
- Basic debugging a story, recording choices and asking facts where they came from
- Debugging a passage by running it with a canned bag of facts
- Debugging-enabled player within the IDE (manipulating facts, etc)
- Debugging-enabled player within the IDE (manipulating facts, etc)
- User-driven, generative testing of stories
- Test everything!!!
- Write about Saga's design decisions
- Package Player as a React Native app
- Set up Github releases with Travis CI artifacts