diff --git a/compiler/InkParser/InkParser_Expressions.cs b/compiler/InkParser/InkParser_Expressions.cs index 106f8953..ea361cdc 100644 --- a/compiler/InkParser/InkParser_Expressions.cs +++ b/compiler/InkParser/InkParser_Expressions.cs @@ -188,7 +188,7 @@ protected Expression ExpressionUnary() // - Since we allow numbers at the start of variable names, variable names are checked before literals // - Function calls before variable names in case we see parentheses - var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral) as Expression; + var expr = OneOf (ExpressionList, ExpressionParen, ExpressionFunctionCall, ExpressionVariableName, ExpressionLiteral, ExpressionStack) as Expression; // Only recurse immediately if we have one of the (usually optional) unary ops if (expr == null && prefixOp != null) { @@ -415,6 +415,17 @@ private InfixOperator ParseInfixOperator() return null; } + + protected Parsed.Stack ExpressionStack() { + Whitespace(); + if(ParseString ("[") == null) { return null; }; + Whitespace(); + List contents = SeparatedList(Expression, Spaced(String(","))); + Whitespace(); + if (ParseString("]") == null) { return null;}; + return new Stack(contents); + } + protected Parsed.List ExpressionList () { Whitespace (); diff --git a/compiler/InkParser/InkParser_Logic.cs b/compiler/InkParser/InkParser_Logic.cs index df8b04fd..73be90e8 100644 --- a/compiler/InkParser/InkParser_Logic.cs +++ b/compiler/InkParser/InkParser_Logic.cs @@ -97,8 +97,8 @@ protected Parsed.Object VariableDeclaration() var expr = definition as Parsed.Expression; if (expr) { - if (!(expr is Number || expr is StringExpression || expr is DivertTarget || expr is VariableReference || expr is List)) { - Error ("initial value for a variable must be a number, constant, list or divert target"); + if (!(validInitialValue(expr) || expr is Stack)) { + Error ("initial value for a variable must be a number, constant, list, stack or divert target"); } if (Parse (ListElementDefinitionSeparator) != null) @@ -110,6 +110,15 @@ protected Parsed.Object VariableDeclaration() if (!strExpr.isSingleString) Error ("Constant strings cannot contain any logic."); } + // Ensure stack literals don't contain invalid values for initial value + else if (expr is Stack) { + var stackExpr = expr as Stack; + foreach(var value in stackExpr.contents) { + if(!(validInitialValue(value))) { + Error ("initial values in a stack variable must be a number, constant, list or divert target"); + } + } + } var result = new VariableAssignment (varName, expr); result.isGlobalDeclaration = true; @@ -117,6 +126,10 @@ protected Parsed.Object VariableDeclaration() } return null; + + bool validInitialValue(Expression expression) { + return (expression is Number || expression is StringExpression || expression is DivertTarget || expression is VariableReference || expression is List); + } } protected Parsed.VariableAssignment ListDeclaration () diff --git a/compiler/ParsedHierarchy/DivertTarget.cs b/compiler/ParsedHierarchy/DivertTarget.cs index c1223a49..f77be1d4 100644 --- a/compiler/ParsedHierarchy/DivertTarget.cs +++ b/compiler/ParsedHierarchy/DivertTarget.cs @@ -61,6 +61,10 @@ public override void ResolveReferences (Story context) } foundUsage = true; } + else if (usageParent is Stack) { + badUsage = false; + foundUsage = true; + } else if (usageParent is Expression) { badUsage = true; foundUsage = true; diff --git a/compiler/ParsedHierarchy/FunctionCall.cs b/compiler/ParsedHierarchy/FunctionCall.cs index d3d40f5c..1e377fde 100644 --- a/compiler/ParsedHierarchy/FunctionCall.cs +++ b/compiler/ParsedHierarchy/FunctionCall.cs @@ -16,6 +16,9 @@ public class FunctionCall : Expression public bool isListRange { get { return name == "LIST_RANGE"; } } public bool isListRandom { get { return name == "LIST_RANDOM"; } } public bool isReadCount { get { return name == "READ_COUNT"; } } + public bool isStackPopNewest { get { return name == "STACK_POP_NEWEST"; } } + public bool isStackPopOldest { get { return name == "STACK_POP_OLDEST"; } } + public bool isStackPopRandom { get { return name == "STACK_POP_RANDOM"; } } public bool shouldPopReturnedValue; @@ -122,6 +125,48 @@ public override void GenerateIntoContainer (Runtime.Container container) container.AddContent (Runtime.ControlCommand.ListRandom ()); + } else if (isStackPopNewest) { + if (arguments == null || arguments.Count != 2) + Error("STACK_POP_NEWEST should take 2 parameter - a stack, and a variable to assign the popped value to"); + + var reference = arguments[1]; + if (!(reference is VariableReference)) + { + Error("STACK_POP_NEWEST should take 2 parameter - a stack, and a variable to assign the popped value to"); + } + + + arguments[0].GenerateIntoContainer(container); + container.AddContent(new Runtime.VariablePointerValue((reference as VariableReference).name)); + container.AddContent(Runtime.ControlCommand.StackPopNewest()); + + } else if (isStackPopOldest) { + if (arguments == null || arguments.Count != 2) + Error("STACK_POP_OLDEST should take 2 parameter - a stack, and a variable to assign the popped value to"); + + var reference = arguments[1]; + if (!(reference is VariableReference)) + { + Error("STACK_POP_OLDEST should take 2 parameter - a stack, and a variable to assign the popped value to"); + } + + arguments[0].GenerateIntoContainer(container); + container.AddContent(new Runtime.VariablePointerValue((reference as VariableReference).name)); + container.AddContent(Runtime.ControlCommand.StackPopOldest()); + + } else if (isStackPopRandom) { + if (arguments == null || arguments.Count != 2) + Error("STACK_POP_RANDOM should take 2 parameter - a stack, and a variable to assign the popped value to"); + + var reference = arguments[1]; + if (!(reference is VariableReference)) + { + Error("STACK_POP_RANDOM should take 2 parameter - a stack, and a variable to assign the popped value to"); + } + + arguments[0].GenerateIntoContainer(container); + container.AddContent(new Runtime.VariablePointerValue((reference as VariableReference).name)); + container.AddContent(Runtime.ControlCommand.StackPopRandom()); } else if (Runtime.NativeFunctionCall.CallExistsWithName (name)) { var nativeCall = Runtime.NativeFunctionCall.CallWithName (name); diff --git a/compiler/ParsedHierarchy/Stack.cs b/compiler/ParsedHierarchy/Stack.cs new file mode 100644 index 00000000..e8af00a0 --- /dev/null +++ b/compiler/ParsedHierarchy/Stack.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; + +namespace Ink.Parsed +{ + public class Stack : Parsed.Expression + { + public List contents; + + public Stack (List expressions) + { + this.contents = expressions ?? new List(); + } + + public override void ResolveReferences(Story context) + { + base.ResolveReferences(context); + foreach(var content in contents) { + content.ResolveReferences(context); + } + } + + // Only known after GenerateIntoContainer has run + public bool isValidGlobalStackLiteral; + + public override void GenerateIntoContainer(Runtime.Container container) + { + // Assume true until we find a counter + isValidGlobalStackLiteral = true; + + if (contents != null) { + foreach (var valueExpression in contents) { + valueExpression.parent = this; + valueExpression.GenerateIntoContainer(container); + var variableReference = valueExpression as VariableReference; + if(variableReference && !variableReference.isConstantReference && !variableReference.isListItemReference) { + isValidGlobalStackLiteral = false; + } + } + } + + var count = contents == null ? 0 : contents.Count; + container.AddContent(new Runtime.IntValue(count)); + + container.AddContent(Runtime.ControlCommand.StackLiteralEnd()); + } + } +} diff --git a/compiler/ParsedHierarchy/VariableAssignment.cs b/compiler/ParsedHierarchy/VariableAssignment.cs index 34e281e2..4819ecb1 100644 --- a/compiler/ParsedHierarchy/VariableAssignment.cs +++ b/compiler/ParsedHierarchy/VariableAssignment.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; namespace Ink.Parsed { @@ -89,6 +90,10 @@ public override void ResolveReferences (Story context) if (variableReference && !variableReference.isConstantReference && !variableReference.isListItemReference) { Error ("global variable assignments cannot refer to other variables, only literal values, constants and list items"); } + var stack = expression as Stack; + if(stack && !stack.isValidGlobalStackLiteral) { + Error ("global variable assignments cannot refer to other variables, only literal values, constants and list items"); + } } if (!this.isNewTemporaryDeclaration) { diff --git a/ink-engine-runtime/ControlCommand.cs b/ink-engine-runtime/ControlCommand.cs index a4e59d45..f2f21720 100644 --- a/ink-engine-runtime/ControlCommand.cs +++ b/ink-engine-runtime/ControlCommand.cs @@ -33,6 +33,10 @@ public enum CommandType ListRandom, BeginTag, EndTag, + StackPopNewest, + StackPopOldest, + StackPopRandom, + StackLiteralEnd, //---- TOTAL_VALUES } @@ -173,6 +177,26 @@ public static ControlCommand EndTag () return new ControlCommand (CommandType.EndTag); } + public static ControlCommand StackPopNewest() + { + return new ControlCommand(CommandType.StackPopNewest); + } + + public static ControlCommand StackPopOldest() + { + return new ControlCommand(CommandType.StackPopOldest); + } + + public static ControlCommand StackPopRandom() + { + return new ControlCommand(CommandType.StackPopRandom); + } + + public static ControlCommand StackLiteralEnd() + { + return new ControlCommand(CommandType.StackLiteralEnd); + } + public override string ToString () { return commandType.ToString(); diff --git a/ink-engine-runtime/InkStack.cs b/ink-engine-runtime/InkStack.cs new file mode 100644 index 00000000..caf73108 --- /dev/null +++ b/ink-engine-runtime/InkStack.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Collections; +using System.Text; + +namespace Ink.Runtime +{ + /// + /// The underlying type for a stack value in ink. + /// + public class InkStack: IEnumerable + { + /// + /// Create a new empty stack + /// + public InkStack() { + _values = new List(); + } + + /// + /// Create a new ink stack that contains copies of the values in + /// an other stack + /// + public InkStack(InkStack otherStack) + { + _values = new List(otherStack); + } + + /// + /// Create a new empty stack + /// + public InkStack(IEnumerable values) { + _values = new List(values); + } + + /// + /// Create a new ink stack that contains a single value + /// + public InkStack(Value v) + { + _values = new List(); + _values.Add(v); + } + + /// + /// Adds the given item to the ink stack. + /// + public InkStack AddItem(Value value) + { + return Addition(new InkStack(value)); + } + + /// + /// Returns a new stack that is the combination of the current stack and one that's + /// passed in. Equivalent to calling (stack1 + stack2) in ink. + /// + public InkStack Addition(InkStack otherStack) + { + var result = new InkStack(this); + result._values.AddRange(otherStack); + return result; + } + + /// + /// Returns a new stack that has the items in the second stack removed; + /// if there are duplicate values in the original stack, only the oldest + /// instance will be removed. + /// + public InkStack Subtract(InkStack otherStack) + { + var result = new InkStack(this); + foreach(var v in otherStack._values) { + var found = result._values.FindIndex((target) => target.valueObject.Equals(v.valueObject)); + if(found != -1) { + result._values.RemoveAt(found); + } + } + return result; + } + + public int Count { + get { return _values.Count; } + } + + /// + /// Returns true if the passed object is also an ink stack that contains + /// the same items as the current stack, false otherwise. + /// + public override bool Equals(object other) + { + var otherRawStack = other as InkStack; + if (otherRawStack == null) return false; + if (otherRawStack.Count != Count) return false; + + for (int i = 0; i < Count; i++) + { + if (!otherRawStack._values[i].valueObject.Equals(_values[i].valueObject)) + return false; + } + + return true; + } + + /// + /// Take the most recently added item from the stack (if one exists) + /// + public InkStack PopNewest(out Runtime.Object popped) { + if(Count == 0) { + popped = new Runtime.Void(); + return this; + } else { + var resultStack = new InkStack(this); + popped = resultStack._values[resultStack.Count - 1]; + resultStack._values.RemoveAt(resultStack.Count - 1); + return resultStack; + } + } + + /// + /// Take the least recently added item from the stack (if one exists) + /// + public InkStack PopOldest(out Runtime.Object popped) { + if(Count == 0) { + popped = new Runtime.Void(); + return this; + } else { + var resultStack = new InkStack(this); + popped = resultStack._values[0]; + resultStack._values.RemoveAt(0); + return resultStack; + } + } + + /// + /// Take the Nth item from the stack (if one exists) + /// + public InkStack PopNth(out Runtime.Object popped, int index) { + if(Count <= index || index < 0) { + popped = new Runtime.Void(); + return this; + } else { + var resultStack = new InkStack(this); + popped = resultStack._values[index]; + resultStack._values.RemoveAt(index); + return resultStack; + } + } + + /// + /// Return the hashcode for this object, used for comparisons and inserting into dictionaries. + /// + public override int GetHashCode() + { + int ownHash = 0; + foreach (var v in this) + ownHash += v.GetHashCode(); + return ownHash; + } + + /// + /// Returns a string in the form "a, b, c" with the items in the stack in order. + /// + public override string ToString() + { + var sb = new StringBuilder(); + for (int i = 0; i < Count; i++) + { + if (i > 0) + sb.Append(", "); + + sb.Append(_values[i].ToString()); + } + + return sb.ToString(); + } + + List _values; + + IEnumerator IEnumerable.GetEnumerator() { + return _values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return _values.GetEnumerator(); + } + } +} diff --git a/ink-engine-runtime/JsonSerialisation.cs b/ink-engine-runtime/JsonSerialisation.cs index b6ca4ab3..3411e906 100644 --- a/ink-engine-runtime/JsonSerialisation.cs +++ b/ink-engine-runtime/JsonSerialisation.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; @@ -152,6 +151,21 @@ public static void WriteRuntimeObject(SimpleJson.Writer writer, Runtime.Object o return; } + var stackVal = obj as StackValue; + if (stackVal) + { + writer.WriteObjectStart(); + writer.WriteProperty("stack", (w) => { + w.WriteArrayStart(); + foreach(var v in stackVal.value) { + WriteRuntimeObject(w, v); + } + w.WriteArrayEnd(); + }); + writer.WriteObjectEnd(); + return; + } + var divTargetVal = obj as DivertTargetValue; if (divTargetVal) { @@ -299,6 +313,7 @@ public static Dictionary JObjectToIntDictionary(Dictionary": "path.target"} // {"^var": "varname", "ci": 0} + // {"stack": [5, {"^->": "path.target"}] } // // Container: [...] // [..., @@ -497,6 +512,13 @@ public static Runtime.Object JTokenToRuntimeObject(object token) return new ListValue (rawList); } + // Stack value + if (obj.TryGetValue ("stack", out propValue)) { + var stackContent = (List)propValue; + var stack = new InkStack(stackContent.Select((pv) => (Value)JTokenToRuntimeObject(pv))); + return new StackValue(stack); + } + // Used when serialising save state only if (obj ["originalChoicePath"] != null) return JObjectToChoice (obj); @@ -735,6 +757,10 @@ static Json() _controlCommandNames [(int)ControlCommand.CommandType.ListFromInt] = "listInt"; _controlCommandNames [(int)ControlCommand.CommandType.ListRange] = "range"; _controlCommandNames [(int)ControlCommand.CommandType.ListRandom] = "lrnd"; + _controlCommandNames [(int)ControlCommand.CommandType.StackPopNewest] = "stnew"; + _controlCommandNames [(int)ControlCommand.CommandType.StackPopOldest] = "stold"; + _controlCommandNames [(int)ControlCommand.CommandType.StackPopRandom] = "strnd"; + _controlCommandNames [(int)ControlCommand.CommandType.StackLiteralEnd] = "stend"; _controlCommandNames [(int)ControlCommand.CommandType.BeginTag] = "#"; _controlCommandNames [(int)ControlCommand.CommandType.EndTag] = "/#"; diff --git a/ink-engine-runtime/NativeFunctionCall.cs b/ink-engine-runtime/NativeFunctionCall.cs index 32101008..a5121e8f 100644 --- a/ink-engine-runtime/NativeFunctionCall.cs +++ b/ink-engine-runtime/NativeFunctionCall.cs @@ -45,6 +45,11 @@ public class NativeFunctionCall : Runtime.Object public const string ValueOfList = "LIST_VALUE"; public const string Invert = "LIST_INVERT"; + public const string StackCount = "STACK_COUNT"; + public const string PopOldest = "STACK_POP_OLDEST"; + public const string PopNewest = "STACK_POP_NEWEST"; + public const string PopRandom = "STACK_POP_RANDOM"; + public static NativeFunctionCall CallWithName(string functionName) { return new NativeFunctionCall (functionName); @@ -101,6 +106,19 @@ public Runtime.Object Call(List parameters) hasList = true; } + // In the case of binary operations with stacks, we treat the + // second value as a stack with one item in it. This allows + // most operations to work as expected; add adds an item to + // the stack, equality is true if the stack contains only that + // one value, etc. + if ( parameters.Count == 2 && parameters[0] is StackValue && !(parameters[1] is StackValue)) { + var callParams = new List(); + callParams.Add(parameters[0] as StackValue); + var y = new StackValue(new InkStack(parameters[1] as Value)); + callParams.Add(y); + return Call(callParams); + } + // Binary operations on lists are treated outside of the standard coerscion rules if( parameters.Count == 2 && hasList ) return CallBinaryListOperation (parameters); @@ -118,6 +136,8 @@ public Runtime.Object Call(List parameters) return Call (coercedParams); } else if (coercedType == ValueType.List) { return Call (coercedParams); + } else if (coercedType == ValueType.Stack) { + return Call(coercedParams); } return null; @@ -419,6 +439,15 @@ static void GenerateNativeFunctionsIfNecessary() AddListUnaryOp (Count, (x) => x.Count); AddListUnaryOp (ValueOfList, (x) => x.maxItem.Value); + // Stack operations + AddStackBinaryOp(Add, (x, y) => x.Addition(y)); + AddStackBinaryOp(Subtract, (x, y) => x.Subtract(y)); + AddStackBinaryOp(Equal, (x, y) => x.Equals(y)); + AddStackBinaryOp(And, (x, y) => x.Count > 0 && y.Count > 0); + AddStackBinaryOp(Or, (x, y) => x.Count > 0 || y.Count > 0); + + AddStackUnaryOp(StackCount, (x) => x.Count); + // Special case: The only operations you can do on divert target values BinaryOp divertTargetsEqual = (Path d1, Path d2) => { return d1.Equals (d2); @@ -482,6 +511,16 @@ static void AddListUnaryOp (string name, UnaryOp op) AddOpToNativeFunc (name, 1, ValueType.List, op); } + static void AddStackBinaryOp (string name, BinaryOp op) + { + AddOpToNativeFunc(name, 2, ValueType.Stack, op); + } + + static void AddStackUnaryOp (string name, UnaryOp op) + { + AddOpToNativeFunc(name, 1, ValueType.Stack, op); + } + static void AddFloatUnaryOp(string name, UnaryOp op) { AddOpToNativeFunc (name, 1, ValueType.Float, op); diff --git a/ink-engine-runtime/Story.cs b/ink-engine-runtime/Story.cs index 45292b9b..2c2a978a 100644 --- a/ink-engine-runtime/Story.cs +++ b/ink-engine-runtime/Story.cs @@ -1648,6 +1648,62 @@ bool PerformLogicAndFlowControl(Runtime.Object contentObj) break; } + case ControlCommand.CommandType.StackPopNewest: { + var resultVar = state.PopEvaluationStack() as VariablePointerValue; + if (resultVar == null) + throw new StoryException("Expected variable reference for STACK_POP_NEWEST"); + var stackArg = state.PopEvaluationStack() as StackValue; + if (stackArg == null) + throw new StoryException("Expected stack for STACK_POP_NEWEST"); + Runtime.Object popped; + var stackResult = stackArg.value.PopNewest(out popped); + state.variablesState.Assign(new Runtime.VariableAssignment(resultVar.value, false), popped); + state.PushEvaluationStack(new StackValue(stackResult)); + break; + } + case ControlCommand.CommandType.StackPopOldest: { + var resultVar = state.PopEvaluationStack() as VariablePointerValue; + if (resultVar == null) + throw new StoryException("Expected variable reference for STACK_POP_OLDEST"); + var stackArg = state.PopEvaluationStack() as StackValue; + if (stackArg == null) + throw new StoryException("Expected stack for STACK_POP_OLDEST"); + Runtime.Object popped; + var stackResult = stackArg.value.PopOldest(out popped); + state.variablesState.Assign(new Runtime.VariableAssignment(resultVar.value, false), popped); + state.PushEvaluationStack(new StackValue(stackResult)); + break; + } + case ControlCommand.CommandType.StackPopRandom: { + var resultVar = state.PopEvaluationStack() as VariablePointerValue; + if (resultVar == null) + throw new StoryException("Expected variable reference for STACK_POP_RANDOM"); + var stackArg = state.PopEvaluationStack() as StackValue; + if (stackArg == null) + throw new StoryException("Expected stack for STACK_POP_RANDOM"); + + // Generate a random index for the element to take + var resultSeed = state.storySeed + state.previousRandom; + var random = new Random (resultSeed); + + var nextRandom = random.Next (); + Runtime.Object popped; + var stackResult = stackArg.value.PopNth(out popped, nextRandom % stackArg.value.Count); + state.variablesState.Assign(new Runtime.VariableAssignment(resultVar.value, false), popped); + state.PushEvaluationStack(new StackValue(stackResult)); + state.previousRandom = nextRandom; + + break; + } + + case ControlCommand.CommandType.StackLiteralEnd: { + var stackCount = state.PopEvaluationStack() as IntValue; + var contents = state.PopEvaluationStack(stackCount.value); + var runtimeStack = new InkStack(contents.Select((v) => (Value)v)); + state.PushEvaluationStack(new StackValue(runtimeStack)); + break; + } + default: Error ("unhandled ControlCommand: " + evalCommand); break; diff --git a/ink-engine-runtime/Value.cs b/ink-engine-runtime/Value.cs index f870dffa..23553dc4 100644 --- a/ink-engine-runtime/Value.cs +++ b/ink-engine-runtime/Value.cs @@ -22,7 +22,8 @@ public enum ValueType // Not used for coersion described above DivertTarget, - VariablePointer + VariablePointer, + Stack } public abstract class Value : Runtime.Object @@ -58,6 +59,8 @@ public static Value Create(object val) return new DivertTargetValue ((Path)val); } else if (val is InkList) { return new ListValue ((InkList)val); + } else if (val is InkStack) { + return new StackValue((InkStack)val); } return null; @@ -400,6 +403,51 @@ public static void RetainListOriginsForAssignment (Runtime.Object oldValue, Runt newList.value.SetInitialOriginNames (oldList.value.originNames); } } + + public class StackValue : Value + { + public override ValueType valueType { + get { + return ValueType.Stack; + } + } + + // Truthy if it is non-empty + public override bool isTruthy { + get { + return value.Count > 0; + } + } + + public StackValue () : base(null) { + value = new InkStack (); + } + + public StackValue (InkStack stack) : base (null) + { + value = stack; + } + + /// There's an argument that stack could be coerced in + /// some ways, but given they only support equality comparison + /// it seems like it wouldn't be much gain + public override Value Cast(ValueType newType) + { + if (newType == valueType) + return this; + + if (newType == ValueType.Bool) + return new BoolValue(isTruthy); + + throw BadCastException (newType); + } + + public override Object Copy() + { + return new StackValue(this.value); + } + + } } diff --git a/tests/Tests.cs b/tests/Tests.cs index 8b51f6d5..cbfabb21 100644 --- a/tests/Tests.cs +++ b/tests/Tests.cs @@ -3645,6 +3645,212 @@ public void TestListRange() Two, Three, Four, Five, Six One, Two, Three Pizza, Pasta +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackEquality() + { + var storyStr = + @" +LIST food = butter, bread, marmite +VAR just_butter = [bread, butter] +VAR sandwich = [bread, butter, marmite] +VAR dropped_sandwich = [marmite, butter, bread] +VAR lunch = [bread, butter, marmite] + +{ +- just_butter == lunch: + Boooooring +- dropped_sandwich == lunch: + Oops. +- sandwich == lunch: + Nice +} +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"Nice +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackPopOldest() + { + var storyStr = + @" +LIST food = bread, butter +VAR myStack = [butter] +~myStack += bread +VAR popped = () +{myStack} +~myStack = STACK_POP_OLDEST(myStack, popped) +{popped} +~myStack = STACK_POP_OLDEST(myStack, popped) +{popped} +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"butter, bread +butter +bread +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + + [Test()] + public void TestStackWithDiverts() + { + var storyStr = + @" +VAR myStack = [ -> target ] +VAR myTarget = -> not_target + +~STACK_POP_NEWEST(myStack, myTarget) + +-> myTarget + +=== target +This is the way +-> END + +=== not_target +This is not the way +-> END +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"This is the way +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackPopNewest() + { + var storyStr = + @" +LIST food = bread, butter +VAR myStack = [] +~myStack += butter +~myStack += bread +VAR popped = () +{myStack} +~myStack = STACK_POP_NEWEST(myStack, popped) +{popped} +~myStack = STACK_POP_NEWEST(myStack, popped) +{popped} +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"butter, bread +bread +butter +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackAddition() + { + var storyStr = + @" +VAR myStack1 = [111, ""hello""] +VAR myStack2 = [222, ""flowers""] +VAR myStack = [] +~myStack = myStack1 + myStack2 +{myStack} +~myStack = myStack2 + myStack1 +{myStack} +~myStack += 333 +{myStack} +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"111, hello, 222, flowers +222, flowers, 111, hello +222, flowers, 111, hello, 333 +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackSubtraction() + { + var storyStr = + @" +VAR myStack = [111, ""hello"", 111, ""flowers""] +{myStack} +~myStack -= 111 +{myStack} +~myStack += 111 +{myStack} +~myStack -= [111, 111] +{myStack} +"; + + var story = CompileString(storyStr); + + Assert.AreEqual( +@"111, hello, 111, flowers +hello, 111, flowers +hello, 111, flowers, 111 +hello, flowers +".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); + } + + [Test()] + public void TestStackingThreads() + { + var storyStr = + @" +LIST cards = Punch1, Punch2, Kick1, Kick2, Dodge1, Dodge2 +VAR hand = [] +VAR played = () + +Hello + +~hand += Punch1 +~hand += Punch1 +~hand += Kick1 + +-> branching(hand) -> + +Done! You played {played}! + +=== branching(options) +{ STACK_COUNT(options) == 0: + * ->-> +- else: + ~temp nextOption = () + ~temp rest = STACK_POP_NEWEST(options, nextOption) + + <- branching(rest) + * [{nextOption}] + ~played += nextOption + ->-> +} +"; + + var story = CompileString(storyStr); + var intro = story.ContinueMaximally(); + + Assert.AreEqual( +@"Hello +".Replace(Environment.NewLine, "\n"), intro); + Assert.AreEqual("Punch1", story.currentChoices[0].text); + Assert.AreEqual("Punch1", story.currentChoices[1].text); + Assert.AreEqual("Kick1", story.currentChoices[2].text); + story.ChooseChoiceIndex(0); + Assert.AreEqual( +@"Done! You played Punch1! ".Replace(Environment.NewLine, "\n"), story.ContinueMaximally()); } @@ -4257,4 +4463,4 @@ public void TestCharacterRangeIdentifiersForDivertNamesWithAsciiSuffix() private class TestWarningException : System.Exception { } } -} \ No newline at end of file +}