full in-memory enumeration of stories complete

This commit is contained in:
Rudis Muiznieks 2014-07-26 23:54:50 -05:00
parent 178a44a1b9
commit 1d0834e8ac
14 changed files with 310 additions and 130 deletions

View File

@ -18,8 +18,8 @@
Assert.Equal("The Robot King", story.Name); Assert.Equal("The Robot King", story.Name);
Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name); Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name);
var player = new GameTraverser(); var traverser = new GameTraverser(story);
player.ExportStaticStory(story, @"C:\Users\Rudis\Desktop\template.html", @"C:\Users\Rudis\Desktop\output"); var test = traverser.Enumerate();
} }
} }
} }

View File

@ -101,10 +101,14 @@ There is a grand double staircase leading up to the throne room, a hallway strai
**Where do you want to go?** **Where do you want to go?**
- [Go upstairs to the throne room.](/throne-room) - [Go upstairs to the throne room.](#throne-room)
- [Go through the hall to the living quarters.](/living-quarters) - [Go through the hall to the living quarters.](/living-quarters)
- [Go downstairs to see the Master Janitor Robot.](/palace-basement) - [Go downstairs to see the Master Janitor Robot.](/palace-basement)
### Throne Room
You are about to ascend the stairs, but it occurs to you that maybe the King has more important things to do than meet the new janitor right now.
## Living Quarters ## Living Quarters
You walk into the hall that leads to the living quarters, and find a gate blocking your way. There is a robot scanner installed on the gate. I guess it only opens for robots who live or work here. Maybe the Master Janitor Robot will have a way for you to get through. You walk into the hall that leads to the living quarters, and find a gate blocking your way. There is a robot scanner installed on the gate. I guess it only opens for robots who live or work here. Maybe the Master Janitor Robot will have a way for you to get through.
@ -163,7 +167,7 @@ That's when you realize that you never asked the Master Janitor Bot what your jo
**You have failed to perform your new job because you never found out what it was.** **You have failed to perform your new job because you never found out what it was.**
## [Living Quarters 2](?job-started "Living Quarters") ## [Living Quarters 2](?started-job "Living Quarters")
That's no problem though, because you already know what your job is. You continue down the hall, looking at and passing all of the doors until you come to the one marked with a "13." Right next to it is another door labeled "Janitor's Closet." That's no problem though, because you already know what your job is. You continue down the hall, looking at and passing all of the doors until you come to the one marked with a "13." Right next to it is another door labeled "Janitor's Closet."

View File

@ -1,6 +1,6 @@
namespace Ficdown.Parser.Tests namespace Ficdown.Parser.Tests
{ {
using System; using System.Linq;
using Parser; using Parser;
using Xunit; using Xunit;
@ -9,99 +9,91 @@
[Fact] [Fact]
public void FullAnchorMatches() public void FullAnchorMatches()
{ {
Console.WriteLine(RegexLib.Href.ToString());
var anchorStr = @"[Link text](/target-scene)"; var anchorStr = @"[Link text](/target-scene)";
var anchor = RegexLib.Anchors.Match(anchorStr); var anchor = Utilities.ParseAnchor(anchorStr);
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); Assert.Equal(anchorStr, anchor.Original);
anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")"; anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")";
anchor = RegexLib.Anchors.Match(anchorStr); anchor = Utilities.ParseAnchor(anchorStr);
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); Assert.Equal(anchorStr, anchor.Original);
anchorStr = @"[Link text](""Title text"")"; anchorStr = @"[Link text](""Title text"")";
anchor = RegexLib.Anchors.Match(anchorStr); anchor = Utilities.ParseAnchor(anchorStr);
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value); Assert.Equal(anchorStr, anchor.Original);
} }
[Fact] [Fact]
public void AnchorWithTargetMatches() public void AnchorWithTargetMatches()
{ {
var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene)"); var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("target-scene", anchor.Href.Target);
Assert.Equal("/target-scene", anchor.Groups["href"].Value);
} }
[Fact] [Fact]
public void AnchorsWithConditionsMatch() public void AnchorsWithConditionsMatch()
{ {
var anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state)"); var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.True(anchor.Href.Conditions["condition-state"]);
Assert.Equal("?condition-state", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](?!condition-state)"); anchor = Utilities.ParseAnchor(@"[Link text](?!condition-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.False(anchor.Href.Conditions["condition-state"]);
Assert.Equal("?!condition-state", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](?condition-1&!condition-2)"); anchor = Utilities.ParseAnchor(@"[Link text](?condition-1&!condition-2)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.True(anchor.Href.Conditions["condition-1"]);
Assert.Equal("?condition-1&!condition-2", anchor.Groups["href"].Value); Assert.False(anchor.Href.Conditions["condition-2"]);
} }
[Fact] [Fact]
public void AnchorsWithTogglesMatch() public void AnchorsWithTogglesMatch()
{ {
var anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-state)"); var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("#toggle-state", anchor.Href.Original);
Assert.Equal("#toggle-state", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-1+toggle-2)"); anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original);
Assert.Equal("#toggle-1+toggle-2", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-1+!toggle-2)"); anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original);
Assert.Equal("#toggle-1+!toggle-2", anchor.Groups["href"].Value);
} }
[Fact] [Fact]
public void AnchorsWithTitlesMatch() public void AnchorsWithTitlesMatch()
{ {
var anchor = RegexLib.Anchors.Match(@"[Link text](""Title text"")"); var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("Title text", anchor.Title);
Assert.Equal("Title text", anchor.Groups["title"].Value);
anchor = Utilities.ParseAnchor(@"[Talking to Kid](""Lobby"")");
Assert.Equal("Talking to Kid", anchor.Text);
Assert.Equal("Lobby", anchor.Title);
} }
[Fact] [Fact]
public void ComplexAnchorsMatch() public void ComplexAnchorsMatch()
{ {
var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")"); var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Href.Original);
Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Groups["href"].Value); Assert.Equal("Title text", anchor.Title);
Assert.Equal("Title text", anchor.Groups["title"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene#toggle-state)"); anchor = Utilities.ParseAnchor(@"[Link text](/target-scene#toggle-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("/target-scene#toggle-state", anchor.Href.Original);
Assert.Equal("/target-scene#toggle-state", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state)"); anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("/target-scene?condition-state", anchor.Href.Original);
Assert.Equal("/target-scene?condition-state", anchor.Groups["href"].Value);
anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state#toggle-state)"); anchor = Utilities.ParseAnchor(@"[Link text](?condition-state#toggle-state)");
Assert.True(anchor.Success); Assert.Equal("Link text", anchor.Text);
Assert.Equal("Link text", anchor.Groups["text"].Value); Assert.Equal("?condition-state#toggle-state", anchor.Href.Original);
Assert.Equal("?condition-state#toggle-state", anchor.Groups["href"].Value);
} }
[Fact] [Fact]
@ -136,15 +128,12 @@
var anchors = Utilities.ParseAnchors("[Anchor](#toggle-state)"); var anchors = Utilities.ParseAnchors("[Anchor](#toggle-state)");
Assert.Null(anchors[0].Href.Target); Assert.Null(anchors[0].Href.Target);
Assert.Null(anchors[0].Href.Conditions); Assert.Null(anchors[0].Href.Conditions);
Assert.Equal(1, anchors[0].Href.Toggles.Count); Assert.Equal(1, anchors[0].Href.Toggles.Count());
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
anchors = Utilities.ParseAnchors("[Anchor](#toggle-1+!toggle-2)"); anchors = Utilities.ParseAnchors("[Anchor](#toggle-1+toggle-2)");
Assert.Null(anchors[0].Href.Target); Assert.Null(anchors[0].Href.Target);
Assert.Null(anchors[0].Href.Conditions); Assert.Null(anchors[0].Href.Conditions);
Assert.Equal(2, anchors[0].Href.Toggles.Count); Assert.Equal(2, anchors[0].Href.Toggles.Count());
Assert.True(anchors[0].Href.Toggles["toggle-1"]);
Assert.False(anchors[0].Href.Toggles["toggle-2"]);
} }
[Fact] [Fact]
@ -154,8 +143,7 @@
Assert.Equal("target-scene", anchors[0].Href.Target); Assert.Equal("target-scene", anchors[0].Href.Target);
Assert.Equal(1, anchors[0].Href.Conditions.Count); Assert.Equal(1, anchors[0].Href.Conditions.Count);
Assert.True(anchors[0].Href.Conditions["condition-state"]); Assert.True(anchors[0].Href.Conditions["condition-state"]);
Assert.Equal(1, anchors[0].Href.Toggles.Count); Assert.Equal(1, anchors[0].Href.Toggles.Count());
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state)"); anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state)");
Assert.Equal("target-scene", anchors[0].Href.Target); Assert.Equal("target-scene", anchors[0].Href.Target);
@ -166,17 +154,14 @@
anchors = Utilities.ParseAnchors("[Anchor](/target-scene#toggle-state)"); anchors = Utilities.ParseAnchors("[Anchor](/target-scene#toggle-state)");
Assert.Equal("target-scene", anchors[0].Href.Target); Assert.Equal("target-scene", anchors[0].Href.Target);
Assert.Null(anchors[0].Href.Conditions); Assert.Null(anchors[0].Href.Conditions);
Assert.Equal(1, anchors[0].Href.Toggles.Count); Assert.Equal(1, anchors[0].Href.Toggles.Count());
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
anchors = Utilities.ParseAnchors("[Anchor](?condition-one&!condition-two#toggle-one+!toggle-two)"); anchors = Utilities.ParseAnchors("[Anchor](?condition-one&!condition-two#toggle-one+toggle-two)");
Assert.Null(anchors[0].Href.Target); Assert.Null(anchors[0].Href.Target);
Assert.Equal(2, anchors[0].Href.Conditions.Count); Assert.Equal(2, anchors[0].Href.Conditions.Count);
Assert.True(anchors[0].Href.Conditions["condition-one"]); Assert.True(anchors[0].Href.Conditions["condition-one"]);
Assert.False(anchors[0].Href.Conditions["condition-two"]); Assert.False(anchors[0].Href.Conditions["condition-two"]);
Assert.Equal(2, anchors[0].Href.Toggles.Count); Assert.Equal(2, anchors[0].Href.Toggles.Count());
Assert.True(anchors[0].Href.Toggles["toggle-one"]);
Assert.False(anchors[0].Href.Toggles["toggle-two"]);
} }
} }
} }

View File

@ -42,8 +42,9 @@
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Model\Traverser\PageState.cs" />
<Compile Include="Player\IRenderer.cs" /> <Compile Include="Player\IRenderer.cs" />
<Compile Include="Player\MarkdownRenderer.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" />
@ -58,6 +59,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\StateManager.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Model\Story\Action.cs" /> <Compile Include="Model\Story\Action.cs" />
<Compile Include="Model\Story\Scene.cs" /> <Compile Include="Model\Story\Scene.cs" />

View File

@ -7,6 +7,6 @@
public string Original { get; set; } public string Original { get; set; }
public string Target { get; set; } public string Target { get; set; }
public IDictionary<string, bool> Conditions { get; set; } public IDictionary<string, bool> Conditions { get; set; }
public IDictionary<string, bool> Toggles { get; set; } public IEnumerable<string> Toggles { get; set; }
} }
} }

View File

@ -2,7 +2,8 @@
{ {
public class Action public class Action
{ {
public string State { get; set; } public int Id { get; set; }
public string Toggle { get; set; }
public string Description { get; set; } public string Description { get; set; }
} }
} }

View File

@ -4,6 +4,7 @@
public class Scene public class Scene
{ {
public int Id { get; set; }
public string Name { get; set; } public string Name { get; set; }
public string Key { get; set; } public string Key { get; set; }
public string Description { get; set; } public string Description { get; set; }

View File

@ -0,0 +1,40 @@
namespace Ficdown.Parser.Model.Traverser
{
using System;
using System.Collections;
using System.Collections.Generic;
using Story;
internal class PageState
{
public Guid Id { get; set; }
public BitArray PlayerState { get; set; }
public BitArray ScenesSeen { get; set; }
public BitArray ActionsToShow { get; set; }
public Scene Scene { get; set; }
public string Resolved { get; set; }
public IDictionary<string, string> Links { get; set; }
private string _uniqueHash;
public string UniqueHash
{
get
{
if (_uniqueHash == null)
{
var combined = new bool[PlayerState.Count + ScenesSeen.Count + ActionsToShow.Count];
PlayerState.CopyTo(combined, 0);
ScenesSeen.CopyTo(combined, PlayerState.Count);
ActionsToShow.CopyTo(combined, PlayerState.Count + 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

@ -55,14 +55,16 @@
Actions = new Dictionary<string, Action>() Actions = new Dictionary<string, Action>()
}; };
var scenes = blocks.Where(b => b.Type == BlockType.Scene).Select(BlockToScene); var sid = 1;
var scenes = blocks.Where(b => b.Type == BlockType.Scene).Select(b => BlockToScene(b, sid++));
foreach (var scene in scenes) foreach (var scene in scenes)
{ {
if (!story.Scenes.ContainsKey(scene.Key)) story.Scenes.Add(scene.Key, new List<Scene>()); if (!story.Scenes.ContainsKey(scene.Key)) story.Scenes.Add(scene.Key, new List<Scene>());
story.Scenes[scene.Key].Add(scene); story.Scenes[scene.Key].Add(scene);
} }
var aid = 1;
story.Actions = story.Actions =
blocks.Where(b => b.Type == BlockType.Action).Select(BlockToAction).ToDictionary(a => a.State, a => a); blocks.Where(b => b.Type == BlockType.Action).Select(b => BlockToAction(b, aid++)).ToDictionary(a => a.Toggle, a => a);
if (!story.Scenes.ContainsKey(storyAnchor.Href.Target)) if (!story.Scenes.ContainsKey(storyAnchor.Href.Target))
throw new FormatException(string.Format("Story targets non-existent scene: {0}", storyAnchor.Href.Target)); throw new FormatException(string.Format("Story targets non-existent scene: {0}", storyAnchor.Href.Target));
@ -72,10 +74,11 @@
} }
private Scene BlockToScene(Block block) private Scene BlockToScene(Block block, int id)
{ {
var scene = new Scene var scene = new Scene
{ {
Id = id,
Description = string.Join("\n", block.Lines).Trim() Description = string.Join("\n", block.Lines).Trim()
}; };
@ -97,11 +100,12 @@
return scene; return scene;
} }
private Action BlockToAction(Block block) private Action BlockToAction(Block block, int id)
{ {
return new Action return new Action
{ {
State = Utilities.NormalizeString(block.Name), Id = id,
Toggle = Utilities.NormalizeString(block.Name),
Description = string.Join("\n", block.Lines).Trim() Description = string.Join("\n", block.Lines).Trim()
}; };
} }

View File

@ -19,7 +19,7 @@
private const string RegexValidName = @"[a-zA-Z](-?[a-zA-Z0-9])*"; private const string RegexValidName = @"[a-zA-Z](-?[a-zA-Z0-9])*";
private static readonly string RegexHrefTarget = string.Format(@"\/({0})", RegexValidName); private static readonly string RegexHrefTarget = string.Format(@"\/({0})", RegexValidName);
private static readonly string RegexHrefConditions = string.Format(@"\?((!?{0})(&!?{0})*)?", RegexValidName); private static readonly string RegexHrefConditions = string.Format(@"\?((!?{0})(&!?{0})*)?", RegexValidName);
private static readonly string RegexHrefToggles = string.Format(@"#(!?{0})(\+!?{0})*", RegexValidName); private static readonly string RegexHrefToggles = string.Format(@"#({0})(\+{0})*", RegexValidName);
public static Regex Href = public static Regex Href =
new Regex( new Regex(

View File

@ -36,8 +36,7 @@ namespace Ficdown.Parser.Parser
: null, : null,
Toggles = Toggles =
!string.IsNullOrEmpty(tstr) !string.IsNullOrEmpty(tstr)
? new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower())) ? new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower())).ToArray()
.ToDictionary(t => t.TrimStart('!'), t => !t.StartsWith("!"))
: null : null
}; };
} }
@ -48,29 +47,33 @@ namespace Ficdown.Parser.Parser
{ {
var match = RegexLib.Anchors.Match(anchorText); var match = RegexLib.Anchors.Match(anchorText);
if (!match.Success) throw new FormatException(string.Format("Invalid anchor: {0}", anchorText)); if (!match.Success) throw new FormatException(string.Format("Invalid anchor: {0}", anchorText));
var astr = match.Groups["anchor"].Value; return MatchToAnchor(match);
var txstr = match.Groups["text"].Value;
var ttstr = match.Groups["title"].Value;
return new Anchor
{
Original = !string.IsNullOrEmpty(astr) ? astr : null,
Text = !string.IsNullOrEmpty(txstr) ? txstr : null,
Title = !string.IsNullOrEmpty(ttstr) ? ttstr : null,
Href = ParseHref(match.Groups["href"].Value)
};
} }
public static IList<Anchor> ParseAnchors(string text) public static IList<Anchor> ParseAnchors(string text)
{ {
var matches = RegexLib.Anchors.Matches(text); var matches = RegexLib.Anchors.Matches(text);
return matches.Cast<Match>().Select(m => return matches.Cast<Match>().Select(MatchToAnchor).ToList();
new Anchor }
{
Original = m.Groups["anchor"].Value, private static Anchor MatchToAnchor(Match match)
Text = m.Groups["text"].Value, {
Title = m.Groups["title"].Value, var astr = match.Groups["anchor"].Value;
Href = ParseHref(m.Groups["href"].Value) var txstr = match.Groups["text"].Value;
}).ToList(); var ttstr = match.Groups["title"].Value;
var hrefstr = match.Groups["href"].Value;
if (hrefstr.StartsWith(@""""))
{
ttstr = hrefstr.Trim('"');
hrefstr = string.Empty;
}
return new Anchor
{
Original = !string.IsNullOrEmpty(astr) ? astr : null,
Text = !string.IsNullOrEmpty(txstr) ? txstr : null,
Title = !string.IsNullOrEmpty(ttstr) ? ttstr : null,
Href = ParseHref(hrefstr)
};
} }
public static IDictionary<bool, string> ParseConditionalText(string text) public static IDictionary<bool, string> ParseConditionalText(string text)
@ -94,6 +97,11 @@ namespace Ficdown.Parser.Parser
: null; : null;
} }
public static string ToHrefString(this IEnumerable<string> values, string separator)
{
return values != null ? string.Join(separator, values.ToArray()) : null;
}
public static bool ConditionsMet(IDictionary<string, bool> playerState, IDictionary<string, bool> conditions) public static bool ConditionsMet(IDictionary<string, bool> playerState, IDictionary<string, bool> conditions)
{ {
return return

View File

@ -1,44 +1,62 @@
namespace Ficdown.Parser.Player namespace Ficdown.Parser.Player
{ {
using System.IO; using System.Collections.Generic;
using System.Threading; using System.Linq;
using Model.Story; using Model.Story;
using Model.Traverser;
using Parser;
internal class GameTraverser internal class GameTraverser
{ {
private IRenderer _renderer; private readonly StateManager _manager;
private readonly Queue<PageState> _processingQueue;
private readonly IDictionary<string, PageState> _processed;
public IRenderer Renderer public GameTraverser(Story story)
{ {
get _manager = new StateManager(story);
{ _processingQueue = new Queue<PageState>();
return _renderer ?? _processed = new Dictionary<string, PageState>();
(_renderer =
new HtmlRenderer {Template = File.ReadAllText(@"C:\Users\Rudis\Desktop\template.html")});
}
set { _renderer = value; }
} }
private volatile int _page = 0; public IEnumerable<PageState> Enumerate()
private string _template;
public void ExportStaticStory(Story story, string templateFile, string outputDirectory)
{ {
_template = File.ReadAllText(templateFile); // generate comprehensive enumeration
var dir = new DirectoryInfo(outputDirectory);
if (!dir.Exists) dir.Create(); _processingQueue.Enqueue(_manager.InitialState);
else while (_processingQueue.Count > 0)
{ {
foreach (var finfo in dir.GetFileSystemInfos()) var state = _processingQueue.Dequeue();
if (!_processed.ContainsKey(state.UniqueHash))
{ {
finfo.Delete(); _processed.Add(state.UniqueHash, state);
ProcessState(state);
} }
} }
var index = string.Format("# {0}\n\n{1}\n\n[Start the game.](page{2}.html)", story.Name, // compress redundancies
story.Description, _page++);
Renderer.Render(index, Path.Combine(outputDirectory, "index.html"));
return _processed.Values;
}
private void ProcessState(PageState currentState)
{
var states = new HashSet<string>();
foreach (
var anchor in
Utilities.ParseAnchors(currentState.Scene.Description)
.Where(a => a.Href.Target != null || a.Href.Toggles != null))
{
var newState = _manager.ResolveNewState(anchor, currentState);
if (!currentState.Links.ContainsKey(anchor.Original))
currentState.Links.Add(anchor.Original, newState.UniqueHash);
if (!states.Contains(newState.UniqueHash) && !_processed.ContainsKey(newState.UniqueHash))
{
states.Add(newState.UniqueHash);
_processingQueue.Enqueue(newState);
}
}
} }
} }
} }

View File

@ -0,0 +1,117 @@
namespace Ficdown.Parser.Player
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Model.Parser;
using Model.Story;
using Model.Traverser;
using Parser;
internal class StateManager
{
private readonly Story _story;
private readonly Dictionary<string, int> _stateMatrix;
private readonly int _sceneCount;
private readonly int _actionCount;
public StateManager(Story story)
{
_story = story;
var allScenes = _story.Scenes.SelectMany(s => s.Value);
_sceneCount = allScenes.Max(s => s.Id);
_actionCount = _story.Actions.Max(a => a.Value.Id);
_stateMatrix = new Dictionary<string, int>();
var state = 0;
foreach (
var toggle in
allScenes.SelectMany(
sc =>
Utilities.ParseAnchors(sc.Description)
.SelectMany(
a =>
a.Href.Toggles != null
? a.Href.Toggles.Where(t => !_stateMatrix.ContainsKey(t))
: new string[] {})))
{
_stateMatrix.Add(toggle, state++);
}
}
public PageState InitialState
{
get
{
return new PageState
{
Id = Guid.Empty,
Links = new Dictionary<string, string>(),
PlayerState = new BitArray(_stateMatrix.Keys.Count),
ScenesSeen = new BitArray(_sceneCount),
ActionsToShow = new BitArray(_actionCount),
Scene = _story.Scenes[_story.FirstScene].Single(s => s.Conditions == null)
};
}
}
public PageState ResolveNewState(Anchor anchor, PageState current)
{
var target = anchor.Href.Target ?? current.Scene.Key;
var newState = CloneState(current);
newState.ScenesSeen[current.Scene.Id - 1] = true;
if (anchor.Href.Toggles != null)
{
foreach (var toggle in anchor.Href.Toggles)
{
if (_story.Actions.ContainsKey(toggle))
{
newState.ActionsToShow[_story.Actions[toggle].Id - 1] = true;
}
newState.PlayerState[_stateMatrix[toggle]] = true;
}
}
newState.Scene = GetScene(target, newState.PlayerState);
return newState;
}
private Scene GetScene(string target, BitArray playerState)
{
if (!_story.Scenes.ContainsKey(target))
throw new FormatException(string.Format("Encountered link to non-existant scene: {0}", target));
Scene newScene = null;
foreach (var scene in _story.Scenes[target])
{
if (ConditionsMatch(scene, playerState) &&
(newScene == null || newScene.Conditions == null ||
scene.Conditions.Count > newScene.Conditions.Count))
{
newScene = scene;
}
}
if (newScene == null)
throw new FormatException(string.Format("Scene {0} reached with unmatched player state", target));
return newScene;
}
private bool ConditionsMatch(Scene scene, BitArray playerState)
{
if (scene.Conditions == null) return true;
return scene.Conditions.All(c => playerState[_stateMatrix[c.Key]] == c.Value);
}
private PageState CloneState(PageState state)
{
return new PageState
{
Id = Guid.NewGuid(),
Links = new Dictionary<string, string>(),
PlayerState = state.PlayerState.Clone() as BitArray,
ScenesSeen = state.ScenesSeen.Clone() as BitArray,
ActionsToShow = new BitArray(_actionCount)
};
}
}
}