diff --git a/hank/Choice.hx b/hank/Choice.hx index 99dfe83..655f638 100644 --- a/hank/Choice.hx +++ b/hank/Choice.hx @@ -3,8 +3,9 @@ package hank; import haxe.ds.Option; typedef Choice = {id:Int, onceOnly:Bool, label:Option, condition:Option, depth:Int, output:Output, divertTarget:Option}; -typedef ChoicePointInfo = {choices:Array, fallbackIndex:Int}; -typedef FallbackChoice = {choice:Choice, index:Int}; +typedef ChoiceInfo = {choice: Choice, tags: Array}; +typedef ChoicePointInfo = {choices:Array, fallbackIndex:Int}; +typedef FallbackChoiceInfo = {choiceInfo:ChoiceInfo, index:Int}; class ChoiceExtension { public static function toString(choice:Choice):String { diff --git a/hank/Extensions.hx b/hank/Extensions.hx index 37dbeb6..cce9da9 100644 --- a/hank/Extensions.hx +++ b/hank/Extensions.hx @@ -1,6 +1,8 @@ package hank; +using StringTools; import haxe.ds.Option; +import hank.HankBuffer; class Extensions { public static function unwrap(o:Option):T { @@ -16,4 +18,27 @@ class Extensions { public static function toIterable(i:Void->Iterator): Iterable { return { iterator: i }; } + + public static function tokenize(s: String): Array { + var tokenStartIndices = [ ]; + var whitespaceIndices = [ ]; + var tokens = [ ]; + var lastWasChar = false; + s = s.trim(); + + for (i in 0...s.length) { + if (s.isSpace(i)) { + lastWasChar = false; + if (lastWasChar) whitespaceIndices.push(i); + } + else { + if (!lastWasChar) tokenStartIndices.push(i); + lastWasChar = true; + } + } + + whitespaceIndices.push(null); // This ensures the last token will substr() correctly + + return [for (i in 0...tokenStartIndices.length) s.substr(tokenStartIndices[i], whitespaceIndices[i])]; + } } diff --git a/hank/HankAST.hx b/hank/HankAST.hx index 778ac2d..ee6c486 100644 --- a/hank/HankAST.hx +++ b/hank/HankAST.hx @@ -2,6 +2,7 @@ package hank; import haxe.ds.Option; import hank.Alt.AltInstance; +import hank.Choice.ChoiceInfo; import hank.Choice.ChoicePointInfo; enum ExprType { @@ -16,8 +17,10 @@ enum ExprType { EHaxeLine(haxe:String); EHaxeBlock(haxe:String); EGather(label:Option, depth:Int, expr:ExprType); - // Choices are the most complicated expressions + // Hank pre-tag-implementation: Choices are the most complicated expressions EChoice(c:Choice); + // Tags: Hold my beer + ETagged(e: ExprType, tags:Array); } typedef HankExpr = { @@ -53,12 +56,19 @@ class ASTExtension { return -1; } + static function tryAddFunc(choices: Array, expectedDepth: Int, c: Choice, tags: Array) { + var valid = (c.depth == expectedDepth); + if (valid) choices.push({choice:c,tags:tags}); + return valid; + } + /** Collect every choice in the choice point starting at the given index. **/ public static function collectChoices(ast:HankAST, startingIndex:Int, depth:Int):ChoicePointInfo { - var choices = []; + var choices = new Array(); var lastChoiceIndex = 0; + var tryAdd = tryAddFunc.bind(choices, depth); if (startingIndex > ast.length || startingIndex < 0) { throw 'Trying to collect choices starting from expr ${startingIndex + 1}/${ast.length}'; } @@ -68,12 +78,9 @@ class ASTExtension { switch (ast[i].expr) { // Gather choices of the current depth case EChoice(choice): - if (choice.depth != depth) - continue; - else { - lastChoiceIndex = i; - choices.push(choice); - }; + if (tryAdd(choice, [])) lastChoiceIndex = i; + case ETagged(EChoice(choice), tags): + if (tryAdd(choice, tags)) lastChoiceIndex = 1; // Stop at the next gather of this depth case EGather(_, d, _) if (d == depth): break; @@ -103,8 +110,9 @@ class ASTExtension { for (i in 0...ast.length) { var expr = ast[i].expr; switch (expr) { - case EChoice(c) if (c.id == id): - return i; + case EChoice(c) | ETagged(EChoice(c), _): + if (c.id == id) + return i; default: } } diff --git a/hank/Parser.hx b/hank/Parser.hx index 01d7dae..beaf13b 100644 --- a/hank/Parser.hx +++ b/hank/Parser.hx @@ -23,7 +23,8 @@ class Parser { ['```' => haxeBlock], ['-' => gather], ['*' => choice], - ['+' => choice]]; + ['+' => choice], + ['#' => tag]]; var buffers:Array = []; var ast:HankAST = []; @@ -223,6 +224,13 @@ class Parser { }); } + static function tag(buffer:HankBuffer, position: HankBuffer.Position):ExprType { + buffer.drop('#'); + var tagLine = buffer.takeLine('lr').unwrap(); + var tags = tagLine.tokenize(); + return ETagged(parseExpr(buffer, position), tags); + } + static function haxeBlock(buffer:HankBuffer, position:HankBuffer.Position):ExprType { buffer.drop('```\n'); var rawContents = buffer.takeUntil(['```'], false, true).unwrap().output; diff --git a/hank/Story.hx b/hank/Story.hx index 9d44aae..4f7c5d9 100644 --- a/hank/Story.hx +++ b/hank/Story.hx @@ -12,10 +12,10 @@ using hank.Extensions; using HankAST.ASTExtension; import hank.Choice; - +import hank.Choice.ChoiceInfo; +import hank.Choice.FallbackChoiceInfo; using Choice.ChoiceExtension; -import hank.Choice.FallbackChoice; import hank.HankAST.ExprType; import hank.StoryTree; import hank.Alt.AltInstance; @@ -307,37 +307,37 @@ class Story { private function nextChoiceFrame() { var optionsText = [ - for (c in availableChoices()) - c.output.format(this, hInterface, random, altInstances, nodeScopes, false) + for (choiceInfo in availableChoices()) + choiceInfo.choice.output.format(this, hInterface, random, altInstances, nodeScopes, false) ]; if (optionsText.length > 0) { return finalChoiceProcessing(optionsText); } else { var fallback = fallbackChoice(); - switch (fallback.choice.divertTarget) { + switch (fallback.choiceInfo.choice.divertTarget) { case Some(t) if (t.length > 0): - var fallbackText = evaluateChoice(fallback.choice); + var fallbackText = evaluateChoice(fallback.choiceInfo.choice); if (fallbackText.length > 0) { throw 'For some reason a fallback choice evaluated to text!'; } return nextFrame(); default: exprIndex = fallback.index + 1; - weaveDepth = fallback.choice.depth + 1; + weaveDepth = fallback.choiceInfo.choice.depth + 1; return nextFrame(); } } } - private function traceChoiceArray(choices:Array) { - for (choice in choices) { - trace(choice.toString()); + private function traceChoiceArray(choices:Array) { + for (choiceInfo in choices) { + trace('${choiceInfo.choice.toString()}: #${choiceInfo.tags.join(" #")}'); } trace('---'); } - private function availableChoices():Array { - var choices = []; + private function availableChoices():Array { + var choices = new Array(); // If we're threading, collect all the childrens' choices, too. if (embedMode == Thread) { @@ -351,18 +351,18 @@ class Story { } 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) { - switch (choice.condition) { + var allChoiceInfo = ast.collectChoices(exprIndex, weaveDepth).choices; + for (choiceInfo in allChoiceInfo) { + if (choicesTaken.indexOf(choiceInfo.choice.id) == -1 || !choiceInfo.choice.onceOnly) { + switch (choiceInfo.choice.condition) { case Some(expr): if (!hInterface.cond(expr, nodeScopes)) { continue; } case None: } - if (!choice.output.isEmpty()) { - choices.push(choice); + if (!choiceInfo.choice.output.isEmpty()) { + choices.push(choiceInfo); } } } @@ -375,11 +375,11 @@ class Story { return choices; } - private function fallbackChoice():FallbackChoice { + private function fallbackChoice():FallbackChoiceInfo { var choiceInfo = ast.collectChoices(exprIndex, weaveDepth); var lastChoice = choiceInfo.choices[choiceInfo.choices.length - 1]; - if (lastChoice.output.isEmpty()) { - return {choice: lastChoice, index: choiceInfo.fallbackIndex}; + if (lastChoice.choice.output.isEmpty()) { + return {choiceInfo: lastChoice, index: choiceInfo.fallbackIndex}; } else { throw 'there is no fallback choice!'; } @@ -526,7 +526,7 @@ class Story { return embeddedBlocks[0].choose(choiceIndex); } else { // if not embedded, actually make the choice. avalaibleChoices() accounts for aggregating threaded choices - var output = evaluateChoice(availableChoices()[choiceIndex]); + var output = evaluateChoice(availableChoices()[choiceIndex].choice); if (embedMode == Thread) { embedMode = Tunnel; embeddedBlocks = [];