added page compression to remove redundant page states; implemented state resolver
This commit is contained in:
parent
bd99018ed1
commit
a0c9ce5d8c
15 changed files with 210 additions and 107 deletions
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
8
Ficdown.Parser/Model/Parser/ResolvedPage.cs
Normal file
8
Ficdown.Parser/Model/Parser/ResolvedPage.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ficdown.Parser.Model.Parser
|
||||
{
|
||||
public class ResolvedPage
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Content { get; set; }
|
||||
}
|
||||
}
|
|
@ -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)); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
|
||||
|
||||
|
||||
namespace Ficdown.Parser.Parser
|
||||
{
|
||||
using System;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
12
Ficdown.Parser/Player/IGameTraverser.cs
Normal file
12
Ficdown.Parser/Player/IGameTraverser.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Ficdown.Parser.Player
|
||||
{
|
||||
public interface IRenderer
|
||||
{
|
||||
void Render(string text, string outFile);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue