full in-memory enumeration of stories complete
This commit is contained in:
parent
178a44a1b9
commit
1d0834e8ac
14 changed files with 310 additions and 130 deletions
|
@ -18,8 +18,8 @@
|
|||
Assert.Equal("The Robot King", story.Name);
|
||||
Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name);
|
||||
|
||||
var player = new GameTraverser();
|
||||
player.ExportStaticStory(story, @"C:\Users\Rudis\Desktop\template.html", @"C:\Users\Rudis\Desktop\output");
|
||||
var traverser = new GameTraverser(story);
|
||||
var test = traverser.Enumerate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?**
|
||||
|
||||
- [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 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
|
||||
|
||||
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.**
|
||||
|
||||
## [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."
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
namespace Ficdown.Parser.Tests
|
||||
{
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Parser;
|
||||
using Xunit;
|
||||
|
||||
|
@ -9,99 +9,91 @@
|
|||
[Fact]
|
||||
public void FullAnchorMatches()
|
||||
{
|
||||
Console.WriteLine(RegexLib.Href.ToString());
|
||||
var anchorStr = @"[Link text](/target-scene)";
|
||||
var anchor = RegexLib.Anchors.Match(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value);
|
||||
var anchor = Utilities.ParseAnchor(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Original);
|
||||
|
||||
anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")";
|
||||
anchor = RegexLib.Anchors.Match(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value);
|
||||
anchor = Utilities.ParseAnchor(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Original);
|
||||
|
||||
anchorStr = @"[Link text](""Title text"")";
|
||||
anchor = RegexLib.Anchors.Match(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value);
|
||||
anchor = Utilities.ParseAnchor(anchorStr);
|
||||
Assert.Equal(anchorStr, anchor.Original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnchorWithTargetMatches()
|
||||
{
|
||||
var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("/target-scene", anchor.Groups["href"].Value);
|
||||
var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("target-scene", anchor.Href.Target);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnchorsWithConditionsMatch()
|
||||
{
|
||||
var anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("?condition-state", anchor.Groups["href"].Value);
|
||||
var anchor = Utilities.ParseAnchor(@"[Link text](?condition-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.True(anchor.Href.Conditions["condition-state"]);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](?!condition-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("?!condition-state", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](?!condition-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.False(anchor.Href.Conditions["condition-state"]);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](?condition-1&!condition-2)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("?condition-1&!condition-2", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](?condition-1&!condition-2)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.True(anchor.Href.Conditions["condition-1"]);
|
||||
Assert.False(anchor.Href.Conditions["condition-2"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnchorsWithTogglesMatch()
|
||||
{
|
||||
var anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("#toggle-state", anchor.Groups["href"].Value);
|
||||
var anchor = Utilities.ParseAnchor(@"[Link text](#toggle-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("#toggle-state", anchor.Href.Original);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-1+toggle-2)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("#toggle-1+toggle-2", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](#toggle-1+!toggle-2)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("#toggle-1+!toggle-2", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](#toggle-1+toggle-2)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("#toggle-1+toggle-2", anchor.Href.Original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnchorsWithTitlesMatch()
|
||||
{
|
||||
var anchor = RegexLib.Anchors.Match(@"[Link text](""Title text"")");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("Title text", anchor.Groups["title"].Value);
|
||||
var anchor = Utilities.ParseAnchor(@"[Link text](""Title text"")");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("Title text", anchor.Title);
|
||||
|
||||
anchor = Utilities.ParseAnchor(@"[Talking to Kid](""Lobby"")");
|
||||
Assert.Equal("Talking to Kid", anchor.Text);
|
||||
Assert.Equal("Lobby", anchor.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComplexAnchorsMatch()
|
||||
{
|
||||
var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Groups["href"].Value);
|
||||
Assert.Equal("Title text", anchor.Groups["title"].Value);
|
||||
var anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state#toggle-state ""Title text"")");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("/target-scene?condition-state#toggle-state", anchor.Href.Original);
|
||||
Assert.Equal("Title text", anchor.Title);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene#toggle-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("/target-scene#toggle-state", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](/target-scene#toggle-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("/target-scene#toggle-state", anchor.Href.Original);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("/target-scene?condition-state", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](/target-scene?condition-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("/target-scene?condition-state", anchor.Href.Original);
|
||||
|
||||
anchor = RegexLib.Anchors.Match(@"[Link text](?condition-state#toggle-state)");
|
||||
Assert.True(anchor.Success);
|
||||
Assert.Equal("Link text", anchor.Groups["text"].Value);
|
||||
Assert.Equal("?condition-state#toggle-state", anchor.Groups["href"].Value);
|
||||
anchor = Utilities.ParseAnchor(@"[Link text](?condition-state#toggle-state)");
|
||||
Assert.Equal("Link text", anchor.Text);
|
||||
Assert.Equal("?condition-state#toggle-state", anchor.Href.Original);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -136,15 +128,12 @@
|
|||
var anchors = Utilities.ParseAnchors("[Anchor](#toggle-state)");
|
||||
Assert.Null(anchors[0].Href.Target);
|
||||
Assert.Null(anchors[0].Href.Conditions);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count);
|
||||
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count());
|
||||
|
||||
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.Conditions);
|
||||
Assert.Equal(2, anchors[0].Href.Toggles.Count);
|
||||
Assert.True(anchors[0].Href.Toggles["toggle-1"]);
|
||||
Assert.False(anchors[0].Href.Toggles["toggle-2"]);
|
||||
Assert.Equal(2, anchors[0].Href.Toggles.Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -154,8 +143,7 @@
|
|||
Assert.Equal("target-scene", anchors[0].Href.Target);
|
||||
Assert.Equal(1, anchors[0].Href.Conditions.Count);
|
||||
Assert.True(anchors[0].Href.Conditions["condition-state"]);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count);
|
||||
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count());
|
||||
|
||||
anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state)");
|
||||
Assert.Equal("target-scene", anchors[0].Href.Target);
|
||||
|
@ -166,17 +154,14 @@
|
|||
anchors = Utilities.ParseAnchors("[Anchor](/target-scene#toggle-state)");
|
||||
Assert.Equal("target-scene", anchors[0].Href.Target);
|
||||
Assert.Null(anchors[0].Href.Conditions);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count);
|
||||
Assert.True(anchors[0].Href.Toggles["toggle-state"]);
|
||||
Assert.Equal(1, anchors[0].Href.Toggles.Count());
|
||||
|
||||
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.Equal(2, anchors[0].Href.Conditions.Count);
|
||||
Assert.True(anchors[0].Href.Conditions["condition-one"]);
|
||||
Assert.False(anchors[0].Href.Conditions["condition-two"]);
|
||||
Assert.Equal(2, anchors[0].Href.Toggles.Count);
|
||||
Assert.True(anchors[0].Href.Toggles["toggle-one"]);
|
||||
Assert.False(anchors[0].Href.Toggles["toggle-two"]);
|
||||
Assert.Equal(2, anchors[0].Href.Toggles.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,8 +42,9 @@
|
|||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Model\Traverser\PageState.cs" />
|
||||
<Compile Include="Player\IRenderer.cs" />
|
||||
<Compile Include="Player\MarkdownRenderer.cs" />
|
||||
<Compile Include="Player\HtmlRenderer.cs" />
|
||||
<Compile Include="Parser\BlockHandler.cs" />
|
||||
<Compile Include="Parser\IBlockHandler.cs" />
|
||||
<Compile Include="Parser\RegexLib.cs" />
|
||||
|
@ -58,6 +59,7 @@
|
|||
<Compile Include="Model\Player\PlayerState.cs" />
|
||||
<Compile Include="Model\Story\Extensions\SceneExtensions.cs" />
|
||||
<Compile Include="Player\GameTraverser.cs" />
|
||||
<Compile Include="Player\StateManager.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Model\Story\Action.cs" />
|
||||
<Compile Include="Model\Story\Scene.cs" />
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
public string Original { get; set; }
|
||||
public string Target { get; set; }
|
||||
public IDictionary<string, bool> Conditions { get; set; }
|
||||
public IDictionary<string, bool> Toggles { get; set; }
|
||||
public IEnumerable<string> Toggles { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
{
|
||||
public class Action
|
||||
{
|
||||
public string State { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Toggle { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
public class Scene
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Key { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
|
40
Ficdown.Parser/Model/Traverser/PageState.cs
Normal file
40
Ficdown.Parser/Model/Traverser/PageState.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,14 +55,16 @@
|
|||
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)
|
||||
{
|
||||
if (!story.Scenes.ContainsKey(scene.Key)) story.Scenes.Add(scene.Key, new List<Scene>());
|
||||
story.Scenes[scene.Key].Add(scene);
|
||||
}
|
||||
var aid = 1;
|
||||
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))
|
||||
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
|
||||
{
|
||||
Id = id,
|
||||
Description = string.Join("\n", block.Lines).Trim()
|
||||
};
|
||||
|
||||
|
@ -97,11 +100,12 @@
|
|||
return scene;
|
||||
}
|
||||
|
||||
private Action BlockToAction(Block block)
|
||||
private Action BlockToAction(Block block, int id)
|
||||
{
|
||||
return new Action
|
||||
{
|
||||
State = Utilities.NormalizeString(block.Name),
|
||||
Id = id,
|
||||
Toggle = Utilities.NormalizeString(block.Name),
|
||||
Description = string.Join("\n", block.Lines).Trim()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
private const string RegexValidName = @"[a-zA-Z](-?[a-zA-Z0-9])*";
|
||||
private static readonly string RegexHrefTarget = string.Format(@"\/({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 =
|
||||
new Regex(
|
||||
|
|
|
@ -36,8 +36,7 @@ namespace Ficdown.Parser.Parser
|
|||
: null,
|
||||
Toggles =
|
||||
!string.IsNullOrEmpty(tstr)
|
||||
? new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower()))
|
||||
.ToDictionary(t => t.TrimStart('!'), t => !t.StartsWith("!"))
|
||||
? new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower())).ToArray()
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
@ -48,29 +47,33 @@ namespace Ficdown.Parser.Parser
|
|||
{
|
||||
var match = RegexLib.Anchors.Match(anchorText);
|
||||
if (!match.Success) throw new FormatException(string.Format("Invalid anchor: {0}", anchorText));
|
||||
var astr = match.Groups["anchor"].Value;
|
||||
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)
|
||||
};
|
||||
return MatchToAnchor(match);
|
||||
}
|
||||
|
||||
public static IList<Anchor> ParseAnchors(string text)
|
||||
{
|
||||
var matches = RegexLib.Anchors.Matches(text);
|
||||
return matches.Cast<Match>().Select(m =>
|
||||
new Anchor
|
||||
{
|
||||
Original = m.Groups["anchor"].Value,
|
||||
Text = m.Groups["text"].Value,
|
||||
Title = m.Groups["title"].Value,
|
||||
Href = ParseHref(m.Groups["href"].Value)
|
||||
}).ToList();
|
||||
return matches.Cast<Match>().Select(MatchToAnchor).ToList();
|
||||
}
|
||||
|
||||
private static Anchor MatchToAnchor(Match match)
|
||||
{
|
||||
var astr = match.Groups["anchor"].Value;
|
||||
var txstr = match.Groups["text"].Value;
|
||||
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)
|
||||
|
@ -94,6 +97,11 @@ namespace Ficdown.Parser.Parser
|
|||
: 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)
|
||||
{
|
||||
return
|
||||
|
|
|
@ -1,44 +1,62 @@
|
|||
namespace Ficdown.Parser.Player
|
||||
{
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Model.Story;
|
||||
using Model.Traverser;
|
||||
using Parser;
|
||||
|
||||
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
|
||||
{
|
||||
return _renderer ??
|
||||
(_renderer =
|
||||
new HtmlRenderer {Template = File.ReadAllText(@"C:\Users\Rudis\Desktop\template.html")});
|
||||
}
|
||||
set { _renderer = value; }
|
||||
_manager = new StateManager(story);
|
||||
_processingQueue = new Queue<PageState>();
|
||||
_processed = new Dictionary<string, PageState>();
|
||||
}
|
||||
|
||||
private volatile int _page = 0;
|
||||
|
||||
private string _template;
|
||||
|
||||
public void ExportStaticStory(Story story, string templateFile, string outputDirectory)
|
||||
public IEnumerable<PageState> Enumerate()
|
||||
{
|
||||
_template = File.ReadAllText(templateFile);
|
||||
var dir = new DirectoryInfo(outputDirectory);
|
||||
if (!dir.Exists) dir.Create();
|
||||
else
|
||||
// generate comprehensive enumeration
|
||||
|
||||
_processingQueue.Enqueue(_manager.InitialState);
|
||||
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,
|
||||
story.Description, _page++);
|
||||
Renderer.Render(index, Path.Combine(outputDirectory, "index.html"));
|
||||
// compress redundancies
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
Ficdown.Parser/Player/StateManager.cs
Normal file
117
Ficdown.Parser/Player/StateManager.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue