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
{
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());
}
}
}

View File

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

View File

@ -42,11 +42,10 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Model\Parser\ResolvedPage.cs" />
<Compile Include="Model\Traverser\PageState.cs" />
<Compile Include="Model\Traverser\State.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\IBlockHandler.cs" />
<Compile Include="Parser\RegexLib.cs" />
@ -61,6 +60,7 @@
<Compile Include="Model\Player\PlayerState.cs" />
<Compile Include="Model\Story\Extensions\SceneExtensions.cs" />
<Compile Include="Player\GameTraverser.cs" />
<Compile Include="Player\IGameTraverser.cs" />
<Compile Include="Player\StateManager.cs" />
<Compile Include="Properties\AssemblyInfo.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
{
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<string, int> StateMatrix { get; set; }
public string Resolved { get; set; }
public IDictionary<string, string> 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)); }
}
}
}
}

View File

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

View File

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

View File

@ -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<string, bool> playState, bool firstSeen);
IEnumerable<ResolvedPage> Resolve(IEnumerable<PageState> pages);
}
}

View File

@ -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<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 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<string, bool> 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();
}
}
}
}

View File

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

namespace Ficdown.Parser.Parser
{
using System;

View File

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

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