P5: Storylets
P5: Storylets
CMPM 35 – Fall 2021
1
Learning objectives
2
You will …
– model a story as a set of dramatic beats that can play out in a flexible order
– work from base code that provides less support
– express conditional logic in authored data files (TOML)
– interpret those conditions in scripting files (JS)
Your story-definition language is growing beyond common programming languages where computation proceeds from one step to
the next via selection of well defined alternatives (if/else).
Storylets
3
Storylets are a system for organizing narrative content. Unlike the nodes in a graph-based
branching narrative, storylets allow narrative chunks to be experienced in a much more flexible
order — easily allowing long-term impacts of choices and interleaving of orthogonal story arc.
Each storylet has three key components:
– Some narrative content
– Conditions under which this storylet is available to the player
– Effects applied to the story world that impact the conditions for other storylets
One of the simplest effects of experiencing a storylet is that whether it has been experienced or not
already can be listed in the conditions of another storylet.
Example output
4
Story title
Narrative chunks
Available storylets
(by name)
https://corporate-bodies.glitch.me/
https://glitch.com/~corporate-bodies
https://corporate-bodies.glitch.me/
https://glitch.com/~corporate-bodies
Base code on Glitch
Remix this project on Glitch to get started: https://glitch.com/~storylets-base
The base code initial output is mostly blank
(use methods on the engine object to change this). Click the links
provided to test your rules.js file on several different test stories.
Delete the links from index.html when you no longer need them.
You should focus on story.toml
and rules.js, but you might
learn more by browsing and
tinkering with the other files.
You can always delete your own
project and create a fresh remix
of the base code you break
something important.
5
https://glitch.com/~storylets-base
story.toml (What’s TOML?)
Look in the base code README for documentation on this format (visible at the bottom
of the base code project page). Beyond this, check files like test-chain.toml and
test-rejoin.toml for concrete examples of storylet design patterns.
6
Story requirements:
Stories need a title. It should be a string.
Stories need an introduction. It should be either a string or array of
strings.
Stories need a collection of storylets. These should be defined in
square-bracketed tables in your TOML file. The name for each
table should start with “storylets.” as in
“[storylets.good_ending]”.
Individual storylet object requirements:
Each storylet implicitly has an identifier. If the storylet was defined
in a table with “[storylets.good_ending]”, then the
identifier is just “good_ending”.
Each storylet needs a name. It should be a string. If the storylet
should surprise the player, donʼt spoil the surprise in the name of
the storylet.
Each storylet needs a description. It should be either a string or
array of strings.
Each storylet may also have requires and/or conflicts conditions.
See the README and test examples for more details.
https://github.com/toml-lang/toml/blob/master/README.md
https://glitch.com/~storylets-base
rules.js
The start(text) function is called with the text of your
TOML file when the story begins. It should not return a value.
Instead, it should use methods on the engine object to
show the initial state of the story (title, introductory chunks,
and choices).
The applyChoice(target) function is called when the
player has chosen one of the available storylets. The target
parameter holds the identifier of the storylet chosen. This
function should not return a value. Instead, it should use
methods on the engine object to append additional
narrative chunks and provide a new set of choices (or an
empty array to end the story).
Because you need to check conditions of storylet availability
and update the provided choices in both function above,
you will benefit from making additional helper functions to
capture the shared logic.
7
The engine object is already accessible by in rules.js by a
global variable. What can you do with the engine object?
engine.setTitle(“Meow”); will make visible “Meow”
as the story title.
engine.appendChunk(“Time passed.”); will
make a new narrative chunk (light gray bubble) appear at
the bottom of the story containing the text “Time passed.”
engine.provideChoices([
{text: “Talk to boss”, target: “get_fired”},
{text: “Have lunch”, target: “lunch”}
]); will make (only) two choice visible to the player at the
bottom of the screen as buttons. One button, with the text
“Talk to boss”, will cause applyChoice to be called with
target “get_fired”.
Reminder: Itʼs okay to look at and talk through other studentʼs solutions, but
each rules.js should be hand-typed by you (with minor help on the typing
aspect) from your collaborators — Glitch saves your development history.
This file will be about 100 lines of code by the time you are done with it.
Suggested development process
1. Remix the base code on Glitch.
2. Edit start(text) in rules.js to show the title of the starter story (given as story.toml).
3. Edit start(text) in rules.js to show the introduction as narrative chunks.
4. Edit start(text) in rules.js to show the provide the first storylet as a choice.
5. Use the links provided in index.html to work through each of the test-*.toml files in turn. Continue to edit rules.js (now
including applyChoice(target)) to fully support each story before continuing.
a. Test-1-chain will require you to check that storylets are not visible until their requires condition is satisfied.
b. Test-2-both will require you to support a logical-and (conjunction) of requires conditions. It also checks that you can
support a multi-chunk introduction to your story.
c. Test-3-exclusive will require you to check that a storyletʼs conflicts conditions are not satisfied.
d. Test-4-rejoin will require you to support a logical-or (disjunction) of requires conditions. It also checks multi-chunk
descriptions for storylets.
6. Edit index.html remove links to the test-*.toml files.
7. Sketch out a story with at least two distinct ends. To speed prototyping, just use an empty array ([]) as the description for
most storylets. You can add the narrative details later.
8. Play through your own story many times/ways and ask other to do so as well. While you are playing, note if certain storylets
are visible when they are not supposed to be (or vice versa). Edit your requires/conflicts conditions accordingly. If you suspect
a bug in your rules.js code, always make sure your JS code is compatible with the test stories.
9. Give your proof-readers a thank on Glitch when they make a useful contribution to your story (even a small one).
8
Key requirement: 5+ storylets
Each of your stories (youʼll make two) needs to have at least 5 distinct storylets. Because
storylets can yield zero or more narrative chunks, we also need to see at least 5 distinct
narrative chunks.
9
Key requirement: Distinct pathways
Using conflicts conditions, your stories should allow the player to explore multiple
distinct and mutually-exclusive pathways. Somewhere in your story, you should force
the player to commit to a choice that eliminates some other possibilities
(test-3-exclusive.toml shows one approach). By using the rejoining pattern (seen in
test-4-rejoin.toml), the story might always end in the same way, but there should be
meaningfully distinct pathways of reaching that end (or ends). Two major pathways
would be enough (e.g. using the sorting hat pattern).
In Adamʼs example story (Corporate Bodies), the main character always ends up alone in
the end. However, they either get there by moving too fast in the relationship or by
moving too slow.
10
Storylets: You Want Them
https://corporate-bodies.glitch.me/
Youʼll need to submit two of your own Glitch projects (by Project Page URL) on Canvas to get full credit. Having
two creatively distinct projects will allow you to get 2 out of 2 points for satisfying the other requirements.
Subjective: Perhaps the story emphasizes social or emotional issues. Maybe it focuses on a single personʼs
experience or the relationship between two people. Good/evil and happy/sad are opposites, but they are still
open to interpretation. Examples: Itʼs a dating simulator; who will you end up with? Your magic powers develop in
distinct directions as you commit (debatably) righteous or villainous acts upon others.
Objective: Perhaps the story downplays individual humans (if it mentions them at all). Maybe it documents
crafting/assembly of an object or the technological developments of a civilization. Examples: Cooking in the
kitchen, you can use your remaining few eggs to make pancakes or an omelette, but you canʼt do both. Itʼs a
business simulator (for a game studio?); what product will you release next and how will the market respond?
Key requirement: Distinct themes (subjective/objective)
11
Key requirement: artist statement / design document
In your README.md file, use Markdown syntax to write an informative* artist statement
for each story. Each statement should help the audience see the meaningfully distinct
pathways through your story. You might literally include a bulleted list of choices to
make in a specific order follow the most interesting paths.
* 1-2 paragraphs that call attention to your creative process, the intent of the piece, and
any technical highlights (such as where you customized the engine or webpage)
12
https://www.markdownguide.org/cheat-sheet/
Scoring (14 points possible)
[8pts] All test-stories work as expected and yield no errors visible in the console. The grader will run commands
like engine.run(“test-1-chain.toml”) in the console while looking at your project. Please do not
delete the test stories from your project (and copy them from the base code again if you accidentally delete
them). Each test story in each submitted project earns one point.
[2pts] Sufficient size for each story (5+ storylets). Although each storylet can have zero to many narrative chunks,
your story should also include 5+ narrative chunks across all storylets and the introduction.
[2pts] Distinct theme for each story (one is clearly the “objective” one and one is the “subjective” one). Your
README should help us identify and understand how the theme plays out in the story.
[2 pts] Distinct pathways within each story (implemented by using conflicts conditions one or more times). Your
README should make it easy for us to play through the story to see each of these pathways. For example, just tell
us what to click to have the desired experience.
13
Programming tip: tracking story state
How should you keep track of which storylets have already been chosen?
Use a Set data structure. Create a global variable (to be assigned in start(text)) that will
refer to a new Set(). Think of it as an inventory of previously-collected scenes.
When the player has just chosen a storylet:
inventory.add(target);
When you need to know if the player has already seen a storylet:
if(inventory.has(target)) { /* … */
14
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
Programming tip: condition checking
To get the desired effects of the conditions written in the
sample stories, youʼll need to implement somewhat
complicated condition checking logic. Because this logic is
needed multiple times, you should implemented it in a
function by itself.
Because this is a data structures class (rather than an
algorithms class), weʼre happy to hand you the algorithm
needed.
To the right is Python-flavored pseudocode for checking
conditions represented as disjunctions-of-conjunction
array-of-arrays. The hard part of translating is remembering
how to loop over JavaScript arrays or check if a Set contains
an element.
15
def judge(condition):
if condition is a string:
condition = [ condition ]
# now it is an array: conjunction of one item
if condition[0] is a string:
condition = [ condition ]
# now it is an array of arrays: a disjunction of one case
for each disjunct of condition:
satisfied = True # hope for the best
for each conjunct of disjunct:
if conjunct has NOT been seen:
satisfied = False # hopes dashed
if satisfied:
return True
# We get here if we never returned True above.
# So, we know none of the disjuncts were satisfied.
return False
Condition-checking in JavaScript-ish syntax
function judge(condition) {
if (condition is a string) {
condition = [ condition ];
// now it is an array: an AND condition with one item
}
if (condition[0] is a string) {
condition = [ condition ];
// now it is an array of arrays: an OR condition of one case
}
for each outer_array of condition {
satisfied = true; // hope for the best
for each inner_item of outer_array {
if inner_item has NOT been seen {
satisfied = false; // hopes dashed
}
}
if satisfied {
return true;
}
}
// We get here if we never returned true above.
// So, we know none of the disjuncts were satisfied.
return false;
}
16
How do you check if some data has a specific
data type?
https://developer.mozilla.org/en-US/docs/W
eb/JavaScript/Reference/Operators/typeof
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof
Optional enhancement: Procedural text
Use some of the small-scale procedural text techniques from P3 to vary the appearance of narrative chunks
before they are displayed.
Cheap tricks:
– Writing a few alternatives in an array
– Using $slots and fillers
Tricky tricks:
– Keep track of game state beyond just the names of which storylets were already chosen. Use this to
customize the display of chunks (i.e. reporting the playerʼs current number of health points or relationship
points)
17
Optional enhancement: Rich text
Use your ability to find and/or author non-text media to tell a visual story using the same storylets engine.
Instead of plain text, author HTML to embed that beyond-text media.
Ideas:
– Images/video: Each chunk shows an image + caption as in P2. Maybe the images are actually video (in
which case youʼve just implemented an interactive video engine more complicated that the one Netflix has
— they donʼt have storylets!).
– Dialog: Chunks represent dialog turns between characters, and each chunk includes a little avatar image
so that you know who is speaking. If you know CSS, you can style the speech from different characters
consistently by editing style.css.
– Minigames: Some chunks include a tiny game youʼve made with p5.js. Depending on how the minigame
ends, the state of the game changes in a way that unlocks specific future storylets.
18