public static Page TryLoad( StreamReader reader, Dictionary <string, Unit> unitsByUniqueId, Dictionary <string, ReactionArrow> reactionArrowsByUniqueId) { var result = new Page(); result.ActionText = null; result.Reactions = new Dictionary <string, ScoredReactionArrow>(); // End of file right at the beginning (maybe after an 'x' operation) indicates a valid end of the file. var line = reader.ReadLine(); if (line == null) { return(null); } while (true) { var parts = line.Split(SaveFileDelimiter); switch (parts[0]) { case "s": Setting setting; switch (parts[2]) { case "s": setting = new StringSetting(parts[3]); break; case "c": if (!int.TryParse(parts[3], out var chosen)) { throw new InvalidOperationException(string.Format($"Can't parse int chosen '{parts[3]}'.")); } if (!int.TryParse(parts[4], out var opportunity)) { throw new InvalidOperationException(string.Format($"Can't parse int opportunity '{parts[4]}'.")); } setting = new ScoreSetting(chosen, opportunity); break; case "b": switch (parts[3]) { case "0": setting = new BooleanSetting(false); break; case "1": setting = new BooleanSetting(true); break; default: throw new InvalidOperationException(string.Format($"Unexpected boolean value '{parts[3]}'.")); } break; default: throw new InvalidOperationException(string.Format($"Unexpected setting type '{parts[2]}'.")); } result.Settings.Add(parts[1], setting); break; case "n": result.NextTargetUnitOnReturn.Push(unitsByUniqueId[parts[1]]); break; case "a": result.ActionText = parts[1]; break; case "r": if (!double.TryParse(parts[2], out var score)) { throw new InvalidOperationException(string.Format($"Can't parse double score '{parts[2]}'.")); } result.Reactions.Add(parts[3], new ScoredReactionArrow(score, reactionArrowsByUniqueId[parts[1]])); break; case "x": // Flip the stack so it's going the right way. When you copy a stack, it flips it. result.NextTargetUnitOnReturn = new Stack <Unit>(result.NextTargetUnitOnReturn); return(result); default: throw new InvalidOperationException(string.Format($"Unexpected operation '{parts[0]}'.")); } line = reader.ReadLine(); if (line == null) { throw new InvalidOperationException(string.Format($"Unexpected end of save file.")); } } }
public TokenList( string sourceText, string sourceNameForErrorMessages, Dictionary <string, Setting> settings) { // Tokens only get created as part of this list of tokens. char pushedLetter = '\0'; char gottenLetter; int letterIndex; var specialIds = new List <string> { "John", "Jane", "Smith", "he", "He", "him", "Him", "his", "His", "himself", "Himself", "man", "Man", "boy", "Boy", "Mr", "Mrs", "she", "She", "her", "Her", "hers", "Hers", "herself", "Herself", "woman", "Woman", "girl", "Girl", "Ms" }; TheList = new List <Token>(); string textAccumulator = ""; int lineNumber = 1; letterIndex = 0; // This outer loop is for accumulating text strings while (true) { if (!GetLetter()) { if (textAccumulator.Length != 0) { TheList.Add(new Token(TokenType.Characters, textAccumulator, lineNumber)); } TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } switch (gottenLetter) { case '\n': // @ lets you explicitly put in a paragraph break. We'll clean up any extra spaces later. This lets you break contiguous text up into multiple lines within 'if' groups without having it affect formatting. textAccumulator += "\n"; ++lineNumber; break; case '\r': // Ignore the CR in CRLF. break; case '\t': textAccumulator += "\t"; break; // Didn't want to put this way at the bottom--gets lost. default: textAccumulator += gottenLetter; break; case '[': if (!GetLetter()) { TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } // Check for a [[ comment starter. if (gottenLetter != '[') { // No comment--pretend this never happened. UngetLetter(); } else { // [[ This is an example comment. ]] if (!GetComment()) { // If it returns false, it means you've reached the end. TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } // We got a text mode comment, so we break back out to the outer loop. break; // switch } // If we have accumulated a text string, record it as the next token before going into code mode. if (textAccumulator.Length != 0) { TheList.Add(new Token(TokenType.Characters, textAccumulator, lineNumber)); textAccumulator = ""; } // This inner loop is for breaking out control codes for (bool gotClosingBracket = false; !gotClosingBracket;) { if (!GetLetter()) { TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } switch (gottenLetter) { case ' ': case '\r': break; case '\n': ++lineNumber; break; case ']': // Leave code mode gotClosingBracket = true; break; case '=': TheList.Add(new Token(TokenType.Equal, "=", lineNumber)); break; case '.': TheList.Add(new Token(TokenType.Period, ".", lineNumber)); break; case ',': TheList.Add(new Token(TokenType.Comma, ",", lineNumber)); break; case '[': // Must be a comment. if (!GetLetter()) { TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } if (gottenLetter != '[') { throw new InvalidOperationException(string.Format($"file {sourceNameForErrorMessages} line {lineNumber}: expected '[[' but got '[{gottenLetter}' in\n{sourceText}")); } if (!GetComment()) { // If it returns false, it means you've reached the end. TheList.Add(new Token(TokenType.EndOfSourceText, "", lineNumber)); return; } // Done skipping comment. Move on to the next token. break; default: if (!char.IsLetterOrDigit(gottenLetter) || gottenLetter == '_') { throw new InvalidOperationException(string.Format($"file {sourceNameForErrorMessages} line {lineNumber}: unexpected character '{gottenLetter}' in\n{sourceText}")); } string id = ""; do { id += gottenLetter; if (!GetLetter()) { break; } } while (Char.IsLetterOrDigit(gottenLetter) || gottenLetter == '_' || gottenLetter == '.'); UngetLetter(); if (id == "if") { TheList.Add(new Token(TokenType.If, id, lineNumber)); } else if (id == "else") { TheList.Add(new Token(TokenType.Else, id, lineNumber)); } else if (id == "or") { TheList.Add(new Token(TokenType.Or, id, lineNumber)); } else if (id == "not") { TheList.Add(new Token(TokenType.Not, id, lineNumber)); } else if (id == "end") { TheList.Add(new Token(TokenType.End, id, lineNumber)); } else if (id == "when") { TheList.Add(new Token(TokenType.When, id, lineNumber)); } else if (id == "set") { TheList.Add(new Token(TokenType.Set, id, lineNumber)); } else if (id == "score") { TheList.Add(new Token(TokenType.Score, id, lineNumber)); } else if (id == "sort") { TheList.Add(new Token(TokenType.Sort, id, lineNumber)); } else if (id == "text") { TheList.Add(new Token(TokenType.Text, id, lineNumber)); } else if (id == "merge") { TheList.Add(new Token(TokenType.Merge, id, lineNumber)); } else if (id == "return") { TheList.Add(new Token(TokenType.Return, id, lineNumber)); } else if (id == "scene") { TheList.Add(new Token(TokenType.Scene, id, lineNumber)); } else if (specialIds.Contains(id)) { TheList.Add(new Token(TokenType.SpecialId, id, lineNumber)); } else { var type = settings.ContainsKey(id) ? settings[id] switch { ScoreSetting _ => TokenType.ScoreId, StringSetting _ => TokenType.StringId, BooleanSetting _ => TokenType.BooleanId, _ => throw new NotImplementedException(), // Just making the compiler happy. There is no other case. } : TokenType.Id; TheList.Add(new Token(type, id, lineNumber)); } break; } } break; }
public void Build( Unit firstUnit, string startingTrace) { // Starting with the current unit box, a) merge the texts of all units connected below it into one text, and b) collect all the reaction arrows. var accumulatedReactions = new Dictionary <string, ScoredReactionArrow>(); // The action text will contain all the merged action texts. var accumulatedActionTexts = startingTrace; // If there were no reaction arrows, we've reached an end point and need to return to var gotAReactionArrow = false; // Reactions are sorted by score, which is a floating point number. But some reactions may have the same score. So add a small floating-point sequence number to each one, to disambiguate them. double reactionScoreDisambiguator = 0; // Scores use this to compute whether you are above average in a score. Set it now, before creating the page, so it can be used in conditions evaluated throughout page creation. ScoreSetting.Average = Settings.Values.Where(setting => setting is ScoreSetting).Select(setting => (setting as ScoreSetting).ScoreValue).DefaultIfEmpty().Average(); if (Game.DebugMode) { accumulatedActionTexts += String.Format($"@{Game.PositiveDebugTextStart}average = {ScoreSetting.Average:0.00}%{Game.DebugTextStop}"); } // This recursive routine will accumulate all the action and reaction text values in the above variables. Accumulate(firstUnit); while (!gotAReactionArrow) { // We got to a dead end without finding any reaction options for the player. So pop back to a pushed location and continue merging from there. if (!NextTargetUnitOnReturn.Any()) { throw new InvalidOperationException(string.Format($"Got to a dead end with no place to return to.")); } var unit = NextTargetUnitOnReturn.Pop(); if (Game.DebugMode) { accumulatedActionTexts += "@" + Game.PositiveDebugTextStart + "pop " + unit.UniqueId + Game.DebugTextStop; } Accumulate(unit); } ActionText = FixPlus(accumulatedActionTexts); Reactions = accumulatedReactions; void Accumulate( Unit unit) { // First append this action box's own text and execute any settings. if (accumulatedActionTexts.Length == 0) { accumulatedActionTexts = EvaluateText(unit.ActionCode); } else { accumulatedActionTexts += " " + EvaluateText(unit.ActionCode); } EvaluateSettingsAndScores(unit.ActionCode, out string trace1); accumulatedActionTexts += trace1; // Next examine all the arrows for the action. var allWhensFailed = true; var whenElseArrows = new List <Arrow>(); var returnArrows = new List <ReturnArrow>(); foreach (var arrow in unit.GetArrows()) { if (arrow is ReturnArrow returnArrow) { // We'll deal with these return arrows at the end of the loop. returnArrows.Add(returnArrow); } else if (EvaluateWhenElse(arrow.Code)) { // Save 'when else' arrows for possible later execution. whenElseArrows.Add(arrow); } else { // If conditions in the arrow are false, then just ignore the arrow completely. This includes all types of arrows. (var succeeded, var hadWhen) = EvaluateWhen(arrow.Code, out string trace2); accumulatedActionTexts += trace2; if (!succeeded) { continue; } if (hadWhen) { allWhensFailed = false; } AccumulateArrow(arrow); } } if (allWhensFailed) { // If none of the 'when EXPRESSIONS' arrows succeeded, execute the 'when else' arrows now. foreach (var arrow in whenElseArrows) { AccumulateArrow(arrow); } } if (returnArrows.Any()) { // Create a unit on the fly and push it on the stack for execution on return. This converts the return arrows to merge arrows. var returnUnit = Unit.BuildReturnUnitFor(returnArrows); NextTargetUnitOnReturn.Push(returnUnit); } } void AccumulateArrow( Arrow arrow) { switch (arrow) { case MergeArrow mergeArrow: // There may be 'set' parameters for a referential merge. EvaluateSettingsAndScores(arrow.Code, out string trace); accumulatedActionTexts += trace; if (Game.DebugMode) { accumulatedActionTexts += "@" + Game.PositiveDebugTextStart + "merge" + (mergeArrow.DebugSceneId != null ? " " + mergeArrow.DebugSceneId : "") + Game.DebugTextStop; } // There are two kinds of merge arrows. Unit targetUnit; if (mergeArrow.TargetSceneUnit != null) { targetUnit = mergeArrow.TargetSceneUnit; // When we finish the jump to the other scene, we will continue merging with the action this arrow points to. NextTargetUnitOnReturn.Push(mergeArrow.TargetUnit); } else { // It's a local merge arrow. Merge the action it points to. // It should be impossible for it to have no target. Let it crash if that's the case. targetUnit = mergeArrow.TargetUnit; } // Call this routine again recursively. It will append the target's text and examine the target's arrows. Accumulate(targetUnit); break; case ReactionArrow reactionArrow: gotAReactionArrow = true; double highestScore = 0; var reactionText = EvaluateText(reactionArrow.Code); // There's a little trickiness with links here... if (reactionText.Length > 0 && reactionText[0] == '{') { // If it's in braces, it refers to a hyperlink already in the text. Don't make a new hyperlink for it. Just take off the braces. When the user clicks on the link, it won't have braces. reactionText = reactionText.Substring(1); var end = reactionText.IndexOf("}"); if (end != -1) { reactionText = reactionText.Substring(0, end); } // -1 tells the UI to not put embedded hyperlinks on the list on the screen. highestScore = -1; } else { // Sort by scores. var highestScoreIdPlusSpace = ""; reactionArrow.Code.Traverse((code, originalSourceText) => { if (!(code is ScoreCode scoreCode)) { return(true); } foreach (var id in scoreCode.Ids) { ScoreSetting scoreSetting; if (Settings.TryGetValue(id, out Setting setting)) { scoreSetting = setting as ScoreSetting; } else { scoreSetting = new ScoreSetting(); Settings.Add(id, scoreSetting); } var value = scoreSetting.ScoreValue; if (value > highestScore) { highestScore = value; highestScoreIdPlusSpace = id + " "; } } return(true); }); if (Game.DebugMode) { reactionText = Game.PositiveDebugTextStart + highestScoreIdPlusSpace + ((int)(highestScore * 100)).ToString() + "% " + Game.DebugTextStop + reactionText; } } accumulatedReactions[reactionText] = new ScoredReactionArrow(highestScore, reactionArrow); reactionScoreDisambiguator += 0.00001; break; } } }
public static Page?TryLoad( TextReader reader, World world) { // Loads saved story state data and links it to the static world description. Either loads and returns a valid Page or returns null if it reaches the end of the reader. Run this multiple times to read multiple Pages from the reader. var settings = new Dictionary <string, Setting>(world.Settings); string actionText = ""; var reactions = new Dictionary <string, ScoredReactionArrow>(); var nextTargetNodeOnReturn = new Stack <Node>(); // End of file right at the beginning (maybe after an 'x' operation) indicates a valid end of the file. That means we're done reading all the pages in the file. var line = reader.ReadLine(); if (line == null) { return(null); } while (true) { var parts = line.Split(SaveFileDelimiter); switch (parts[0]) { case "s": Setting setting; switch (parts[2]) { case "s": setting = new StringSetting(parts[3]); break; case "c": if (!int.TryParse(parts[3], out var chosen)) { throw new InvalidOperationException(string.Format($"Can't parse int chosen '{parts[3]}'.")); } if (!int.TryParse(parts[4], out var opportunity)) { throw new InvalidOperationException(string.Format($"Can't parse int opportunity '{parts[4]}'.")); } setting = new ScoreSetting(chosen, opportunity); break; case "b": switch (parts[3]) { case "0": setting = new BooleanSetting(false); break; case "1": setting = new BooleanSetting(true); break; default: throw new InvalidOperationException(string.Format($"Unexpected boolean value '{parts[3]}'.")); } break; default: throw new InvalidOperationException(string.Format($"Unexpected setting type '{parts[2]}'.")); } settings[parts[1]] = setting; break; case "n": nextTargetNodeOnReturn.Push(world.NodesByUniqueId[parts[1]]); break; case "a": actionText = parts[1]; break; case "r": if (!double.TryParse(parts[2], out var score)) { throw new InvalidOperationException(string.Format($"Can't parse double score '{parts[2]}'.")); } reactions.Add(parts[3], new ScoredReactionArrow(score, world.ReactionArrowsByUniqueId[parts[1]])); break; case "x": // Flip the stack so it's going the right way. When you copy a stack, it flips it. nextTargetNodeOnReturn = new Stack <Node>(nextTargetNodeOnReturn); return(new Page(actionText, reactions, settings, nextTargetNodeOnReturn)); default: throw new InvalidOperationException(string.Format($"Unexpected operation '{parts[0]}'.")); } line = reader.ReadLine(); if (line == null) { throw new InvalidOperationException(string.Format($"Unexpected end of save file.")); } } }