Threads are in! Closed #38
This commit is contained in:
32
examples/threads/main.hank
Normal file
32
examples/threads/main.hank
Normal file
@@ -0,0 +1,32 @@
|
||||
// Based on https://github.com/inkle/ink/blob/01898140be43d29baac70c1cb6544bdb10164209/Documentation/WritingWithInk.md#threads-join-multiple-sections-together
|
||||
|
||||
-> thread_example
|
||||
|
||||
== thread_example ==
|
||||
I had a headache; threading is hard to get your head around.
|
||||
<- conversation
|
||||
<- walking
|
||||
|
||||
I added this line.
|
||||
* And this choice. -> house
|
||||
|
||||
|
||||
== conversation ==
|
||||
It was a tense moment for Monty and me.
|
||||
* "What did you have for lunch today?"[] I asked.
|
||||
"Spam and eggs," he replied.
|
||||
* "Nice weather, we're having,"[] I said.
|
||||
"I've seen better," he replied.
|
||||
- -> house
|
||||
|
||||
== walking ==
|
||||
We continued to walk down the dusty road.
|
||||
* [Continue walking]
|
||||
-> house
|
||||
|
||||
== house ==
|
||||
Before long, we arrived at his house.
|
||||
|
||||
|
||||
// TODO test nested threading
|
||||
// TODO test threading with divert target variables
|
||||
10
examples/threads/test1.hlog
Normal file
10
examples/threads/test1.hlog
Normal file
@@ -0,0 +1,10 @@
|
||||
I had a headache; threading is hard to get your head around.
|
||||
It was a tense moment for Monty and me.
|
||||
We continued to walk down the dusty road.
|
||||
I added this line.
|
||||
* "What did you have for lunch today?"
|
||||
* "Nice weather, we're having,"
|
||||
* Continue walking
|
||||
* And this choice.
|
||||
> 3:
|
||||
Before long, we arrived at his house.
|
||||
11
examples/threads/test2.hlog
Normal file
11
examples/threads/test2.hlog
Normal file
@@ -0,0 +1,11 @@
|
||||
I had a headache; threading is hard to get your head around.
|
||||
It was a tense moment for Monty and me.
|
||||
We continued to walk down the dusty road.
|
||||
I added this line.
|
||||
* "What did you have for lunch today?"
|
||||
* "Nice weather, we're having,"
|
||||
* Continue walking
|
||||
* And this choice.
|
||||
> 1: "What did you have for lunch today?" I asked.
|
||||
"Spam and eggs," he replied.
|
||||
Before long, we arrived at his house.
|
||||
@@ -11,6 +11,7 @@ enum ExprType {
|
||||
EOutput(o: Output);
|
||||
|
||||
EDivert(target: String);
|
||||
EThread(target: String);
|
||||
EKnot(name: String);
|
||||
EStitch(name: String);
|
||||
ENoOp;
|
||||
@@ -61,6 +62,9 @@ class ASTExtension {
|
||||
public static function collectChoices(ast: HankAST, startingIndex: Int, depth: Int): ChoicePointInfo {
|
||||
var choices = [];
|
||||
var lastChoiceIndex = 0;
|
||||
if (startingIndex > ast.length || startingIndex < 0) {
|
||||
throw 'Trying to collect choices starting from expr ${startingIndex+1}/${ast.length}';
|
||||
}
|
||||
var currentFile = ast[startingIndex].position.file;
|
||||
|
||||
for (i in startingIndex... findEOF(ast, currentFile)) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import hank.HankBuffer;
|
||||
class Parser {
|
||||
static var symbols: Array<Map<String, HankBuffer -> HankBuffer.Position -> ExprType>> = [
|
||||
['INCLUDE ' => include],
|
||||
['<-' => thread],
|
||||
['->' => divert],
|
||||
['===' => knot],
|
||||
['==' => knot],
|
||||
@@ -147,6 +148,13 @@ class Parser {
|
||||
return EDivert(tokens[0]);
|
||||
}
|
||||
|
||||
static function thread(buffer: HankBuffer, position: HankBuffer.Position) : ExprType {
|
||||
buffer.drop('<-');
|
||||
buffer.skipWhitespace();
|
||||
var tokens = lineTokens(buffer, 1, position, true, true);
|
||||
return EThread(tokens[0]);
|
||||
}
|
||||
|
||||
static function output(buffer: HankBuffer, position: HankBuffer.Position) : ExprType {
|
||||
return EOutput(Output.parse(buffer));
|
||||
}
|
||||
|
||||
156
hank/Story.hx
156
hank/Story.hx
@@ -9,6 +9,7 @@ import haxe.ds.Option;
|
||||
using hank.Extensions;
|
||||
using HankAST.ASTExtension;
|
||||
import hank.Choice;
|
||||
using Choice.ChoiceExtension;
|
||||
import hank.Choice.FallbackChoice;
|
||||
import hank.HankAST.ExprType;
|
||||
import hank.StoryTree;
|
||||
@@ -25,6 +26,17 @@ enum StoryFrame {
|
||||
|
||||
typedef InsertionHook = Dynamic -> String;
|
||||
|
||||
enum EmbedMode {
|
||||
/**
|
||||
The current embedded Stories are to be executed sequentially until they return Finished
|
||||
*/
|
||||
Tunnel;
|
||||
/**
|
||||
The current embedded Stories are to be executed sequentially until they return HasChoices. Choices will be aggregated into a single frame
|
||||
*/
|
||||
Thread;
|
||||
}
|
||||
|
||||
/**
|
||||
Runtime interpreter for Hank stories.
|
||||
**/
|
||||
@@ -44,7 +56,7 @@ class Story {
|
||||
var altInstances: Map<Alt, AltInstance> = new Map();
|
||||
|
||||
var parser: Parser;
|
||||
|
||||
var embedMode: EmbedMode = Tunnel;
|
||||
var embeddedBlocks: Array<Story> = [];
|
||||
var parent: Option<Story> = None;
|
||||
|
||||
@@ -72,12 +84,30 @@ class Story {
|
||||
function embeddedStory(h: String): Story {
|
||||
var ast = parser.parseString(h);
|
||||
|
||||
var story = new Story(random, parser, ast, storyTree, nodeScopes, viewCounts, hInterface);
|
||||
var story = new Story(
|
||||
|
||||
random, // embedded stories must continue giving deterministic random numbers without resetting -- to avoid exploitable behavior
|
||||
parser,
|
||||
|
||||
ast,// embedded stories have their OWN AST of Hank statements
|
||||
// but they keep the parent's view count tree and current scope
|
||||
storyTree,
|
||||
nodeScopes, viewCounts, hInterface);
|
||||
story.exprIndex = 0;
|
||||
story.parent = Some(this);
|
||||
return story;
|
||||
}
|
||||
|
||||
function storyFork(t: String): Story {
|
||||
// Everything is the same as when embedding blocks, but a fork uses the same AST as its parent -- simply starting after a hypothetical divert
|
||||
var story = new Story(random, parser, this.ast, storyTree, nodeScopes, viewCounts, hInterface);
|
||||
story.parent = Some(this);
|
||||
// trace('story parent: ${story.parent.match(Some(_))}');
|
||||
story.divertTo(t);
|
||||
return story;
|
||||
|
||||
}
|
||||
|
||||
public static function FromAST(script: String, ast: HankAST, ?randomSeed: Int): Story {
|
||||
var random = new Random(randomSeed);
|
||||
var storyTree = StoryNode.FromAST(ast);
|
||||
@@ -124,18 +154,59 @@ class Story {
|
||||
return f;
|
||||
default:
|
||||
}
|
||||
|
||||
// trace (embeddedBlocks.length);
|
||||
// trace (embedMode);
|
||||
while (embeddedBlocks.length > 0) {
|
||||
switch (embedMode) {
|
||||
case Tunnel:
|
||||
var nf = embeddedBlocks[0].nextFrame();
|
||||
if(nf == Finished) {
|
||||
embeddedBlocks.remove(embeddedBlocks[0]);
|
||||
} else {
|
||||
return nf;
|
||||
}
|
||||
case Thread:
|
||||
var idx = 0;
|
||||
while (idx < embeddedBlocks.length) {
|
||||
var nf = embeddedBlocks[idx].nextFrame();
|
||||
switch (nf) {
|
||||
case HasChoices(_) | Finished:
|
||||
// trace('Hit end of flow for thread $idx');
|
||||
idx++;
|
||||
//exprIndex++;
|
||||
continue;
|
||||
default:
|
||||
return nf;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// All of the threaded blocks are out of content, so follow the original fork until it also runs out of flow.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (exprIndex >= ast.length) {
|
||||
if (embedMode == Thread) {
|
||||
//trace('Warning: Hit EOF while threading (are you sure you meant to do that?)');
|
||||
return nextChoiceFrame();
|
||||
}
|
||||
return Finished;
|
||||
}
|
||||
return processExpr(ast[exprIndex].expr);
|
||||
|
||||
// trace('It fell to the roots next expr: ${ast[exprIndex].expr}');
|
||||
var rootNf = processExpr(ast[exprIndex].expr);
|
||||
|
||||
|
||||
// if (parent == None) trace('root frame: $rootNf');
|
||||
|
||||
if (embedMode == Thread) {
|
||||
if (rootNf == Finished) {
|
||||
return nextChoiceFrame();
|
||||
}
|
||||
}
|
||||
|
||||
return rootNf;
|
||||
}
|
||||
|
||||
private function processExpr(expr: ExprType): StoryFrame {
|
||||
@@ -154,12 +225,27 @@ class Story {
|
||||
hInterface.runEmbeddedHaxe(h, nodeScopes);
|
||||
return nextFrame();
|
||||
case EDivert(target):
|
||||
// Fallback choices simply advance flow using divert syntax
|
||||
if (target.length == 0) {
|
||||
exprIndex += 1;
|
||||
} else {
|
||||
}
|
||||
// All other diverts:
|
||||
else {
|
||||
divertTo(target);
|
||||
}
|
||||
return nextFrame();
|
||||
case EThread(target):
|
||||
// The thread only needs to be added once
|
||||
exprIndex++;
|
||||
embedMode = Thread;
|
||||
// trace('before: ${embeddedBlocks.length}');
|
||||
embeddedBlocks.push(storyFork(target));
|
||||
// trace('after: ${embeddedBlocks.length}');
|
||||
// ^ These before/after comments help diagnose whether divert() is erasing the embedded blocks before they can start
|
||||
//trace ('starting thread $target');
|
||||
var nf = nextFrame();
|
||||
// trace('frame immediately after: $nf');
|
||||
return nf;
|
||||
|
||||
case EGather(label, depth, nextExpr):
|
||||
// gathers need to update their view counts
|
||||
@@ -180,6 +266,16 @@ class Story {
|
||||
return nextFrame();
|
||||
}
|
||||
|
||||
return nextChoiceFrame();
|
||||
|
||||
default:
|
||||
trace('$expr is not implemented');
|
||||
return Finished;
|
||||
}
|
||||
return Finished;
|
||||
}
|
||||
|
||||
private function nextChoiceFrame() {
|
||||
var optionsText = [for(c in availableChoices()) c.output.format(this, hInterface, random, altInstances, nodeScopes, false)];
|
||||
if (optionsText.length > 0) {
|
||||
return finalChoiceProcessing(optionsText);
|
||||
@@ -198,15 +294,28 @@ class Story {
|
||||
return nextFrame();
|
||||
}
|
||||
}
|
||||
default:
|
||||
trace('$expr is not implemented');
|
||||
return Finished;
|
||||
}
|
||||
return Finished;
|
||||
}
|
||||
|
||||
private function traceChoiceArray(choices: Array<Choice>) {
|
||||
for (choice in choices) { trace (choice.toString());}
|
||||
trace('---');
|
||||
}
|
||||
private function availableChoices(): Array<Choice> {
|
||||
var choices = [];
|
||||
|
||||
// If we're threading, collect all the childrens' choices, too.
|
||||
if (embedMode == Thread) {
|
||||
var idx = 0;
|
||||
for (thread in embeddedBlocks) {
|
||||
|
||||
choices = choices.concat(thread.availableChoices());
|
||||
// trace('after fork $idx:');
|
||||
// traceChoiceArray(choices);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
if (exprIndex < ast.length && ast[exprIndex].expr.match(EChoice(_))) {
|
||||
var allChoices = ast.collectChoices(exprIndex, weaveDepth).choices;
|
||||
for (choice in allChoices) {
|
||||
if (choicesTaken.indexOf(choice.id) == -1 || !choice.onceOnly) {
|
||||
@@ -222,7 +331,12 @@ class Story {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trace('final:');
|
||||
// traceChoiceArray(choices);
|
||||
|
||||
//traceChoiceArray(choices);
|
||||
return choices;
|
||||
}
|
||||
|
||||
@@ -284,7 +398,7 @@ class Story {
|
||||
return;
|
||||
}
|
||||
switch (parent) {
|
||||
case Some(p):
|
||||
case Some(p) if(p.embedMode == Tunnel):
|
||||
// A divert from inside embedded hank, must leave the embedded context
|
||||
p.embeddedBlocks = [];
|
||||
p.divertTo(target);
|
||||
@@ -350,12 +464,25 @@ class Story {
|
||||
}
|
||||
|
||||
public function choose(choiceIndex: Int): String {
|
||||
// If embedded, let the embedded section evaluate the choice
|
||||
if (embeddedBlocks.length > 0) {
|
||||
var nf =nextFrame() ;
|
||||
if (!nf.match(HasChoices(_))) {
|
||||
throw 'Trying to make a choice when next frame is $nf';
|
||||
}
|
||||
// trace('choosing $choiceIndex');
|
||||
// If tunnel-embedded, let the proper embedded section evaluate the choice
|
||||
if (embeddedBlocks.length > 0 && embedMode == Tunnel) {
|
||||
return embeddedBlocks[0].choose(choiceIndex);
|
||||
|
||||
} else {
|
||||
// if not embedded, actually make the choice
|
||||
return evaluateChoice(availableChoices()[choiceIndex]);
|
||||
|
||||
// if not embedded, actually make the choice. avalaibleChoices() accounts for aggregating threaded choices
|
||||
var output = evaluateChoice(availableChoices()[choiceIndex]);
|
||||
if (embedMode == Thread) {
|
||||
embedMode = Tunnel;
|
||||
embeddedBlocks = [];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +521,7 @@ class Story {
|
||||
|
||||
/** Parse and run embedded Hank script on the fly. **/
|
||||
public function runEmbeddedHank(h: String) {
|
||||
embedMode = Tunnel;
|
||||
embeddedBlocks.push(embeddedStory(h));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user