Created
June 17, 2020 06:32
-
-
Save CyberShadow/bc8e856b2954cb0122322a9cac3256ef to your computer and use it in GitHub Desktop.
Tombs & Tentacles
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/twine_graph | |
/graph.dot | |
/graph.dot.svg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -eu | |
drun twine_graph > graph.dot | |
dot -Tsvg -O graph.dot | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import std.algorithm.comparison; | |
import std.algorithm.iteration; | |
import std.algorithm.searching; | |
import std.array; | |
import std.conv; | |
import std.exception; | |
import std.file; | |
import std.math; | |
import std.stdio; | |
import std.string; | |
import std.typecons; | |
import ae.utils.array; | |
import ae.utils.regex; | |
import ae.utils.text; | |
import ae.utils.xml.entities; | |
import ae.utils.xml.lite; | |
void main() | |
{ | |
auto d = "/home/vladimir/tmp/2020-06-16/data/twine.xml".readText.xmlParse(); | |
string[string] passages; | |
foreach (n; d.findChildren("tw-storydata")[$-1].findChildren("tw-passagedata")) | |
passages[n.attributes["name"]] = n.text; | |
writeln("digraph {"); | |
writeln("\tgraph [size=\"20,1000\"] [ratio=compress concentrate=true]"); | |
auto fontName = "helvetica"; | |
writefln("\tgraph [fontname=%(%s%)]", [fontName]); | |
writefln("\tnode [fontname=%(%s%)]", [fontName]); | |
writefln("\tedge [fontname=%(%s%)]", [fontName]); | |
writefln(" graph [bgcolor=black];"); | |
writefln(" node [color=white fontcolor=white];"); | |
writefln(" edge [color=white fontcolor=white];"); | |
foreach (name, passage; passages) | |
{ | |
scope(failure) stderr.writeln("Error with " ~ name ~ ":"); | |
struct State | |
{ | |
string[][string] parents; | |
string[][string][] queuedParents; | |
int blockLevel; | |
string fontFlush; | |
bool terminal; | |
} | |
State[] stack = [State(null)]; | |
void popStack() | |
{ | |
enforce(stack.length > 1, "Stack underflow"); | |
auto tail = stack.stackPop(); | |
if (!tail.terminal) | |
stack[$-1].queuedParents ~= tail.parents; | |
} | |
void mergeParents() | |
{ | |
string[][string][] nextParents; | |
if (!stack[$-1].terminal) | |
nextParents ~= stack[$-1].parents; | |
nextParents ~= stack[$-1].queuedParents; | |
string[][string] mergedParents; | |
foreach (qp; nextParents) | |
foreach (qpName, labels; qp) | |
mergedParents[qpName] ~= labels; | |
stack[$-1] = State(mergedParents); | |
} | |
int counter; | |
void putEdges(string[][string] parents, string target) | |
{ | |
foreach (parent, props; parents) | |
{ | |
writef("\t%(%s%) -> %(%s%)", [parent], [target]); | |
if (target == "Start") | |
props ~= "constraint=false"; | |
if (props.length) | |
writef(" [%-(%s %)]", props); | |
writeln(";"); | |
} | |
} | |
string newNode() | |
{ | |
auto result = counter++ == 0 ? name : name ~ "__" ~ text(counter); | |
putEdges(stack[$-1].parents, result); | |
stack[$-1].parents = [result:null]; | |
return result; | |
} | |
string html; | |
bool allowStyles; | |
void flush() | |
{ | |
html ~= stack[$-1].fontFlush; | |
stack[$-1].fontFlush = null; | |
html = html.replace(`<B></B>`, ``); | |
html = html.strip; | |
// html = html.replace("\n", `<BR/>`); | |
if (html.length) | |
writefln("\t%s [shape=box label=<%s>]", newNode(), html.wrapNodeText); | |
html = null; | |
} | |
string parseExpression(string var, string value, string node) | |
{ | |
string result; | |
if (var == "$return") | |
{ | |
enforce(value.startsWith('"') && value.endsWith('"')); | |
writefln("\t%(%s%) -> %(%s%) [style=dashed];", [node], [value[1 .. $-1]]); | |
return "here"; | |
} | |
else | |
if (value.matchCaptures(re!`^Math\.floor\(Math\.random\(\)\*(.*)\)\+1$`, | |
(string max) | |
{ | |
result = "a random number\nbetween 1 and " ~ max; | |
})) | |
return result; | |
else | |
if (value.matchCaptures(re!`^Math\.floor\(Math\.random\(\)\*([0-9.]+)\)(?:/([0-9.]+))?([-+][0-9.]+)$`, | |
(double max, double divisor, double base) | |
{ | |
max = floor(max - 0.001); | |
if (!divisor.isNaN) | |
max /= divisor; | |
result = "a random number\nbetween %s and %s".format(base, base + max); | |
})) | |
return result; | |
else | |
if (value.matchCaptures(re!`^Math\.random\(\)\*([0-9.]+)$`, | |
(double max) | |
{ | |
result = "a real random number\nbetween 0 and " ~ max.text; | |
})) | |
return result; | |
else | |
return value; | |
//Math.floor(Math.random()*75)/10.0+1 | |
} | |
while (passage.length) | |
if (passage.skipOver("\\\n")) | |
continue; | |
else | |
if (!passage.startsWith("[[[") && passage.skipOver("[[")) | |
{ | |
flush(); | |
auto dir = passage.skipUntil("]]").enforce("Unterminated [["); | |
auto parts = dir.findSplit("->"); | |
string target, text; | |
if (parts[1].length) | |
list(text, null, target) = parts; | |
else | |
text = target = dir; | |
enforce(stack[$-1].parents.length, "Choice at start"); | |
// stack[$-1].terminal = true; | |
stack ~= State(stack[$-1].parents); | |
stack[$-1].terminal = true; | |
writefln("\t%s [shape=box style=filled fillcolor=\"#333333\" label=%(%s%)]", newNode(), ["▶ " ~ text]); | |
putEdges(stack[$-1].parents, target); | |
popStack(); | |
} | |
else | |
if (passage.skipOver("[")) | |
{ | |
flush(); | |
stack[$-1].blockLevel++; | |
} | |
else | |
if (passage.skipOver("]")) | |
{ | |
flush(); | |
stack[$-1].blockLevel--; | |
} | |
else | |
if (stack.length > 1 && stack[$-1].blockLevel == 0 && passage.startsWith("\n")) | |
{ | |
flush(); | |
popStack(); | |
mergeParents(); | |
} | |
else | |
if (passage.skipOver("(set:")) | |
{ | |
flush(); | |
auto dir = passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (").strip(); | |
auto parts = dir.findSplit(" to "); | |
enforce(parts[1].length, "No ' to ' in (set: ...): " ~ dir); | |
// parts[0].skipOver("$"); | |
auto node = newNode(); | |
auto value = parseExpression(parts[0], parts[2], node); | |
writefln("\t%s [label=%(%s%) shape=box color=\"#BBDDFF\" style=filled fillcolor=\"#002244\"]", node, ["Set %s\nto %s".format(parts[0], value)]); | |
} | |
else | |
if (passage.skipOver("(display:")) | |
{ | |
flush(); | |
auto dir = passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (").strip(); | |
enforce(stack[$-1].parents.length, "(display:) at start"); | |
if (dir.startsWith('"') && dir.endsWith('"')) | |
{ | |
dir = dir[1..$-1]; | |
putEdges(stack[$-1].parents, dir); | |
} | |
else | |
writefln("\t%s [label=%(%s%) shape=box color=\"#BBDDFF\" style=filled fillcolor=\"#002244\"]", newNode(), ["Go to\n" ~ dir]); | |
stack[$-1].parents = null; | |
} | |
else | |
if (passage.skipOver("(if:")) | |
{ | |
flush(); | |
auto dir = passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (").strip(); | |
writefln("\t%s [label=%(%s%) shape=box color=\"#FFAAFF\" style=filled fillcolor=\"#330033\"]", newNode(), [dir ~ " ?"]); | |
stack ~= [State(stack[$-1].parents.byKeyValue.map!(kv => tuple(kv.key, kv.value ~ ["color=green"])).assocArray)]; | |
stack[$-2].parents = stack[$-2].parents.byKeyValue.map!(kv => tuple(kv.key, kv.value ~ ["color=red"])).assocArray; | |
} | |
else | |
if (passage.skipOver("(else:")) | |
{ | |
flush(); | |
/* auto dir = */ passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (") /*.strip()*/; | |
popStack(); | |
stack[$-1].terminal = true; // no implicit pass-through after else | |
stack ~= [State(stack[$-1].parents.byKeyValue.map!(kv => tuple(kv.key, kv.value ~ ["color=red"])).assocArray)]; | |
} | |
else | |
if (passage.skipOver("(text-input:")) | |
{ | |
flush(); | |
/* auto dir = */ passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (") /*.strip()*/; | |
writefln("\t%s [shape=box style=filled fillcolor=\"#333333\" label=%(%s%)]", newNode(), ["▶ ________\n(text input)"]); | |
} | |
else | |
if (passage.skipOver("(either:")) | |
{ | |
flush(); | |
auto dir = passage.skipUntilBalancedClosed('(', ')').enforce("Unterminated (").strip(); | |
// stack[$-1].terminal = true; | |
// foreach (word; dir.split(",")) | |
// { | |
// stack ~= State(stack[$-1].parents); | |
// writefln("\t%s [shape=box label=%(%s%)]", newNode(), [word[1..$-1]]); | |
// popStack(); | |
// } | |
// mergeParents(); | |
writefln("\t%s [shape=record label=%(%s%)]", newNode(), [dir.split(",").map!(w => w[1..$-1]).join("|")]); | |
} | |
else | |
if (passage.skipOver("```")) | |
allowStyles = !allowStyles; | |
else | |
if (allowStyles && passage.skipOver("#{font:bold}")) | |
{ | |
html ~= "<B>"; | |
stack[$-1].fontFlush = "</B>" ~ stack[$-1].fontFlush; | |
} | |
else | |
if (allowStyles && passage.skipOver("#{font:default}")) | |
{ | |
html ~= stack[$-1].fontFlush; | |
stack[$-1].fontFlush = null; | |
} | |
else | |
{ | |
html ~= passage[0..1].encodeEntities(); | |
passage = passage[1..$]; | |
} | |
flush(); | |
} | |
writeln("}"); | |
} | |
string wrapNodeText(string html) | |
{ | |
assert(html.length, "Empty node!"); | |
// if (draft) | |
// return html; | |
static struct Word | |
{ | |
string html; | |
size_t width; | |
} | |
Word[][] paragraphs; | |
foreach (line; html.split("\n")) | |
{ | |
Word[] words; | |
Word word; | |
bool inTag, inEntity; | |
foreach (i, dchar c; line) | |
{ | |
if (inTag) | |
{ | |
if (c == '>') | |
inTag = false; | |
} | |
else | |
if (inEntity) | |
{ | |
if (c == ';') | |
inEntity = false; | |
} | |
else | |
if (c == '<') | |
inTag = true; | |
else | |
if (c == '&') | |
{ | |
inEntity = true; | |
word.width++; | |
} | |
else | |
if (c == ' ') | |
{ | |
words ~= word; | |
word = Word.init; | |
continue; | |
} | |
else | |
word.width++; | |
word.html ~= c; | |
} | |
words ~= word; | |
paragraphs ~= words; | |
} | |
string[] tryWrap(size_t width) | |
{ | |
string[] allLines; | |
foreach (words; paragraphs) | |
{ | |
string[] lines; | |
size_t currentWidth; | |
foreach (ref word; words) | |
{ | |
if (!lines || (currentWidth > 0 && currentWidth + word.width > width)) | |
{ | |
lines ~= null; | |
currentWidth = 0; | |
} | |
if (lines[$-1].length) | |
lines[$-1] ~= ' '; | |
lines[$-1] ~= word.html; | |
currentWidth += word.width; | |
} | |
if (!lines.length) | |
lines ~= null; | |
allLines ~= lines; | |
} | |
return allLines; | |
} | |
auto textWidth = paragraphs.map!(words => words.map!(word => word.width).sum + words.length - 1).reduce!max; | |
auto initWidth = cast(int)(sqrt(float(textWidth)) * 2).clamp(15, 100); | |
auto width = initWidth; | |
while (width && tryWrap(width - 1).length == tryWrap(width).length) | |
width--; | |
return tryWrap(width).join("<br/>").strip; | |
} | |
string skipUntilBalancedClosed(ref string s, char open, char close) | |
{ | |
int level = 1; | |
size_t p = 0; | |
while (true) | |
{ | |
if (s[p] == open) | |
level++; | |
else | |
if (s[p] == close) | |
{ | |
level--; | |
if (!level) | |
{ | |
auto r = s[0 .. p]; | |
s = s[p+1 .. $]; | |
return r; | |
} | |
} | |
p++; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment