added page compression to remove redundant page states; implemented state resolver

This commit is contained in:
Rudis Muiznieks 2014-08-09 15:18:31 -05:00
parent bd99018ed1
commit a0c9ce5d8c
15 changed files with 210 additions and 107 deletions

View File

@ -1,10 +1,6 @@
namespace Ficdown.Parser.Tests namespace Ficdown.Parser.Tests
{ {
using System;
using System.Linq;
using System.Text; using System.Text;
using Player;
using ServiceStack.Text;
using TestStories; using TestStories;
using Xunit; using Xunit;
@ -16,13 +12,6 @@
var parser = new FicdownParser(); var parser = new FicdownParser();
var storyText = Encoding.UTF8.GetString(Resources.TheRobotKing); var storyText = Encoding.UTF8.GetString(Resources.TheRobotKing);
var story = parser.ParseStory(storyText); 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());
} }
} }
} }

View File

@ -1,32 +1,43 @@
namespace Ficdown.Parser using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
namespace Ficdown.Parser
{ {
using System; using System;
using Model.Story; using System.Collections.Generic;
using Model.Parser;
using Parser; using Parser;
using Player;
public class FicdownParser public class FicdownParser
{ {
private IBlockHandler _blockHandler; private IBlockHandler _blockHandler;
public IBlockHandler BlockHandler internal IBlockHandler BlockHandler
{ {
get { return _blockHandler ?? (_blockHandler = new BlockHandler()); } get { return _blockHandler ?? (_blockHandler = new BlockHandler()); }
set { _blockHandler = value; } 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()); } get { return _stateResolver ?? (_stateResolver = new StateResolver()); }
set { _stateResolver = value; } set { _stateResolver = value; }
} }
public Story ParseStory(string storyText) public IEnumerable<ResolvedPage> ParseStory(string storyText)
{ {
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None); var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
var blocks = BlockHandler.ExtractBlocks(lines); var blocks = BlockHandler.ExtractBlocks(lines);
var story = BlockHandler.ParseBlocks(blocks); GameTraverser.Story = BlockHandler.ParseBlocks(blocks);
return story; return StateResolver.Resolve(GameTraverser.Enumerate());
} }
} }
} }

View File

@ -42,11 +42,10 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Model\Parser\ResolvedPage.cs" />
<Compile Include="Model\Traverser\PageState.cs" /> <Compile Include="Model\Traverser\PageState.cs" />
<Compile Include="Model\Traverser\State.cs" /> <Compile Include="Model\Traverser\State.cs" />
<Compile Include="Model\Traverser\StateQueueItem.cs" /> <Compile Include="Model\Traverser\StateQueueItem.cs" />
<Compile Include="Player\IRenderer.cs" />
<Compile Include="Player\HtmlRenderer.cs" />
<Compile Include="Parser\BlockHandler.cs" /> <Compile Include="Parser\BlockHandler.cs" />
<Compile Include="Parser\IBlockHandler.cs" /> <Compile Include="Parser\IBlockHandler.cs" />
<Compile Include="Parser\RegexLib.cs" /> <Compile Include="Parser\RegexLib.cs" />
@ -61,6 +60,7 @@
<Compile Include="Model\Player\PlayerState.cs" /> <Compile Include="Model\Player\PlayerState.cs" />
<Compile Include="Model\Story\Extensions\SceneExtensions.cs" /> <Compile Include="Model\Story\Extensions\SceneExtensions.cs" />
<Compile Include="Player\GameTraverser.cs" /> <Compile Include="Player\GameTraverser.cs" />
<Compile Include="Player\IGameTraverser.cs" />
<Compile Include="Player\StateManager.cs" /> <Compile Include="Player\StateManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Model\Story\Action.cs" /> <Compile Include="Model\Story\Action.cs" />

View File

@ -0,0 +1,8 @@
namespace Ficdown.Parser.Model.Parser
{
public class ResolvedPage
{
public string Name { get; set; }
public string Content { get; set; }
}
}

View File

@ -1,8 +1,8 @@
namespace Ficdown.Parser.Model.Traverser namespace Ficdown.Parser.Model.Traverser
{ {
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using Ficdown.Parser.Player;
using Story; using Story;
internal class PageState internal class PageState
@ -12,29 +12,22 @@
public State State { get; set; } public State State { get; set; }
public State AffectedState { get; set; } public State AffectedState { get; set; }
public IDictionary<string, int> StateMatrix { get; set; }
public string Resolved { get; set; } public string Resolved { get; set; }
public IDictionary<string, string> Links { get; set; } public IDictionary<string, string> Links { get; set; }
private string _uniqueHash; private string _uniqueHash;
private string _compressedHash;
public string UniqueHash public string UniqueHash
{ {
get get { return _uniqueHash ?? (_uniqueHash = StateManager.GetUniqueHash(State, Scene.Key)); }
}
public string CompressedHash
{ {
if (_uniqueHash == null) get { return _compressedHash ?? (_compressedHash = StateManager.GetCompressedHash(this)); }
{
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;
}
} }
} }
} }

View File

@ -8,7 +8,7 @@
using Model.Story; using Model.Story;
using Action = Model.Story.Action; using Action = Model.Story.Action;
public class BlockHandler : IBlockHandler internal class BlockHandler : IBlockHandler
{ {
public IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines) public IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines)
{ {

View File

@ -4,7 +4,7 @@
using Model.Parser; using Model.Parser;
using Model.Story; using Model.Story;
public interface IBlockHandler internal interface IBlockHandler
{ {
IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines); IEnumerable<Block> ExtractBlocks(IEnumerable<string> lines);
Story ParseBlocks(IEnumerable<Block> blocks); Story ParseBlocks(IEnumerable<Block> blocks);

View File

@ -1,9 +1,11 @@
namespace Ficdown.Parser.Parser namespace Ficdown.Parser.Parser
{ {
using System.Collections.Generic; using System.Collections.Generic;
using Model.Parser;
using Model.Traverser;
public interface IStateResolver internal interface IStateResolver
{ {
string Resolve(string description, IDictionary<string, bool> playState, bool firstSeen); IEnumerable<ResolvedPage> Resolve(IEnumerable<PageState> pages);
} }
} }

View File

@ -1,36 +1,102 @@
namespace Ficdown.Parser.Parser namespace Ficdown.Parser.Parser
{ {
using System;
using System.Collections.Generic; 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<string, bool> playerState, bool firstSeen) private static readonly Random _random = new Random((int) DateTime.Now.Ticks);
private readonly IDictionary<string, string> _pageNames;
private readonly HashSet<string> _usedNames;
public StateResolver()
{ {
foreach (var anchor in Utilities.ParseAnchors(description)) _pageNames = new Dictionary<string, string>();
_usedNames = new HashSet<string>();
}
public IEnumerable<ResolvedPage> Resolve(IEnumerable<PageState> pages)
{ {
return
pages.Select(
page =>
new ResolvedPage
{
Name = GetPageNameForHash(page.CompressedHash),
Content = ResolveDescription(page)
}).ToList();
}
private string ResolveAnchor(Anchor anchor, IDictionary<string, bool> 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 satisfied = Utilities.ConditionsMet(playerState, anchor.Href.Conditions);
var alts = Utilities.ParseConditionalText(anchor.Text); var alts = Utilities.ParseConditionalText(text);
var replace = alts[satisfied]; var replace = alts[satisfied];
replace = RegexLib.EscapeChar.Replace(replace, string.Empty); text = RegexLib.EscapeChar.Replace(replace, string.Empty);
if (!replace.Equals(string.Empty) || (anchor.Href.Toggles == null && anchor.Href.Target == null)) }
return !string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(targetHash)
? string.Format("[{0}](/{1})", text, GetPageNameForHash(targetHash))
: text;
}
private string ResolveDescription(PageState page)
{ {
description = description.Replace(anchor.Original, replace); var resolved = page.Scene.Description;
description = RegexLib.EmptyListItem.Replace(description, string.Empty);
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;
} }
else
private IDictionary<string, bool> GetStateDictionary(PageState page)
{ {
var newAnchor = string.Format(@"[{0}]({1}{2})", replace, return page.StateMatrix.Where(matrix => page.State.PlayerState[matrix.Value])
(anchor.Href.Target != null ? string.Format(@"/{0}", anchor.Href.Target) : null), .ToDictionary(m => m.Key, m => true);
anchor.Href.Toggles.ToHrefString("+")); }
description = description.Replace(anchor.Original, newAnchor);
} private string GetPageNameForHash(string hash)
} {
} if (!_pageNames.ContainsKey(hash))
return firstSeen {
? RegexLib.BlockQuoteToken.Replace(description, string.Empty) string name;
: RegexLib.BlockQuotes.Replace(description, string.Empty); 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();
} }
} }
} }

View File

@ -1,7 +1,4 @@
using System.Runtime.CompilerServices; 
[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
namespace Ficdown.Parser.Parser namespace Ficdown.Parser.Parser
{ {
using System; using System;

View File

@ -6,17 +6,25 @@
using Model.Traverser; using Model.Traverser;
using Parser; using Parser;
internal class GameTraverser internal class GameTraverser : IGameTraverser
{ {
private readonly StateManager _manager; private StateManager _manager;
private readonly Queue<StateQueueItem> _processingQueue; private Queue<StateQueueItem> _processingQueue;
private readonly IDictionary<string, PageState> _processed; private IDictionary<string, PageState> _processed;
private IDictionary<string, PageState> _compressed;
public GameTraverser(Story story) private Story _story;
public Story Story
{ {
_manager = new StateManager(story); get { return _story; }
set
{
_story = value;
_manager = new StateManager(_story);
_processingQueue = new Queue<StateQueueItem>(); _processingQueue = new Queue<StateQueueItem>();
_processed = new Dictionary<string, PageState>(); _processed = new Dictionary<string, PageState>();
_compressed = new Dictionary<string, PageState>();
}
} }
public IEnumerable<PageState> Enumerate() public IEnumerable<PageState> Enumerate()
@ -40,9 +48,21 @@
} }
// compress redundancies // 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 _compressed.Values;
return _processed.Values;
} }
private void ProcessState(StateQueueItem currentState) private void ProcessState(StateQueueItem currentState)

View File

@ -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));
}
}
}

View File

@ -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<PageState> Enumerate();
}
}

View File

@ -1,7 +0,0 @@
namespace Ficdown.Parser.Player
{
public interface IRenderer
{
void Render(string text, string outFile);
}
}

View File

@ -59,7 +59,8 @@
ScenesSeen = new BitArray(_sceneCount), ScenesSeen = new BitArray(_sceneCount),
ActionsToShow = new BitArray(_actionCount) 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; 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) private Scene GetScene(string target, BitArray playerState)
{ {
if (!_story.Scenes.ContainsKey(target)) if (!_story.Scenes.ContainsKey(target))
@ -138,7 +163,8 @@
PlayerState = new BitArray(_stateMatrix.Keys.Count), PlayerState = new BitArray(_stateMatrix.Keys.Count),
ScenesSeen = new BitArray(_sceneCount), ScenesSeen = new BitArray(_sceneCount),
ActionsToShow = new BitArray(_actionCount) ActionsToShow = new BitArray(_actionCount)
} },
StateMatrix = _stateMatrix
}; };
} }
} }