Skip to content

Instantly share code, notes, and snippets.

@sma
Created August 19, 2012 15:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sma/6338d6f132e7a6cca946 to your computer and use it in GitHub Desktop.
Save sma/6338d6f132e7a6cca946 to your computer and use it in GitHub Desktop.
interactive fiction construction set in one page of HTML
<!DOCTYPE html>
<html>
<head>
<!-- Storybuilder Copyright 2012 by Stefan Matthias Aust. All rights reserved. Use under BSD license. -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>Storybuilder</title>
<script src="http://code.jquery.com/jquery-1.8.0.min.js"></script>
<script src="https://raw.github.com/chjj/marked/1a2b02c6695527bd54c4fdd1bfa2f3a4dd64ffc8/lib/marked.js"></script>
<style>
xmp { display: none; }
body { margin: 20px; padding: 0; background: #EEE; color: #444; font: normal 15px/22px Georgia,serif; }
body { max-width: 480px; min-height: 480px; }
h1, h2, h3, h4 { margin: 10px 0; color: #666; }
h1 { font: bold 23px/22px Georgia,serif; }
h2 { font: bold 18px/22px Georgia,serif; }
h3 { font: bold 15px/22px Georgia,serif; }
p, ul, ol { margin:10px 0; }
code, pre { border: 1px solid #DDD; border-radius: 3px; padding:2px; background: #F0F0F0; color: #666; }
code { font: normal 14px/22px Menlo,monospace; }
small code { font-size: 11px; }
pre { line-height: 18px; padding: 9px; overflow: auto; }
pre > code { font: normal 12px/18px Menlo,monospace; border: 0; }
.tagline { margin:-10px 0 10px 0; font-size: 12px; font-style: italic; }
a { color: #36C; font-weight: bold; }
ul { list-style-type: square; }
ul.choices, ul.choices li { margin: 0; padding: 0; list-style: none; }
a.btn {
display: block; min-height:22px; margin: 10px 0; padding: 10px;
border: 1px solid #BBB; border-radius: 10px; background: #DDD;
text-decoration: none; color: #444; font-size: 14px;
}
a.btn:hover { background-color:#444; border-color:#444; color:#EEE; }
#dice { position: relative; width: 240px; height: 240px; }
#dice .die {
position: absolute; width: 48px; height: 48px; border: 1mm solid #840; border-radius: 10px; background: #A50;
font: normal 40px/50px Helvetica,sans-serif; color: #FFF; text-align: center;
}
</style>
<script>
var sections = {};
var start;
var deadEnd = new Section("", "Sackgasse", "Hier geht es nicht weiter.\n\n> Neu beginnen *start*");
function Section(id, title, text){
this.id = id;
this.title = title;
this.text = text;
}
function addSection(section){
if (!start) start = section.id;
sections[section.id.toLowerCase()] = section;
}
function addSectionsFromText(text){
var lines = text.split(/\n/), i = 0, j, m, id, title;
while (i < lines.length){
if (!/^=+$/.test(lines[i + 1])) { i++; continue; }
m = lines[i].match(/^(?:(.*?)\s*:\s*)?(.*)$/);
title = m[2]; id = m[1] || title;
for (j = i + 2; j < lines.length; j++){
if (/^=+$/.test(lines[j + 1])) break;
}
text = lines.slice(i + 2, j).join("\n");
addSection(new Section(id, title, text));
i = j;
}
}
function findSection(id){
if (!id || id === 'start') id = start;
return sections[id.toLowerCase()] || deadEnd;
}
function showSection(section){
$("title").text(section.title);
$("#title").text(section.title);
var text = section.text;
text = executeCommands(text);
text = text.replace(/^>> +(.*) +\[(.*?) +([-+]?\d+)\] +\*([^*]+)\* +\*([^*]+)\*/mg,
"<ul class='choices'><li><a class='btn' href='#' data-roll='$2' data-target='$3' " +
"data-success='#$4' data-failure='#$5'>$1</a></li></ul>");
text = text.replace(/^> +(.*) +\*([^*]+)\*/mg,
"<ul class='choices'><li><a class='btn' href='#$2'>$1</a></li></ul>");
text = text.replace(/<\/ul>\s*<ul class='choices'>/g, "\n");
text = text.replace(/^{([^}]+)}(.*)/mg, "<div class='$1'>$2</div>");
$("#text").html(marked(text));
window.scrollTo(0, 1);
}
function gotoSection(id, track){
if (history && track){
history.pushState(null, null, "#" + id);
}
showSection(findSection(id));
}
var attributesSection = "character";
var state = {};
function getAttribute(name){
name = name.toLowerCase();
var re = /^\* +(.*) +([-+]?\d+)/mg, text = findSection(attributesSection).text, m;
while ((m = re.exec(text))){
if (m[1].toLowerCase() === name){
return m[2];
}
}
return "0";
}
function setAttribute(name, value){
name = name.toLowerCase();
var section = findSection(attributesSection);
section.text = section.text.replace(/^\* +(.*) +([-+]?\d+)/mg, function(m, n, v){
return n.toLowerCase() === name ? m.replace(v, value) : m;
});
}
function executeCommands(text){
return text.replace(/(^\^|\?)\[(.*?)\] *(.*)$/mg, function(_, t, command, rest){
if (t === '?'){
return String(getAttribute(command)) + executeCommands(rest);
}
if (/^(.*?)\s*([-+:]=)\s*([-+]?\d+)/.test(command)){
var name = RegExp.$1, op = RegExp.$2, val = RegExp.$3;
if (op === ":=") setAttribute(name, val);
if (op === "+=") setAttribute(name, Number(getAttribute(name)) + Number(val));
if (op === "-=") setAttribute(name, Number(getAttribute(name)) - Number(val));
} else if (/^setze\s+(.*)/i.test(command)){
state[RegExp.$1.toLowerCase()] = true;
} else if (/^lösche\s+(.*)/i.test(command)){
delete state[RegExp.$1.toLowerCase()];
} else if (/^wenn\s+(.*)/i.test(command)){
return executeCondition(RegExp.$1, rest);
}
return executeCommands(rest);
});
}
function executeCondition(condition, rest){
condition = condition.split(/\s+(und|oder)\s+/i);
var r = evaluate(condition[0]), i = 1;
while (i < condition.length){
if (/und/i.test(condition[i])){
r &= evaluate(condition[i + 1]);
i += 2;
} else if (/oder/i.test(condition[i])){
r |= evaluate(condition[i + 1]);
i += 2;
} else {
break;
}
}
return r ? executeCommands(rest) : "";
function evaluate(c){
if (/nicht\s+(.*)/i.test(c)){
return !evaluate(RegExp.$1);
}
c = c.split(/\s*(<=?|>=?|!?==?)\s*/);
if (c.length === 3){
var a = state[c[0].toLowerCase()];
var b = state[c[2].toLowerCase()];
switch (c[1]){
case '=': case '==': return a == b;
case '!=': case '!==': return a != b;
case '<': return a < b;
case '>': return a > b;
case '<=': return a <= b;
case '>=': return a >= b;
}
}
return state[c[0].toLowerCase()];
}
}
function resolve(roll, target, success, failure) {
var result = -4, i, die, wdiv = rnd(80) + 80, hdiv = rnd(80) + 80, top, left;
var $dice = $("<div id='dice'/>").css({left: (Math.min($(window).width(), 520) - 240) / 2});
for (i = 0; i < 4; i++){
die = rnd(3);
result += die;
// randomize die location on screen
top = (i < 2 ? rnd(hdiv - 80) : hdiv + rnd(160 - hdiv)) + 35;
left = (i % 2 ? rnd(wdiv - 80) : wdiv + rnd(160 - wdiv)) + 35;
$dice.append($("<div class='die'>").css({ top: top, left: left,
"-webkit-transform": "translate(-25px,-25px) rotate(" + (rnd(91) - 45) + "deg)"
}).text(["-1", "0", "+1"][die]));
}
var skill = getAttribute(roll) || "0";
var ok = result + Number(skill) >= Number(target);
$("#title").text(roll + skill + " gegen " + target);
$("#text").html("Die Würfel fallen...").append($dice).append("<a class='btn' data-close='true' href='" +
(ok ? success : failure) + "'>Eine <big>" + (result + Number(skill)) + "</big> ist " +
(ok ? "" : "nicht ") + "ausreichend.</a>");
}
function rnd(n){
return Math.floor(Math.random() * n);
}
$(function(){
$("xmp").each(function(i, e){
if (e.id || e.title){
addSection(new Section(e.id || e.title, e.title || e.id, $(e).text()));
} else addSectionsFromText($(e).text());
}).remove();
$("body").append($("<h1 id='title'/><div id='text'>"));
$("a.btn").live("click", function(e){
e.preventDefault();
if (this.dataset.roll){
return resolve(this.dataset.roll, this.dataset.target, this.dataset.success, this.dataset.failure);
}
gotoSection(this.href.split("#")[1], true);
});
gotoSection(start);
// doesn't work on Firefox
window.addEventListener("popstate", function(e){
gotoSection(location.hash.substr(1));
});
// hide address bar on iOS
window.addEventListener("load", function(){
setTimeout(function(){ window.scrollTo(0, 1); }, 0);
});
});
</script>
</head>
<body>
<xmp>
1: Storybuilder
===============
{tagline}von Stefan Matthias Aust
... ist ein Generator für nicht-lineare Geschichten, bei denen der Leser
wählen kann, welchem Weg er folgen möchte.
Das ganze ist als einzelne HTML-Seite realisiert, sodass man ausgehend von
dieser Vorlage aus eigene Geschichten erstellen kann.
> Wie funktioniert das? *2*
2: Datenformat
==============
Text wird in *Abschnitte* unterteilt, die über *Verweise* verknüpft werden.
## Abschnitte
Text muss in `xmp`-Tags eingeschlossen werden. Entweder steht alles innerhalb
eines Tags. Dann muss jeder Abschnitt mit einer mit `=` unterstrichenen
Überschrift beginnen. Die Überschrift besteht aus einem optionalen
*Identifier*, gefolgt von einem Doppelpunkt (`:`) und dem Titel des
Abschnitts. Fehlt der Identifier, ist der Titel auch der Identifier. Oder es
gibt einen `xmp`-Tag pro Abschnitt, wobei dann das `id`-Attribut des Tags der
Identifier ist und das `title`-Attribut der Titel. Eines kann fehlen, aber
nicht beide.
1: Am Anfang
============
An Anfang war alles trist und leer.
Später
======
Später war es dann bunt und laut.
Gemäß [Markdown](http://de.wikipedia.org/wiki/Markdown) werden Absätze durch
Leerzeilen getrennt. Hervorhebungen gehen mit `*...*` oder `**...**` und
derartige Darstellung erreicht man mit `` `...` ``. Code-Beispiele werden mit
4 Leerzeichen eingerückt. Aufzählungen beginnen mit `* ...` am Zeilenanfang.
![Markdown Logo](https://raw.github.com/dcurtis/markdown-mark/master/png/66x40-solid.png)
## Verweise
Jeder Abschnitt sollte wenigsten auf einem anderen Abschnitt verweisen. Dazu
schreibt man nach einem `>` am Zeilenanfang eine Beschreibung in eine Zeile,
die mit dem in `*` eingeschlossenen Identifier des Abschnitts enden muss.
> Zeit vergeht *Später*
> Zurück zum Anfang *1*
## Proben
Alternativ kann man eine Probe gegen ein *Attribut* definieren und auf zwei
Abschnitte verweisen. Abhängig von dem Ergebnis der Probe wird bei Erfolg zum
ersten und bei Misserfolg zum zweiten Abschnitt gewechselt.
Attributen werden in einem `character` bezeichneten Abschnitt ein Wert
zugeordnet. Zur Zeit werden 4F (Fate Dice) gewürfelt, der Wert addiert und mit
dem angegebenen Zielwert verglichen. Wird er mindestens erreicht, ist die
Probe erfolgreich.
>> Ork angreifen [Kämpfen +2] *3* *4*
> Siehe Beispiel *3*
Attribute kann man auch setzen, erhöhen oder reduzieren, indem man die
folgenden *Befehle* in einen Abschnitt einbaut:
^[Kämpfen += 1]
^[Feilschen -= 2]
^[Gold := 4]
Den Wert eines Attributes kann man mit `?[...]` in den Text einfügen.
Du besitzt ?[Gold] Goldstücke.
## Weitere Befehle
Das folgende ist noch experimentell. Man kann Marker setzen, löschen oder
abfragen. Diese stellen den internen Zustand der Geschichte dar. Basierend auf
einem oder mehreren Markern können dann Verweise oder Absätze in einem
Abschnitt ausgeblendet werden:
^[setze Schatz gefunden]
^[lösche Schatz gefunden]
^[wenn Schatz gefunden] > Öffne Truhe *5*
^[wenn nicht Schatz gefunden] > Untersuche Gegend *6*
Ein Befehl muss am Zeilenanfang stehen und mit `^[` beginnen.
Mit `setze` kann ein ein Marker (im Beispiel `Schatz gefunden`) gesetzt
werden, mit `lösche` wird er wieder gelöscht. Mit `wenn` kann geprüft werden,
ob er setzt ist. Nur dann wird der Text danach berücksichtigt. Marker können
mit `und` und `oder` verknüpft und mit `nicht` negiert werden.
Es wäre auch wünschenswert, Attribute bei `wenn` zu benutzen:
^[wenn Kämpfen >= 2]
So kann es aussehen, wenn man ein Schwert aufnehmen kann:
In einer dunklen Höhle
======================
Du nimmst das Schwert.
^[setze Schwert]
^[Kämpfen := +2]
Charakter
=========
Du besitzt:
^[wenn Schwert] * Ein schartiges Schwert
^[wenn Axt] * Eine alte Axt
Du hast ?[Gold] Gold.
Zur Zeit kann man bei `wenn` allerdings nur Marker und nicht Attribute abfragen.
> Ein Test *test*
<small>Hinweis: Möglicherweise ändere ich noch die Syntax von `[ ]` auf `( )`.</small>
</xmp>
<xmp id="3" title="Kampfbeispiel">
Ein Kobold versperrt Dir den Weg.
>> Kobold angreifen [Kämpfen +2] *4* *5*
> Fliehen *6*
</xmp>
<xmp id="4" title="Gewonnen">
Du hast überlebt.
^[Gold += 1] Du findest ein Goldstück.
> Zurück *3*
> Charakter *character*
</xmp>
<xmp id="5" title="Verloren">
Du bist tot.
> Zurück *3*
</xmp>
<xmp id="character" title="Charakter">
{tagline}Cormac O'Briain
Du bist ein erfahrener Krieger, der in einer Schlacht zu überleben weiß.
## Fertigkeiten
* Kämpfen +1
* Feilschen -1
## Vermögen
* Gold 0
</xmp>
<xmp id="test">
^[Kämpfen += 2] Kämpfen: ?[Kämpfen].
^[Kämpfen -= 1] Kämpfen: ?[Kämpfen].
^[Kämpfen := 0] Kämpfen: ?[Kämpfen].
^[wenn Schwert] Ich habe ein Schwert.
^[setze Schwert]
^[wenn Schwert] Ich habe ein Schwert.
^[lösche Schwert]
^[wenn nicht Schwert] Ich habe kein Schwert.
^[wenn Schwert oder Axt] Du bist bewaffnet.
^[setze Axt]
^[wenn Schwert oder Axt] Du bist bewaffnet.
</xmp>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment