diff --git a/Ficdown.Parser.Tests/IntegrationTests.cs b/Ficdown.Parser.Tests/IntegrationTests.cs index 7b4a57b..72af546 100644 --- a/Ficdown.Parser.Tests/IntegrationTests.cs +++ b/Ficdown.Parser.Tests/IntegrationTests.cs @@ -1,10 +1,6 @@ namespace Ficdown.Parser.Tests { - using System; - using System.Linq; using System.Text; - using Player; - using ServiceStack.Text; using TestStories; using Xunit; @@ -16,13 +12,6 @@ var parser = new FicdownParser(); var storyText = Encoding.UTF8.GetString(Resources.TheRobotKing); var story = parser.ParseStory(storyText); - Assert.NotNull(story); - Assert.Equal("The Robot King", story.Name); - Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name); - - var traverser = new GameTraverser(story); - var test = traverser.Enumerate(); - Console.WriteLine(test.Take(10).Dump()); } } } diff --git a/Ficdown.Parser/FicDownParser.cs b/Ficdown.Parser/FicDownParser.cs index 86a024a..e94b6d2 100644 --- a/Ficdown.Parser/FicDownParser.cs +++ b/Ficdown.Parser/FicDownParser.cs @@ -1,32 +1,43 @@ -namespace Ficdown.Parser +using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")] + +namespace Ficdown.Parser { using System; - using Model.Story; + using System.Collections.Generic; + using Model.Parser; using Parser; + using Player; public class FicdownParser { private IBlockHandler _blockHandler; - public IBlockHandler BlockHandler + internal IBlockHandler BlockHandler { get { return _blockHandler ?? (_blockHandler = new BlockHandler()); } set { _blockHandler = value; } } - private IStateResolver _stateResolver; + private IGameTraverser _gameTraverser; + internal IGameTraverser GameTraverser + { + get { return _gameTraverser ?? (_gameTraverser = new GameTraverser()); } + set { _gameTraverser = value; } + } - public IStateResolver StateResolver + private IStateResolver _stateResolver; + internal IStateResolver StateResolver { get { return _stateResolver ?? (_stateResolver = new StateResolver()); } set { _stateResolver = value; } } - public Story ParseStory(string storyText) + public IEnumerable ParseStory(string storyText) { var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None); var blocks = BlockHandler.ExtractBlocks(lines); - var story = BlockHandler.ParseBlocks(blocks); - return story; + GameTraverser.Story = BlockHandler.ParseBlocks(blocks); + return StateResolver.Resolve(GameTraverser.Enumerate()); } } } diff --git a/Ficdown.Parser/Ficdown.Parser.csproj b/Ficdown.Parser/Ficdown.Parser.csproj index 0b73b6e..b34572a 100644 --- a/Ficdown.Parser/Ficdown.Parser.csproj +++ b/Ficdown.Parser/Ficdown.Parser.csproj @@ -42,11 +42,10 @@ + - - @@ -61,6 +60,7 @@ + diff --git a/Ficdown.Parser/Model/Parser/ResolvedPage.cs b/Ficdown.Parser/Model/Parser/ResolvedPage.cs new file mode 100644 index 0000000..de190a9 --- /dev/null +++ b/Ficdown.Parser/Model/Parser/ResolvedPage.cs @@ -0,0 +1,8 @@ +namespace Ficdown.Parser.Model.Parser +{ + public class ResolvedPage + { + public string Name { get; set; } + public string Content { get; set; } + } +} diff --git a/Ficdown.Parser/Model/Traverser/PageState.cs b/Ficdown.Parser/Model/Traverser/PageState.cs index c5ac4e4..54f2a78 100644 --- a/Ficdown.Parser/Model/Traverser/PageState.cs +++ b/Ficdown.Parser/Model/Traverser/PageState.cs @@ -1,8 +1,8 @@ namespace Ficdown.Parser.Model.Traverser { using System; - using System.Collections; using System.Collections.Generic; + using Ficdown.Parser.Player; using Story; internal class PageState @@ -12,29 +12,22 @@ public State State { get; set; } public State AffectedState { get; set; } + public IDictionary StateMatrix { get; set; } + public string Resolved { get; set; } public IDictionary Links { get; set; } private string _uniqueHash; + private string _compressedHash; + public string UniqueHash { - get - { - if (_uniqueHash == null) - { - var combined = - new bool[State.PlayerState.Count + State.ScenesSeen.Count + State.ActionsToShow.Count]; - State.PlayerState.CopyTo(combined, 0); - State.ScenesSeen.CopyTo(combined, State.PlayerState.Count); - State.ActionsToShow.CopyTo(combined, State.PlayerState.Count + State.ScenesSeen.Count); - var ba = new BitArray(combined); - var byteSize = (int) Math.Ceiling(combined.Length/8.0); - var encoded = new byte[byteSize]; - ba.CopyTo(encoded, 0); - _uniqueHash = string.Format("{0}=={1}", Scene.Key, Convert.ToBase64String(encoded)); - } - return _uniqueHash; - } + get { return _uniqueHash ?? (_uniqueHash = StateManager.GetUniqueHash(State, Scene.Key)); } + } + + public string CompressedHash + { + get { return _compressedHash ?? (_compressedHash = StateManager.GetCompressedHash(this)); } } } -} +} \ No newline at end of file diff --git a/Ficdown.Parser/Parser/BlockHandler.cs b/Ficdown.Parser/Parser/BlockHandler.cs index 612ab3f..8cf71d6 100644 --- a/Ficdown.Parser/Parser/BlockHandler.cs +++ b/Ficdown.Parser/Parser/BlockHandler.cs @@ -8,7 +8,7 @@ using Model.Story; using Action = Model.Story.Action; - public class BlockHandler : IBlockHandler + internal class BlockHandler : IBlockHandler { public IEnumerable ExtractBlocks(IEnumerable lines) { diff --git a/Ficdown.Parser/Parser/IBlockHandler.cs b/Ficdown.Parser/Parser/IBlockHandler.cs index 50b8572..282805b 100644 --- a/Ficdown.Parser/Parser/IBlockHandler.cs +++ b/Ficdown.Parser/Parser/IBlockHandler.cs @@ -4,7 +4,7 @@ using Model.Parser; using Model.Story; - public interface IBlockHandler + internal interface IBlockHandler { IEnumerable ExtractBlocks(IEnumerable lines); Story ParseBlocks(IEnumerable blocks); diff --git a/Ficdown.Parser/Parser/IStateResolver.cs b/Ficdown.Parser/Parser/IStateResolver.cs index 4176964..6d935a2 100644 --- a/Ficdown.Parser/Parser/IStateResolver.cs +++ b/Ficdown.Parser/Parser/IStateResolver.cs @@ -1,9 +1,11 @@ namespace Ficdown.Parser.Parser { using System.Collections.Generic; + using Model.Parser; + using Model.Traverser; - public interface IStateResolver + internal interface IStateResolver { - string Resolve(string description, IDictionary playState, bool firstSeen); + IEnumerable Resolve(IEnumerable pages); } } diff --git a/Ficdown.Parser/Parser/StateResolver.cs b/Ficdown.Parser/Parser/StateResolver.cs index d38a014..66dc2ba 100644 --- a/Ficdown.Parser/Parser/StateResolver.cs +++ b/Ficdown.Parser/Parser/StateResolver.cs @@ -1,36 +1,102 @@ namespace Ficdown.Parser.Parser { + using System; using System.Collections.Generic; + using System.Linq; + using System.Text; + using Model.Parser; + using Model.Traverser; - public class StateResolver : IStateResolver + internal class StateResolver : IStateResolver { - public string Resolve(string description, IDictionary playerState, bool firstSeen) + private static readonly Random _random = new Random((int) DateTime.Now.Ticks); + private readonly IDictionary _pageNames; + private readonly HashSet _usedNames; + + public StateResolver() { - foreach (var anchor in Utilities.ParseAnchors(description)) + _pageNames = new Dictionary(); + _usedNames = new HashSet(); + } + + public IEnumerable Resolve(IEnumerable pages) + { + return + pages.Select( + page => + new ResolvedPage + { + Name = GetPageNameForHash(page.CompressedHash), + Content = ResolveDescription(page) + }).ToList(); + } + + private string ResolveAnchor(Anchor anchor, IDictionary playerState, string targetHash) + { + var text = anchor.Text; + if (anchor.Href.Conditions != null) { - if (anchor.Href.Conditions != null) - { - var satisfied = Utilities.ConditionsMet(playerState, anchor.Href.Conditions); - var alts = Utilities.ParseConditionalText(anchor.Text); - var replace = alts[satisfied]; - replace = RegexLib.EscapeChar.Replace(replace, string.Empty); - if (!replace.Equals(string.Empty) || (anchor.Href.Toggles == null && anchor.Href.Target == null)) - { - description = description.Replace(anchor.Original, replace); - description = RegexLib.EmptyListItem.Replace(description, string.Empty); - } - else - { - var newAnchor = string.Format(@"[{0}]({1}{2})", replace, - (anchor.Href.Target != null ? string.Format(@"/{0}", anchor.Href.Target) : null), - anchor.Href.Toggles.ToHrefString("+")); - description = description.Replace(anchor.Original, newAnchor); - } - } + var satisfied = Utilities.ConditionsMet(playerState, anchor.Href.Conditions); + var alts = Utilities.ParseConditionalText(text); + var replace = alts[satisfied]; + text = RegexLib.EscapeChar.Replace(replace, string.Empty); } - return firstSeen - ? RegexLib.BlockQuoteToken.Replace(description, string.Empty) - : RegexLib.BlockQuotes.Replace(description, string.Empty); + return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash) + ? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash)) + : text; + } + + private string ResolveDescription(PageState page) + { + var resolved = page.Scene.Description; + + var anchors = Utilities.ParseAnchors(resolved); + + resolved = RegexLib.EmptyListItem.Replace(anchors.Aggregate(resolved, + (current, anchor) => + current.Replace(anchor.Original, + ResolveAnchor(anchor, GetStateDictionary(page), + page.Links.ContainsKey(anchor.Original) ? page.Links[anchor.Original] : null))), + string.Empty); + + var seen = page.State.ScenesSeen[page.Scene.Id - 1]; + resolved = !seen + ? RegexLib.BlockQuoteToken.Replace(resolved, string.Empty) + : RegexLib.BlockQuotes.Replace(resolved, string.Empty); + return resolved; + } + + private IDictionary GetStateDictionary(PageState page) + { + return page.StateMatrix.Where(matrix => page.State.PlayerState[matrix.Value]) + .ToDictionary(m => m.Key, m => true); + } + + private string GetPageNameForHash(string hash) + { + if (!_pageNames.ContainsKey(hash)) + { + string name; + do + { + name = RandomString(8); + } while (_usedNames.Contains(name)); + _pageNames.Add(hash, name); + _usedNames.Add(name); + } + return _pageNames[hash]; + } + + private string RandomString(int size) + { + var builder = new StringBuilder(); + for (int i = 0; i < size; i++) + { + char ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26*_random.NextDouble() + 65))); + builder.Append(ch); + } + + return builder.ToString(); } } -} +} \ No newline at end of file diff --git a/Ficdown.Parser/Parser/Utilities.cs b/Ficdown.Parser/Parser/Utilities.cs index 609311f..c145577 100644 --- a/Ficdown.Parser/Parser/Utilities.cs +++ b/Ficdown.Parser/Parser/Utilities.cs @@ -1,7 +1,4 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")] - + namespace Ficdown.Parser.Parser { using System; diff --git a/Ficdown.Parser/Player/GameTraverser.cs b/Ficdown.Parser/Player/GameTraverser.cs index bfb2130..4f4d0af 100644 --- a/Ficdown.Parser/Player/GameTraverser.cs +++ b/Ficdown.Parser/Player/GameTraverser.cs @@ -6,17 +6,25 @@ using Model.Traverser; using Parser; - internal class GameTraverser + internal class GameTraverser : IGameTraverser { - private readonly StateManager _manager; - private readonly Queue _processingQueue; - private readonly IDictionary _processed; + private StateManager _manager; + private Queue _processingQueue; + private IDictionary _processed; + private IDictionary _compressed; - public GameTraverser(Story story) + private Story _story; + public Story Story { - _manager = new StateManager(story); - _processingQueue = new Queue(); - _processed = new Dictionary(); + get { return _story; } + set + { + _story = value; + _manager = new StateManager(_story); + _processingQueue = new Queue(); + _processed = new Dictionary(); + _compressed = new Dictionary(); + } } public IEnumerable Enumerate() @@ -40,9 +48,21 @@ } // compress redundancies + foreach (var row in _processed) + { + if (!_compressed.ContainsKey(row.Value.CompressedHash)) + { + var scene = row.Value; + var links = scene.Links.Keys.ToArray(); + foreach (var link in links) + { + scene.Links[link] = _processed[scene.Links[link]].CompressedHash; + } + _compressed.Add(row.Value.CompressedHash, row.Value); + } + } - - return _processed.Values; + return _compressed.Values; } private void ProcessState(StateQueueItem currentState) diff --git a/Ficdown.Parser/Player/HtmlRenderer.cs b/Ficdown.Parser/Player/HtmlRenderer.cs deleted file mode 100644 index 60f03cf..0000000 --- a/Ficdown.Parser/Player/HtmlRenderer.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Ficdown.Parser.Player -{ - using System.IO; - using MarkdownSharp; - - public class HtmlRenderer : IRenderer - { - public string Template { get; set; } - public void Render(string text, string outFile) - { - File.WriteAllText(outFile, new Markdown().Transform(text)); - } - } -} diff --git a/Ficdown.Parser/Player/IGameTraverser.cs b/Ficdown.Parser/Player/IGameTraverser.cs new file mode 100644 index 0000000..22d2254 --- /dev/null +++ b/Ficdown.Parser/Player/IGameTraverser.cs @@ -0,0 +1,12 @@ +namespace Ficdown.Parser.Player +{ + using System.Collections.Generic; + using Model.Story; + using Model.Traverser; + + internal interface IGameTraverser + { + Story Story { get; set; } + IEnumerable Enumerate(); + } +} diff --git a/Ficdown.Parser/Player/IRenderer.cs b/Ficdown.Parser/Player/IRenderer.cs deleted file mode 100644 index 5d1bded..0000000 --- a/Ficdown.Parser/Player/IRenderer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ficdown.Parser.Player -{ - public interface IRenderer - { - void Render(string text, string outFile); - } -} diff --git a/Ficdown.Parser/Player/StateManager.cs b/Ficdown.Parser/Player/StateManager.cs index a6d612b..ce1565e 100644 --- a/Ficdown.Parser/Player/StateManager.cs +++ b/Ficdown.Parser/Player/StateManager.cs @@ -59,7 +59,8 @@ ScenesSeen = new BitArray(_sceneCount), ActionsToShow = new BitArray(_actionCount) }, - Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null) + Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null), + StateMatrix = _stateMatrix }; } } @@ -95,6 +96,30 @@ state.ScenesSeen[sceneId - 1] = true; } + public static string GetUniqueHash(State state, string sceneKey) + { + var combined = new bool[state.PlayerState.Count + state.ScenesSeen.Count + state.ActionsToShow.Count]; + state.PlayerState.CopyTo(combined, 0); + state.ScenesSeen.CopyTo(combined, state.PlayerState.Count); + state.ActionsToShow.CopyTo(combined, state.PlayerState.Count + state.ScenesSeen.Count); + var ba = new BitArray(combined); + var byteSize = (int)Math.Ceiling(combined.Length / 8.0); + var encoded = new byte[byteSize]; + ba.CopyTo(encoded, 0); + return string.Format("{0}=={1}", sceneKey, Convert.ToBase64String(encoded)); + } + + public static string GetCompressedHash(PageState page) + { + var compressed = new State + { + PlayerState = page.State.PlayerState.And(page.AffectedState.PlayerState), + ScenesSeen = page.State.ScenesSeen.And(page.AffectedState.ScenesSeen), + ActionsToShow = page.State.ActionsToShow + }; + return GetUniqueHash(compressed, page.Scene.Key); + } + private Scene GetScene(string target, BitArray playerState) { if (!_story.Scenes.ContainsKey(target)) @@ -138,7 +163,8 @@ PlayerState = new BitArray(_stateMatrix.Keys.Count), ScenesSeen = new BitArray(_sceneCount), ActionsToShow = new BitArray(_actionCount) - } + }, + StateMatrix = _stateMatrix }; } }