22 Commits
dev ... hiss

Author SHA1 Message Date
Nat Nelson
5f33cdb48d uncommitted changes 2022-04-28 15:19:34 -06:00
756e316866 Choice condition & label var 2020-10-04 23:36:52 -06:00
1a26240503 disable once-only choices after the first time 2020-10-04 22:53:41 -06:00
430cdf5091 Fix the example tests 2020-10-04 18:25:30 -06:00
581e7c3a19 label continuations 2020-10-04 18:22:37 -06:00
2d12f116b1 Don't try to read hiss expressions from all errorsgap 2020-10-04 18:14:33 -06:00
6b960151be rename debug-print 2020-10-04 15:42:30 -06:00
4d3cac8a11 tests for new examples 2020-10-04 15:27:38 -06:00
e99af9b53f make prints from the demo optional 2020-10-04 15:14:19 -06:00
281ef742bb reorganizing demo/examples 2020-10-04 14:02:15 -06:00
9dc1b60937 stack-safe diverts 2020-10-04 13:51:01 -06:00
96628d74c2 hiss-read more ergonomic 2020-10-04 13:50:11 -06:00
b575f8da0a rudimentary choices with stack overflow problems 2020-10-03 18:23:12 -06:00
1743d9c3f5 Reading choices 2020-10-03 16:17:29 -06:00
c43f8e99f3 allow choosing between demos 2020-10-03 13:02:33 -06:00
c0a40bdcaf async architecture 2020-10-03 12:50:38 -06:00
af29fca070 implement knots.hank up-to-date 2020-10-03 11:44:50 -06:00
11fb50c2d2 knots example 2020-10-03 10:32:17 -06:00
61c1eedebc Update repo url 2020-06-30 11:58:28 -06:00
f89250fde1 WIP RC Bank Heist demo 2020-05-11 14:35:57 -06:00
ead7e5202f New, callback-based architecture 2020-05-10 21:54:54 -06:00
e9e4c9f802 Implementing Hank as a hiss variant 2020-04-25 17:25:33 +01:00
17 changed files with 440 additions and 2 deletions

4
demo.hxml Normal file
View File

@@ -0,0 +1,4 @@
-lib hiss
-cp src
-main hank.StoryTellerDemo
--interp

View File

@@ -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
View 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
View 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;
}

View 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
View 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);
}
}

View 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();
}
}
}
}

View 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

View 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.

View 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.

View File

@@ -0,0 +1 @@
Hello, world!

View File

@@ -0,0 +1 @@
Hello, world!

View File

@@ -0,0 +1,9 @@
-> start
== end
We end here!
== start
We start here!
-> end

View File

@@ -0,0 +1,2 @@
We start here!
We end here!

79
src/hank/hanklib.hiss Normal file
View 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
View 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)

4
test.hxml Normal file
View File

@@ -0,0 +1,4 @@
-lib hiss
-cp src
-main hank.TestStoryExamples
--interp