Threads are in! Closed #38

This commit is contained in:
2019-06-06 14:23:10 -06:00
parent b05aa5b872
commit ace33560e1
6 changed files with 211 additions and 18 deletions

View 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

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

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

View File

@@ -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)) {

View File

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

View File

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