From ace33560e144b54c96c64f12f7bf87c37d3f7872 Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Thu, 6 Jun 2019 14:23:10 -0600 Subject: [PATCH] Threads are in! Closed #38 --- examples/threads/main.hank | 32 +++++++ examples/threads/test1.hlog | 10 +++ examples/threads/test2.hlog | 11 +++ hank/HankAST.hx | 4 + hank/Parser.hx | 8 ++ hank/Story.hx | 164 ++++++++++++++++++++++++++++++++---- 6 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 examples/threads/main.hank create mode 100644 examples/threads/test1.hlog create mode 100644 examples/threads/test2.hlog diff --git a/examples/threads/main.hank b/examples/threads/main.hank new file mode 100644 index 0000000..fa9f854 --- /dev/null +++ b/examples/threads/main.hank @@ -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 \ No newline at end of file diff --git a/examples/threads/test1.hlog b/examples/threads/test1.hlog new file mode 100644 index 0000000..dcfbdd1 --- /dev/null +++ b/examples/threads/test1.hlog @@ -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. \ No newline at end of file diff --git a/examples/threads/test2.hlog b/examples/threads/test2.hlog new file mode 100644 index 0000000..4dd6bfe --- /dev/null +++ b/examples/threads/test2.hlog @@ -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. \ No newline at end of file diff --git a/hank/HankAST.hx b/hank/HankAST.hx index faff3d2..d5c1316 100644 --- a/hank/HankAST.hx +++ b/hank/HankAST.hx @@ -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)) { diff --git a/hank/Parser.hx b/hank/Parser.hx index d2aab64..433f2ee 100644 --- a/hank/Parser.hx +++ b/hank/Parser.hx @@ -13,6 +13,7 @@ import hank.HankBuffer; class Parser { static var symbols: Array 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)); } diff --git a/hank/Story.hx b/hank/Story.hx index 6272842..2296cfe 100644 --- a/hank/Story.hx +++ b/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 = new Map(); var parser: Parser; - + var embedMode: EmbedMode = Tunnel; var embeddedBlocks: Array = []; var parent: Option = 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): - if (target.length == 0) { + // 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) { + for (choice in choices) { trace (choice.toString());} + trace('---'); + } private function availableChoices(): Array { 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) { @@ -221,8 +330,13 @@ class Story { choices.push(choice); } } - } + } + } + // 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) { - return embeddedBlocks[0].choose(choiceIndex); + 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)); }