The story can be represented as a directed graph:
There is an example of the corresponding graph of “Manoir de l’Enfer” from Steve Jackson 1:
For each node, we need one dedicated page. And to materialize the links, we need buttons to select one option.
One the previous image (and in books), chapters have a given number. So, if you want to cheat or if you are stuck, just open randomly another page.
To prevent that, I suggest creating the page ID from a hash or a random number.
For instance, with seed = 42
(to prevent someone from guessing the hash too easily), we generate hashes the following way:
import hashlib
seed = 42
n_pages = 50
dic_hash = {}
for ID in range(n_pages):
dic_hash[ID] = hashlib.md5((str(seed) + str(ID)).encode()).digest().hex()
Which gives the following IDs:
{
"0": "b6f0479ae87d244975439c6124592772",
"1": "e0c641195b27425bb056ac56f8953d24",
"2": "f85454e8279be180185cac7d243c5eb3",
"3": "faa9afea49ef2ff029a833cccc778fd0",
"4": "3c7781a36bcd6cf08c11a970fbe0e2a6",
"5": "25b2822c2f5a3230abfadd476e8b04c9",
"6": "6ecbdd6ec859d284dc13885a37ce8d81",
"7": "18997733ec258a9fcaf239cc55d53363",
"8": "8d7d8ee069cb0cbbf816bbb65d56947e",
"9": "75fc093c0ee742f6dddaa13fff98f104",
...
}
If page 1
connects to page 2
and 3
, we must create buttons redirecting to these pages.
Additionally, we may add text on these buttons (rather than 2
and 3
).
My knowledge in javascript and html are quite limited, so I am unable to create a clean button with the correct behavior.
To that purpose, I use bokeh
which is a python
library enabling making html and javascript elements with interaction.
from bokeh.models import Button, CustomJS
from bokeh.layouts import row
from bokeh.embed import components
def create_button(label, url):
"""
Create a button with "label" on it, redirecting to url
"""
button = Button(label=label,
button_type="primary")
js_callback = CustomJS(args={}, code="""
window.open("{}", "_self");
""".format(url))
button.js_on_click(js_callback)
return button
def create_button_row(labels, urls, cmp=True):
"""Create multiple buttons
:param labels: list of string to add on the different buttons
:param urls: list of urls where the button redirect to
:param cmp:
- False: returns a bokeh element
- True: returns script and div (string to be embedded in html)
"""
assert(len(labels) == len(urls))
lst_b = []
for label, url in zip(labels, urls):
lst_b.append(create_button(label, url)
p = row(lst_b)
if cmp:
return components(p)
else:
return p
To create two buttons, we run:
labels = ["Click A", "Click B"]
urls = ["https://google.com", "https://www.qwant.com/"]
div, script = create_button_row(labels, urls, cmp=True, redirect=True)
with open("test_script.txt", "w") as fp:
fp.write("<html>\n" + script + "\n</html>")
with open("test_div.txt", "w") as fp:
fp.write(div)
This elements are stored in the _include
folder of jekyll
in my case.
It generate this:
Note: it does not open a new tab. Otherwise, you would end up with hundreds of tabs open at the end of the story.
Now that we are able to generate buttons, we just need to assemble everything together.
We need:
A jekyll page will be composed of:
and have the hash in its url.
To keep our _include
folder clean, scripts and divs will be located at:
_include/maze/test/
Therefore, pages need to have in it:
{% include maze/test/
{% include maze/test/
Location of the pages does not matter. They just need to be all in the same folder, as the url is relative.
We need to define how to store a story.
We propose to use the json
format, which is flexible enough and easily manipulated with python
.
This is our test file:
{"1": {
"text": "You are on the first page! Welcome. You have to select where you want to go",
"next": [["2", "Go to the left"], ["3", "Go to the right"]]
},
"2": {
"text": "You decided to go left, and arrive in front of the forest. What would you do ?",
"next": [["1", "Go back to the previous place"], ["4", "Enter in the forest"]]
},
"3": {
"text": "You arrive in front of the house. What would you do ?",
"next": [["5", "Try to open the door"], ["6", "Try to look if there is someone in"]]
},
"4": {
"text": "You entered the forest, but unfortunately, you get eaten by a werewolf. The story ends here",
"next": []
},
"5": {
"text": "The door is unlocked. This is the end of the prototype.",
"next": []
},
"6": {
"text": "It doesn't seem to be anyone in the house.",
"next": [["5", "Try to open the door"]]
}
}
Which corresponds to this graph:
In the outer dictionnary, each entry corresponds to a page, where the key is the page ID.
Here, there are 6
pages.
Each page is described by an inner dictionnary, which has two mandatory keys:
text
: This is the story of the pagenext
: This is the list of links, which can be emptyEach element in the next
list is composed of two elements:
ID
of the next page;import json
import os
seed = "42"
SAVE_PATH = "gen/maze/test/"
SAVE_PAGE = "pages/test/"
PATH_header = "my_yaml_header.yaml"
# Load the graph
with open("my_test_graph.json", "r") as fp:
graph = json.load(fp)
# Generate IDs
dic_hash = {}
for ID in graph:
dic_hash[ID] = hashlib.md5(seed + str(ID)).encode()).digest().hex()
# Create button
os.makedirs(SAVE_PATH, exist_ok=True)
for ID, vals in graph.items():
ID_hash = dic_hash[ID]
next_LB = list(map(lambda x: x[1], vals["next"]))
next_ID = list(map(lambda x: dic_hash[x[0]], vals["next"]))
script, div = create_button_row(next_LB, next_ID)
with open("{}{}_div.txt".format(SAVE_PATH, ID_hash), "w") as fp:
fp.write(div)
with open("{}{}_script.txt".format(SAVE_PATH, ID_hash), "w") as fp:
fp.write(script)
# Making the pages
os.makedirs(SAVE_PAGE, exist_ok=True)
data_header = None
with open(PATH_header, "r") as fp:
data_header = fp.read()
for ID, ID_hash in dic_hash.items():
include_div = "{% include " + book_ID + ID_hash + "_div.txt %}"
include_script = "{% include " + book_ID + ID_hash + "_script.txt %}"
page = """{}
{}
<center>
{}
</center>
<html>
{}
</html>
""".format(data_header, graph[ID]["text"], include_div, include_script)
with open("{}{}.md".format(SAVE_PAGE, ID_hash), "w") as fp:
fp.write(page)
Running this script (up to some modification to adapt to your case), you get:
gen/
+ maze/
+ test/
- hash_x_div.txt
- hash_x_script.txt
- hash_y_div.txt
- hash_y_script.txt
pages/
+ test/
- hash_x.md
- hash_y.md
You need to move these items in the corresponding jekyll folder:
my_website/
+ _includes/
+ maze/
+ test/
- hash_x_div.txt
- hash_x_script.txt
- hash_y_div.txt
- hash_y_script.txt
+ my_folder_with_random_name_for_maze/
+ subfolder_if_necessary/
+ test/
- hash_x.md
- hash_y.md
You can get access to the prototype Here
French websites:
>> You can subscribe to my mailing list here for a monthly update. <<