Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f33cdb48d | ||
| 756e316866 | |||
| 1a26240503 | |||
| 430cdf5091 | |||
| 581e7c3a19 | |||
| 2d12f116b1 | |||
| 6b960151be | |||
| 4d3cac8a11 | |||
| e99af9b53f | |||
| 281ef742bb | |||
| 9dc1b60937 | |||
| 96628d74c2 | |||
| b575f8da0a | |||
| 1743d9c3f5 | |||
| c43f8e99f3 | |||
| c0a40bdcaf | |||
| af29fca070 | |||
| 11fb50c2d2 | |||
| 61c1eedebc | |||
| f89250fde1 | |||
| ead7e5202f | |||
| e9e4c9f802 |
4
demo.hxml
Normal file
4
demo.hxml
Normal file
@@ -0,0 +1,4 @@
|
||||
-lib hiss
|
||||
-cp src
|
||||
-main hank.StoryTellerDemo
|
||||
--interp
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "hank",
|
||||
"url": "https://github.com/NQNStudios/hank",
|
||||
"url": "https://github.com/hissvn/hank",
|
||||
"license": "MIT",
|
||||
"tags": ["cross", "game", "interactive-fiction", "ink"],
|
||||
"description": "Narrative scripting language for HaxeFlixel games based on Inkle's Ink engine",
|
||||
"version": "0.0.8",
|
||||
"releasenote": "It isn't safe to use this library yet.",
|
||||
"contributors": ["NQNStudios"],
|
||||
"classPath": "src/",
|
||||
"main": "hank.Test",
|
||||
"dependencies": {
|
||||
"hscript": "",
|
||||
"hiss": "",
|
||||
"utest": ""
|
||||
}
|
||||
}
|
||||
|
||||
87
src/hank/Story.hx
Normal file
87
src/hank/Story.hx
Normal file
@@ -0,0 +1,87 @@
|
||||
package hank;
|
||||
|
||||
import hiss.HTypes;
|
||||
import hiss.CCInterp;
|
||||
import hiss.StaticFiles;
|
||||
import hiss.HissReader;
|
||||
import hiss.HStream;
|
||||
import hiss.HissTools;
|
||||
using hiss.HissTools;
|
||||
using StringTools;
|
||||
|
||||
class Story {
|
||||
var teller: StoryTeller;
|
||||
var interp: CCInterp;
|
||||
var storyScript: String;
|
||||
// Separate reader for Hiss expressions:
|
||||
var reader: HissReader;
|
||||
var debug: Bool;
|
||||
|
||||
function hissRead(str: String) {
|
||||
return reader.read("", HStream.FromString(str));
|
||||
}
|
||||
|
||||
function debugPrint(val: HValue) {
|
||||
return if (debug) val.print() else val;
|
||||
}
|
||||
|
||||
public function new(storyScript: String, storyTeller: StoryTeller, debug = false) {
|
||||
this.debug = debug;
|
||||
|
||||
StaticFiles.compileWith("reader-macros.hiss");
|
||||
StaticFiles.compileWith("hanklib.hiss");
|
||||
|
||||
this.storyScript = storyScript;
|
||||
teller = storyTeller;
|
||||
}
|
||||
|
||||
public function run() {
|
||||
// TODO make a way to do all this loading before calling run(), but still make sure all the loading happens if it hasn't:
|
||||
interp = new CCInterp();
|
||||
reader = new HissReader(interp); // It references the same CCInterp but has its own readtable
|
||||
|
||||
interp.importFunction(teller, teller.handleOutput, "*handle-output*");
|
||||
interp.importFunction(teller, teller.handleChoices, "*handle-choices*");
|
||||
interp.importFunction(this, hissRead, "hiss-read");
|
||||
interp.importFunction(reader, reader.readDelimitedList, "hiss-read-delimited-list", List([Int(3)]) /* keep blankELements wrapped */, ["terminator", "delimiters", "start", "stream"]);
|
||||
|
||||
interp.importFunction(this, debugPrint, "print", T);
|
||||
|
||||
interp.load("hanklib.hiss");
|
||||
interp.load("reader-macros.hiss");
|
||||
|
||||
var storyCode = interp.readAll(StaticFiles.getContent(storyScript));
|
||||
|
||||
// This has to happen AFTER reading the story, for (while) reasons
|
||||
interp.truthy = (value) -> switch (value) {
|
||||
case Nil: false;
|
||||
case List([]): false;
|
||||
case Int(0): false;
|
||||
case String(""): false;
|
||||
default: true;
|
||||
};
|
||||
|
||||
if (debug) {
|
||||
String("Main logic:").message();
|
||||
storyCode.print();
|
||||
|
||||
String("").message();
|
||||
String("").message();
|
||||
String("").message();
|
||||
}
|
||||
|
||||
stackSafeEval(Symbol("begin").cons(storyCode));
|
||||
}
|
||||
|
||||
function stackSafeEval(exp: HValue) {
|
||||
try {
|
||||
interp.eval(exp);
|
||||
} catch (nextExp: String) {
|
||||
if (nextExp.startsWith("STACK-UNWIND")) {
|
||||
stackSafeEval(hissRead(nextExp.substr(13)));
|
||||
} else {
|
||||
throw nextExp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/hank/StoryTeller.hx
Normal file
12
src/hank/StoryTeller.hx
Normal file
@@ -0,0 +1,12 @@
|
||||
package hank;
|
||||
|
||||
import hiss.HTypes;
|
||||
|
||||
/**
|
||||
Due to the design of Hiss, every Hank story needs to be provided a StoryTeller to handle
|
||||
its output and choice using callbacks.
|
||||
**/
|
||||
interface StoryTeller {
|
||||
public function handleOutput(text: String, finished: (Int) -> Void): Void;
|
||||
public function handleChoices(choices: Array<String>, choose: (Int) -> Void): Void;
|
||||
}
|
||||
39
src/hank/StoryTellerDemo.hx
Normal file
39
src/hank/StoryTellerDemo.hx
Normal file
@@ -0,0 +1,39 @@
|
||||
package hank;
|
||||
|
||||
import hiss.HissReader;
|
||||
import hiss.HissTools;
|
||||
import hiss.StaticFiles;
|
||||
|
||||
class StoryTellerDemo implements StoryTeller {
|
||||
public static function main() {
|
||||
StaticFiles.compileWithAll("examples");
|
||||
|
||||
var examples = sys.FileSystem.readDirectory("src/hank/examples");
|
||||
var demo = new StoryTellerDemo();
|
||||
var debug = false;
|
||||
#if debug
|
||||
debug = true;
|
||||
#end
|
||||
|
||||
demo.handleChoices(examples, (index) -> {
|
||||
new Story("examples/" + examples[index] + "/main.hank", demo, debug).run();
|
||||
});
|
||||
}
|
||||
|
||||
public function new() {
|
||||
|
||||
}
|
||||
|
||||
public function handleOutput(text: String, finished: (Int) -> Void) {
|
||||
Sys.println(text);
|
||||
finished(0);
|
||||
}
|
||||
|
||||
public function handleChoices(choices: Array<String>, choose: (Int) -> Void) {
|
||||
var idx = 1;
|
||||
for (choice in choices) {
|
||||
Sys.println('${idx++}. ${choice}');
|
||||
}
|
||||
choose(Std.parseInt(Sys.stdin().readLine()) - 1);
|
||||
}
|
||||
}
|
||||
28
src/hank/StoryTester.hx
Normal file
28
src/hank/StoryTester.hx
Normal file
@@ -0,0 +1,28 @@
|
||||
package hank;
|
||||
|
||||
import hiss.HissReader;
|
||||
import hiss.HissTools;
|
||||
import hiss.StaticFiles;
|
||||
using StringTools;
|
||||
|
||||
class StoryTester implements StoryTeller {
|
||||
var transcriptLines: Array<String>;
|
||||
|
||||
public function new(testTranscript: String) {
|
||||
transcriptLines = StaticFiles.getContent(testTranscript).split("\n").map(StringTools.trim).filter((s) -> s.length > 0);
|
||||
}
|
||||
|
||||
public function handleOutput(text: String, finished: (Int) -> Void) {
|
||||
var expected = transcriptLines.shift();
|
||||
if (expected != text) throw 'expected "$expected" but output was "$text"';
|
||||
finished(0);
|
||||
}
|
||||
|
||||
public function handleChoices(choices: Array<String>, choose: (Int) -> Void) {
|
||||
for (choice in choices) {
|
||||
var expected = transcriptLines.shift().substr(1).trim(); // clip the *
|
||||
if (expected != choice) throw 'expected choice "$expected" but it was "$choice"';
|
||||
}
|
||||
choose(Std.parseInt(transcriptLines.shift().substr(1).trim()) - 1);
|
||||
}
|
||||
}
|
||||
22
src/hank/TestStoryExamples.hx
Normal file
22
src/hank/TestStoryExamples.hx
Normal file
@@ -0,0 +1,22 @@
|
||||
package hank;
|
||||
|
||||
using StringTools;
|
||||
import hiss.StaticFiles;
|
||||
|
||||
class TestStoryExamples {
|
||||
public static function main() {
|
||||
StaticFiles.compileWithAll("examples");
|
||||
|
||||
var examples = sys.FileSystem.readDirectory("src/hank/examples");
|
||||
|
||||
for (example in examples) {
|
||||
var transcripts = sys.FileSystem.readDirectory("src/hank/examples/" + example);
|
||||
transcripts = transcripts.filter((file) -> file.endsWith(".hlog"));
|
||||
|
||||
for (transcript in transcripts) {
|
||||
Sys.println(example + " " + transcript);
|
||||
new Story("examples/" + example + "/main.hank", new StoryTester("examples/" + example + "/" + transcript)).run();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/hank/examples/choices/main.hank
Normal file
16
src/hank/examples/choices/main.hank
Normal file
@@ -0,0 +1,16 @@
|
||||
-> start
|
||||
|
||||
== end
|
||||
|
||||
Good choice.
|
||||
|
||||
== start
|
||||
|
||||
You have to make a choice.
|
||||
|
||||
* (label) A
|
||||
-> start
|
||||
+ {(not label)} B
|
||||
-> start
|
||||
* C
|
||||
-> end
|
||||
19
src/hank/examples/choices/test1.hlog
Normal file
19
src/hank/examples/choices/test1.hlog
Normal file
@@ -0,0 +1,19 @@
|
||||
You have to make a choice.
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
> 2
|
||||
B
|
||||
You have to make a choice.
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
> 2
|
||||
B
|
||||
You have to make a choice.
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
> 3
|
||||
C
|
||||
Good choice.
|
||||
11
src/hank/examples/choices/test2.hlog
Normal file
11
src/hank/examples/choices/test2.hlog
Normal file
@@ -0,0 +1,11 @@
|
||||
You have to make a choice.
|
||||
* A
|
||||
* B
|
||||
* C
|
||||
> 1
|
||||
A
|
||||
You have to make a choice.
|
||||
* C
|
||||
> 1
|
||||
C
|
||||
Good choice.
|
||||
1
src/hank/examples/hello/main.hank
Normal file
1
src/hank/examples/hello/main.hank
Normal file
@@ -0,0 +1 @@
|
||||
Hello, world!
|
||||
1
src/hank/examples/hello/test1.hlog
Normal file
1
src/hank/examples/hello/test1.hlog
Normal file
@@ -0,0 +1 @@
|
||||
Hello, world!
|
||||
9
src/hank/examples/knots/main.hank
Normal file
9
src/hank/examples/knots/main.hank
Normal file
@@ -0,0 +1,9 @@
|
||||
-> start
|
||||
|
||||
== end
|
||||
We end here!
|
||||
|
||||
== start
|
||||
|
||||
We start here!
|
||||
-> end
|
||||
2
src/hank/examples/knots/test1.hlog
Normal file
2
src/hank/examples/knots/test1.hlog
Normal file
@@ -0,0 +1,2 @@
|
||||
We start here!
|
||||
We end here!
|
||||
79
src/hank/hanklib.hiss
Normal file
79
src/hank/hanklib.hiss
Normal file
@@ -0,0 +1,79 @@
|
||||
(defun divert (target)
|
||||
(funcall (eval (symbol target)))) // TODO resolve the target in scope
|
||||
|
||||
(defun final-divert (target)
|
||||
(let (exp (+ "STACK-UNWIND (divert \"" target "\")"))
|
||||
(error! exp))) // Just trust me on this one
|
||||
|
||||
(defun output (direct &rest parts)
|
||||
"Evaluate the text of this output, and either send it directly
|
||||
to the StoryTeller's callback, or return the result"
|
||||
(let (text
|
||||
(apply +
|
||||
(map parts eval-output-part)))
|
||||
(if direct
|
||||
(call/cc "Continuation of output"
|
||||
(lambda (cc)
|
||||
(*handle-output*
|
||||
text
|
||||
cc)))
|
||||
text)))
|
||||
|
||||
// TODO eval-output-part needs to access direct, and if direct, choose what comes after the bracket part. Else choose the bracket part
|
||||
(defun eval-output-part (part)
|
||||
(cond
|
||||
((string? part) part)))
|
||||
|
||||
(defstruct choice
|
||||
once-only
|
||||
depth
|
||||
label
|
||||
condition
|
||||
output
|
||||
chosen-count
|
||||
on-chosen)
|
||||
(defvar *choices* [])
|
||||
(defvar *labeled-choices* (dict))
|
||||
(defmacro defchoice (&rest args)
|
||||
"Define a new choice and return its id"
|
||||
`(let (c (make-choice ,@args)
|
||||
label (choice-label c)
|
||||
id (length *choices*))
|
||||
(print c)
|
||||
(push! *choices* c)
|
||||
(when label (dict-set! *labeled-choices* label c))
|
||||
id))
|
||||
|
||||
(defun get-player-choice-index (choice-texts)
|
||||
(call/cc "Continuation of get-player-choice-index"
|
||||
(lambda (cc)
|
||||
(*handle-choices* choice-texts cc))))
|
||||
|
||||
(defun choice-available (choice)
|
||||
(and
|
||||
(or
|
||||
(not (choice-once-only choice))
|
||||
(= 0 (choice-chosen-count choice)))
|
||||
(eval (choice-condition choice))))
|
||||
|
||||
(defun choice-point (choice-ids on-gather)
|
||||
(let (choices // not to be confused with the global list
|
||||
(filter (map choice-ids (bind nth *choices*)) choice-available)
|
||||
choice-outputs
|
||||
(map choices choice-output)
|
||||
choice-texts
|
||||
(for o choice-outputs (eval o)))
|
||||
(enable-continuations) // <-- I think this works here because everything after a choice point is part of the gather, which means nothing will be left in the begin() after it
|
||||
(begin
|
||||
(setlocal player-choice-index (print (get-player-choice-index choice-texts)))
|
||||
(disable-continuations)
|
||||
|
||||
(let (player-choice
|
||||
(nth choices player-choice-index))
|
||||
(choice-set-chosen-count! player-choice (+ 1 (choice-chosen-count player-choice)))
|
||||
// increment the choice's label var
|
||||
(when-let (label (choice-label player-choice))
|
||||
(let (label-var (symbol label))
|
||||
(eval `(defvar ,label-var (+ ,label-var 1)))))
|
||||
(eval `(output t ,@(slice (choice-output player-choice) 2)))
|
||||
(eval (choice-on-chosen player-choice))))))
|
||||
102
src/hank/reader-macros.hiss
Normal file
102
src/hank/reader-macros.hiss
Normal file
@@ -0,0 +1,102 @@
|
||||
(def-reader-macro "->" (start stream)
|
||||
(let (targets
|
||||
(hiss-read-delimited-list "\n" ["->"] t nil start stream)
|
||||
divert-calls
|
||||
(for target targets 'divert))
|
||||
(print (cons
|
||||
'begin
|
||||
(case (last targets)
|
||||
// tunnel statement:
|
||||
(nil
|
||||
(pop! targets)
|
||||
(pop! divert-calls)
|
||||
(zip divert-calls (map targets symbol-name)))
|
||||
// chain of diverts, the last of which is final
|
||||
(default
|
||||
(pop! divert-calls)
|
||||
(push! divert-calls 'final-divert)
|
||||
(zip divert-calls (map targets symbol-name))))))))
|
||||
|
||||
(def-reader-macro "==" (start stream)
|
||||
(let (knot-name
|
||||
(read-symbol "" stream)
|
||||
_
|
||||
(HStream:take-line stream)
|
||||
knot-body
|
||||
(read-all (first (HStream:take-until stream ["=="] nil nil t))))
|
||||
|
||||
// Define the knot's function at READ-TIME
|
||||
// TODO define the knot's read count
|
||||
(eval (print `(defun ,knot-name () ,@knot-body)))
|
||||
// TODO increment the knot's read count in that function
|
||||
nil))
|
||||
|
||||
(defun read-choice (start stream)
|
||||
"will be called with start=nil for all choices following the first in a choice point"
|
||||
(let (start-char
|
||||
(or start (HStream:peek stream 1))
|
||||
once-only
|
||||
(case start-char
|
||||
("*" t)
|
||||
("+" nil))
|
||||
depth
|
||||
(+ (if start 1 0) (HStream:count-consecutive stream start-char))
|
||||
label
|
||||
(case
|
||||
(begin
|
||||
(HStream:drop-whitespace stream)
|
||||
(HStream:peek stream 1))
|
||||
("("
|
||||
(trim (substring (first (HStream:take-until stream [")"])) 1))))
|
||||
condition
|
||||
(case
|
||||
(begin
|
||||
(HStream:drop-whitespace stream)
|
||||
(HStream:peek stream 1))
|
||||
("{"
|
||||
(hiss-read (trim (substring (first (HStream:take-until stream ["}"])) 1))))
|
||||
(default t))
|
||||
output
|
||||
(read-output "" stream t)
|
||||
on-chosen
|
||||
`(begin
|
||||
,@(read-all
|
||||
(first
|
||||
(HStream:take-until
|
||||
stream
|
||||
(cons "=" // stop reading choice point at knot or stitch
|
||||
(for delim
|
||||
[
|
||||
(* depth "*")
|
||||
(* depth "+")
|
||||
(* depth "-")
|
||||
]
|
||||
(+ delim " ")))
|
||||
nil nil t))))) // don't drop the terminator
|
||||
(when label (eval (print `(defvar ,(symbol label) 0)))) // start the label's variable at 0
|
||||
(defchoice once-only depth label condition output 0 on-chosen)))
|
||||
|
||||
|
||||
(defun read-choice-point (start stream)
|
||||
(let (first-choice
|
||||
(read-choice start stream)
|
||||
choices
|
||||
[first-choice])
|
||||
(while (contains ["*" "+"] (HStream:peek stream 1))
|
||||
(push! choices (read-choice nil stream)))
|
||||
`(choice-point
|
||||
',choices
|
||||
,(case (HStream:peek stream 1)
|
||||
("-" nil))))) // TODO decide how much to include in the on-gather */
|
||||
|
||||
(set-macro-string "*" read-choice-point)
|
||||
(set-macro-string "+" read-choice-point)
|
||||
|
||||
(defun read-output (start stream &optional choice)
|
||||
`(output ,(not choice) ,(HStream:take-line stream "rl")))
|
||||
// TODO read the conditional part of choices,
|
||||
// TODO expression insertions,
|
||||
// TODO inline diverts
|
||||
// TODO sequences/shuffles
|
||||
|
||||
(set-default-read-function read-output)
|
||||
Reference in New Issue
Block a user