adding negative conditions and toggles... what the heck... also hard-coded file paths in the tests, so awesome

This commit is contained in:
Rudis Muiznieks 2014-07-02 23:11:36 -05:00
parent d72b393e21
commit 178a44a1b9
28 changed files with 444 additions and 417 deletions

View File

@ -51,7 +51,7 @@
<Compile Include="BlockHandlerTests.cs" />
<Compile Include="IntegrationTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SceneLinkerTests.cs" />
<Compile Include="StateResolverTests.cs" />
<Compile Include="TestStories\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>

View File

@ -1,9 +1,8 @@
namespace Ficdown.Parser.Tests
{
using System;
using System.Linq;
using System.Text;
using ServiceStack.Text;
using Player;
using TestStories;
using Xunit;
@ -18,7 +17,9 @@
Assert.NotNull(story);
Assert.Equal("The Robot King", story.Name);
Assert.Equal("Robot Cave", story.Scenes[story.FirstScene].First().Name);
Console.WriteLine(story.Dump());
var player = new GameTraverser();
player.ExportStaticStory(story, @"C:\Users\Rudis\Desktop\template.html", @"C:\Users\Rudis\Desktop\output");
}
}
}

View File

@ -1,96 +0,0 @@
namespace Ficdown.Parser.Tests
{
using System;
using System.Collections.Generic;
using System.Linq;
using Engine;
using Model.Story;
using ServiceStack.Text;
using Xunit;
public class SceneLinkerTests
{
private Story MockStoryWithScenes(IEnumerable<Scene> scenes)
{
var sceneDict = new Dictionary<string, IList<Scene>>();
foreach (var scene in scenes)
{
var key = Utilities.NormalizeString(scene.Name);
if(!sceneDict.ContainsKey(key)) sceneDict.Add(key, new List<Scene>());
sceneDict[key].Add(scene);
}
return new Story
{
Name = "Test Story",
Description = "Story description.",
FirstScene = sceneDict.First().Key,
Scenes = sceneDict
};
}
[Fact]
public void ConditionalAnchorGetsReplacedCorrectly()
{
var sl = new SceneLinker();
var story = MockStoryWithScenes(new[]
{
new Scene
{
Name = "Test Scene",
Description = "Test [passed|failed](?test-condition) text."
}
});
sl.ExpandScenes(story);
Console.WriteLine(story.Dump());
Assert.Equal(2, story.Scenes["test-scene"].Count);
Scene passed = null, failed = null;
Assert.DoesNotThrow(() =>
passed =
story.Scenes["test-scene"].SingleOrDefault(
s =>
s.Conditions != null && s.Conditions.Contains("test-condition") &&
s.Description.Equals("Test passed text.")));
Assert.DoesNotThrow(() =>
failed =
story.Scenes["test-scene"].SingleOrDefault(
s => s.Conditions == null && s.Description.Equals("Test failed text.")));
Assert.NotNull(passed);
Assert.NotNull(failed);
}
[Fact]
public void MultipleConditionalAnchorsGetReplacedCorrectly()
{
var sl = new SceneLinker();
var story = MockStoryWithScenes(new[]
{
new Scene
{
Name = "Test Scene",
Description =
"Test1 [passed1|failed1](?test1-condition). Test2 [passed2|failed2](?test2-condition)."
}
});
sl.ExpandScenes(story);
Console.WriteLine(story.Dump());
Assert.Equal(4, story.Scenes["test-scene"].Count);
Assert.Equal(
story.Scenes["test-scene"].Select(s => s.Conditions == null ? null : s.Conditions.ToArray()).ToArray(),
new[]
{
null, new[] {"test1-condition"}, new[] {"test2-condition"},
new[] {"test1-condition", "test2-condition"}
});
Assert.False(
story.Scenes["test-scene"].Any(
s =>
s.Conditions != null && s.Conditions.Contains("test1-condition") &&
s.Description.Contains("Test1 failed1.")));
Assert.False(
story.Scenes["test-scene"].Any(
s =>
s.Conditions != null && s.Conditions.Contains("test2-condition") &&
s.Description.Contains("Test2 failed2.")));
}
}
}

View File

@ -0,0 +1,6 @@
namespace Ficdown.Parser.Tests
{
public class StateResolverTests
{
}
}

View File

@ -109,7 +109,7 @@ There is a grand double staircase leading up to the throne room, a hallway strai
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.
- [Go back to the palace entrance.](/palace-entrance#tried-gate)
[Go back to the palace entrance.](/palace-entrance#tried-gate)
## Palace Basement
@ -149,19 +149,21 @@ He walks to a box in the corner and pulls out a blue janitor's uniform, then han
The Master Janitor Robot scratches his chin for a moment, then resumes pacing back and forth and muttering to himself.
## [Living Quarters](?)
## [Living Quarters](?uniform)
You head into the hallway that leads to the living quarters and come to a large gate. A scanner attached to the gate lights up and beeps a few times. After a moment, you hear a click and a soft hiss as the gate opens to let you pass. Once you walk through, the gate hisses and clicks shut behind you.
You notice with some alarm that there's no scanner on the inside of the gate. You don't know how to get back out!
## [Living Quarters](?uniform)
[Continue...](/living-quarters-2)
## [Living Quarters 2]("Living Quarters")
That's when you realize that you never asked the Master Janitor Bot what your job here was. You just took your uniform and left!
**You have failed to perform your new job because you never found out what it was.**
## [Living Quarters](?uniform&job-started)
## [Living Quarters 2](?job-started "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."

View File

@ -1,11 +1,28 @@
namespace Ficdown.Parser.Tests
{
using System.Collections.Generic;
using Engine;
using System;
using Parser;
using Xunit;
public class UtilityTests
{
[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);
anchorStr = @"[Link text](?condition-state#toggle-state ""Title text"")";
anchor = RegexLib.Anchors.Match(anchorStr);
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value);
anchorStr = @"[Link text](""Title text"")";
anchor = RegexLib.Anchors.Match(anchorStr);
Assert.Equal(anchorStr, anchor.Groups["anchor"].Value);
}
[Fact]
public void AnchorWithTargetMatches()
{
@ -46,15 +63,30 @@
Assert.True(anchor.Success);
Assert.Equal("Link text", anchor.Groups["text"].Value);
Assert.Equal("#toggle-1+toggle-2", anchor.Groups["href"].Value);
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);
}
[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);
}
[Fact]
public void ComplexAnchorsMatch()
{
var anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene?condition-state#toggle-state)");
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);
anchor = RegexLib.Anchors.Match(@"[Link text](/target-scene#toggle-state)");
Assert.True(anchor.Success);
@ -75,84 +107,76 @@
[Fact]
public void HrefWithTargetParses()
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref("/target-scene", out target, out conditions, out toggles);
Assert.Equal("target-scene", target);
Assert.Null(conditions);
Assert.Null(toggles);
var anchors = Utilities.ParseAnchors("[Anchor](/target-scene)");
Assert.Equal("target-scene", anchors[0].Href.Target);
Assert.Null(anchors[0].Href.Conditions);
Assert.Null(anchors[0].Href.Toggles);
}
[Fact]
public void HrefsWithConditionsParse()
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref("?condition-state", out target, out conditions, out toggles);
Assert.Null(target);
Assert.Equal(1, conditions.Count);
Assert.Contains("condition-state", conditions);
Assert.Null(toggles);
var anchors = Utilities.ParseAnchors("[Anchor](?condition-state)");
Assert.Null(anchors[0].Href.Target);
Assert.Equal(1, anchors[0].Href.Conditions.Count);
Assert.True(anchors[0].Href.Conditions["condition-state"]);
Assert.Null(anchors[0].Href.Toggles);
Utilities.ParseHref("?condition-1&condition-2", out target, out conditions, out toggles);
Assert.Null(target);
Assert.Equal(2, conditions.Count);
Assert.Contains("condition-1", conditions);
Assert.Contains("condition-2", conditions);
Assert.Null(toggles);
anchors = Utilities.ParseAnchors("[Anchor](?condition-1&!condition-2)");
Assert.Null(anchors[0].Href.Target);
Assert.Equal(2, anchors[0].Href.Conditions.Count);
Assert.True(anchors[0].Href.Conditions["condition-1"]);
Assert.False(anchors[0].Href.Conditions["condition-2"]);
Assert.Null(anchors[0].Href.Toggles);
}
[Fact]
public void HrefsWithTogglesParse()
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref("#toggle-state", out target, out conditions, out toggles);
Assert.Null(target);
Assert.Null(conditions);
Assert.Equal(1, toggles.Count);
Assert.Contains("toggle-state", toggles);
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"]);
Utilities.ParseHref("#toggle-1+toggle-2", out target, out conditions, out toggles);
Assert.Null(target);
Assert.Null(conditions);
Assert.Equal(2, toggles.Count);
Assert.Contains("toggle-1", toggles);
Assert.Contains("toggle-2", toggles);
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"]);
}
[Fact]
public void ComplexHrefsParse()
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref("/target-scene?condition-state#toggle-state", out target, out conditions, out toggles);
Assert.Equal("target-scene", target);
Assert.Equal(1, conditions.Count);
Assert.Contains("condition-state", conditions);
Assert.Equal(1, toggles.Count);
Assert.Contains("toggle-state", toggles);
var anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state#toggle-state)");
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"]);
Utilities.ParseHref("/target-scene?condition-state", out target, out conditions, out toggles);
Assert.Equal("target-scene", target);
Assert.Equal(1, conditions.Count);
Assert.Contains("condition-state", conditions);
Assert.Null(toggles);
anchors = Utilities.ParseAnchors("[Anchor](/target-scene?condition-state)");
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.Null(anchors[0].Href.Toggles);
Utilities.ParseHref("/target-scene#toggle-state", out target, out conditions, out toggles);
Assert.Equal("target-scene", target);
Assert.Null(conditions);
Assert.Equal(1, toggles.Count);
Assert.Contains("toggle-state", toggles);
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"]);
Utilities.ParseHref("?condition-one&condition-two#toggle-one+toggle-two", out target, out conditions, out toggles);
Assert.Null(target);
Assert.Equal(2, conditions.Count);
Assert.Contains("condition-one", conditions);
Assert.Contains("condition-two", conditions);
Assert.Equal(2, toggles.Count);
Assert.Contains("toggle-one", toggles);
Assert.Contains("toggle-two", toggles);
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"]);
}
}
}

View File

@ -1,9 +0,0 @@
namespace Ficdown.Parser.Engine
{
using Model.Story;
public interface ISceneLinker
{
void ExpandScenes(Story story);
}
}

View File

@ -1,126 +0,0 @@
namespace Ficdown.Parser.Engine
{
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using Model.Story;
using Model.Story.Extensions;
public class SceneLinker : ISceneLinker
{
public void ExpandScenes(Story story)
{
VerifySanity(story);
var newScenes = new Dictionary<string, IList<Scene>>();
foreach(var key in story.Scenes.Keys)
{
newScenes.Add(key, new List<Scene>());
foreach (var scene in story.Scenes[key])
{
var anchors = RegexLib.Anchors.Matches(scene.Description);
// get a list of all unique condition combinations from the anchors
var uniques = new List<IList<string>>();
foreach (Match anchor in anchors)
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref(anchor.Groups["href"].Value, out target, out conditions, out toggles);
if (conditions != null)
{
// union with the conditions required to reach this scene, if any
if (scene.Conditions != null)
{
conditions = conditions.Union(scene.Conditions).ToList();
if (conditions.Count == scene.Conditions.Count)
continue; //WARN this anchor will never resolve false
}
AddUnique(uniques, conditions);
}
}
// resolve the current scene
var original = scene.Clone();
newScenes[key].Add(ResolveScene(scene, anchors));
// resolve the uniques
foreach (var unique in uniques)
{
var uscene = original.Clone();
uscene.Conditions = unique;
newScenes[key].Add(ResolveScene(uscene, anchors));
}
}
}
story.Scenes = newScenes;
}
private void AddUnique(IList<IList<string>> uniques, IList<string> conditions)
{
// ignore this combo if there's a contradiction
if (conditions.Where(c => !c.StartsWith("!"))
.Any(c => conditions.Contains(string.Format("!{0}", c))))
return; // WARN this anchor will never resolve true
// make sure this is actually unique
if (uniques.Any(u => u.Intersect(conditions).Count() == conditions.Count)) return;
uniques.Add(conditions);
// we need to treat this unioned with all other existing uniques as another potential unique
var existing = new List<IList<string>>(uniques);
foreach (var old in existing)
{
AddUnique(uniques, old.Union(conditions).ToList());
}
}
private Scene ResolveScene(Scene scene, MatchCollection anchors)
{
foreach (Match anchor in anchors)
{
string target;
IList<string> conditions, toggles;
Utilities.ParseHref(anchor.Groups["href"].Value, out target, out conditions, out toggles);
if (conditions != null)
{
var satisfied = scene.Conditions != null && conditions.All(c => scene.Conditions.Contains(c));
var text = anchor.Groups["text"].Value;
var alts = RegexLib.ConditionalText.Match(text);
if (!alts.Success)
throw new FormatException(string.Format("Bad conditional anchor: {0}",
anchor.Groups["anchor"].Value));
var replace =
RegexLib.EscapeChar.Replace(satisfied ? alts.Groups["true"].Value : alts.Groups["false"].Value,
string.Empty);
// if there's no target or toggles, or the replace text is an empty string, replace the whole anchor
if (string.IsNullOrEmpty(replace) || (target == null && toggles == null))
{
scene.Description = scene.Description.Replace(anchor.Groups["anchor"].Value, replace);
}
// if there's a target or toggles, replace the text and remove the conditions on the anchor
else
{
var parsedHref = RegexLib.Href.Match(anchor.Groups["href"].Value);
scene.Description = scene.Description.Replace(anchor.Groups["anchor"].Value,
string.Format("[{0}]({1}{2})", replace, parsedHref.Groups["target"].Value,
parsedHref.Groups["toggles"].Value));
}
}
}
return scene;
}
private void VerifySanity(Story story)
{
}
}
}

View File

@ -1,55 +0,0 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
namespace Ficdown.Parser.Engine
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
internal static class Utilities
{
public static string NormalizeString(string raw)
{
return Regex.Replace(Regex.Replace(raw.ToLower(), @"^\W+|\W+$", string.Empty), @"\W+", "-");
}
public static void ParseHref(string href, out string target)
{
IList<string> conditions, toggles;
ParseHref(href, out target, out conditions, out toggles);
if(conditions != null || toggles != null) throw new FormatException();
}
public static void ParseHref(string href, out IList<string> conditions)
{
string target;
IList<string> toggles;
ParseHref(href, out target, out conditions, out toggles);
if(target != null || toggles != null) throw new FormatException();
}
public static void ParseHref(string href, out string target, out IList<string> conditions, out IList<string> toggles)
{
target = null;
conditions = null;
toggles = null;
var match = RegexLib.Href.Match(href);
if (match.Success)
{
var ttstr = match.Groups["target"].Value;
var cstr = match.Groups["conditions"].Value;
var tstr = match.Groups["toggles"].Value;
if (!string.IsNullOrEmpty(ttstr))
target = ttstr.TrimStart('/');
if (!string.IsNullOrEmpty(cstr))
conditions = new List<string>(cstr.TrimStart('?').Split('&').Select(c => c.Trim().ToLower()));
if (!string.IsNullOrEmpty(tstr))
toggles = new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower()));
}
else throw new FormatException(string.Format("Invalid href: {0}", href));
}
}
}

View File

@ -1,8 +1,8 @@
namespace Ficdown.Parser
{
using System;
using Engine;
using Model.Story;
using Parser;
public class FicdownParser
{
@ -13,12 +13,12 @@
set { _blockHandler = value; }
}
private ISceneLinker _sceneLinker;
private IStateResolver _stateResolver;
public ISceneLinker SceneLinker
public IStateResolver StateResolver
{
get { return _sceneLinker ?? (_sceneLinker = new SceneLinker()); }
set { _sceneLinker = value; }
get { return _stateResolver ?? (_stateResolver = new StateResolver()); }
set { _stateResolver = value; }
}
public Story ParseStory(string storyText)
@ -26,7 +26,6 @@
var lines = storyText.Split(new[] {"\n", "\r\n"}, StringSplitOptions.None);
var blocks = BlockHandler.ExtractBlocks(lines);
var story = BlockHandler.ParseBlocks(blocks);
SceneLinker.ExpandScenes(story);
return story;
}
}

View File

@ -30,6 +30,9 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="MarkdownSharp">
<HintPath>..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
@ -39,21 +42,30 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Engine\BlockHandler.cs" />
<Compile Include="Engine\IBlockHandler.cs" />
<Compile Include="Engine\RegexLib.cs" />
<Compile Include="Engine\SceneLinker.cs" />
<Compile Include="Engine\Utilities.cs" />
<Compile Include="Player\IRenderer.cs" />
<Compile Include="Player\MarkdownRenderer.cs" />
<Compile Include="Parser\BlockHandler.cs" />
<Compile Include="Parser\IBlockHandler.cs" />
<Compile Include="Parser\RegexLib.cs" />
<Compile Include="Parser\StateResolver.cs" />
<Compile Include="Parser\Utilities.cs" />
<Compile Include="FicDownParser.cs" />
<Compile Include="Model\Parser\Anchor.cs" />
<Compile Include="Model\Parser\Block.cs" />
<Compile Include="Model\Parser\BlockType.cs" />
<Compile Include="Engine\ISceneLinker.cs" />
<Compile Include="Parser\IStateResolver.cs" />
<Compile Include="Model\Parser\Href.cs" />
<Compile Include="Model\Player\PlayerState.cs" />
<Compile Include="Model\Story\Extensions\SceneExtensions.cs" />
<Compile Include="Player\GameTraverser.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Model\Story\Action.cs" />
<Compile Include="Model\Story\Scene.cs" />
<Compile Include="Model\Story\Story.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@ -0,0 +1,10 @@
namespace Ficdown.Parser.Model.Parser
{
internal class Anchor
{
public string Original { get; set; }
public string Text { get; set; }
public Href Href { get; set; }
public string Title { get; set; }
}
}

View File

@ -0,0 +1,12 @@
namespace Ficdown.Parser.Model.Parser
{
using System.Collections.Generic;
internal class Href
{
public string Original { get; set; }
public string Target { get; set; }
public IDictionary<string, bool> Conditions { get; set; }
public IDictionary<string, bool> Toggles { get; set; }
}
}

View File

@ -0,0 +1,22 @@
namespace Ficdown.Parser.Model.Player
{
using System.Collections.Generic;
internal class PlayerState : Dictionary<string, bool>
{
public PlayerState(IDictionary<string, bool> copyFrom) : base(copyFrom)
{
}
public PlayerState Clone()
{
return new PlayerState(this);
}
public void VisitedScene(string sceneId)
{
var key = string.Format(">{0}", sceneId);
if (!ContainsKey(key)) Add(key, true);
}
}
}

View File

@ -1,11 +1,8 @@
namespace Ficdown.Parser.Model.Story
{
using System.Collections.Generic;
public class Action
{
public string State { get; set; }
public string Description { get; set; }
public IList<string> Conditions { get; set; }
public IList<string> Toggles { get; set; }
}
}

View File

@ -10,7 +10,7 @@
{
Name = scene.Name,
Description = scene.Description,
Conditions = scene.Conditions == null ? null : new List<string>(scene.Conditions)
Conditions = scene.Conditions == null ? null : new Dictionary<string, bool>(scene.Conditions)
};
}
}

View File

@ -5,7 +5,8 @@
public class Scene
{
public string Name { get; set; }
public string Key { get; set; }
public string Description { get; set; }
public IList<string> Conditions { get; set; }
public IDictionary<string, bool> Conditions { get; set; }
}
}

View File

@ -7,7 +7,7 @@
public string Name { get; set; }
public string Description { get; set; }
public string FirstScene { get; set; }
public IDictionary<string, IList<Action>> States { get; set; }
public IDictionary<string, IList<Scene>> Scenes { get; set; }
public IDictionary<string, Action> Actions { get; set; }
}
}

View File

@ -1,4 +1,4 @@
namespace Ficdown.Parser.Engine
namespace Ficdown.Parser.Parser
{
using System;
using System.Collections.Generic;
@ -40,41 +40,33 @@
{
// get the story
var storyBlock = blocks.Single(b => b.Type == BlockType.Story);
var storyName = RegexLib.Anchors.Match(storyBlock.Name);
var storyAnchor = Utilities.ParseAnchor(storyBlock.Name);
string storyTarget;
try
{
Utilities.ParseHref(storyName.Groups["href"].Value, out storyTarget);
}
catch (FormatException)
{
if (storyAnchor.Href.Target == null || storyAnchor.Href.Conditions != null ||
storyAnchor.Href.Toggles != null)
throw new FormatException(string.Format("Story href should only have target: {0}",
storyName.Groups["href"].Value));
}
if (!storyName.Success)
throw new FormatException("Story name must link to the first scene.");
storyAnchor.Original));
var story = new Story
{
Name = storyName.Groups["text"].Value,
Name = storyAnchor.Text,
Description = string.Join("\n", storyBlock.Lines).Trim(),
Scenes = new Dictionary<string, IList<Scene>>(),
States = new Dictionary<string, IList<Action>>()
Actions = new Dictionary<string, Action>()
};
var scenes = blocks.Where(b => b.Type == BlockType.Scene).Select(BlockToScene);
foreach (var scene in scenes)
{
var key = Utilities.NormalizeString(scene.Name);
if (!story.Scenes.ContainsKey(key)) story.Scenes.Add(key, new List<Scene>());
story.Scenes[key].Add(scene);
if (!story.Scenes.ContainsKey(scene.Key)) story.Scenes.Add(scene.Key, new List<Scene>());
story.Scenes[scene.Key].Add(scene);
}
story.Actions =
blocks.Where(b => b.Type == BlockType.Action).Select(BlockToAction).ToDictionary(a => a.State, a => a);
if (!story.Scenes.ContainsKey(storyTarget))
throw new FormatException(string.Format("Story targets non-existent scene: {0}", storyTarget));
story.FirstScene = storyTarget;
if (!story.Scenes.ContainsKey(storyAnchor.Href.Target))
throw new FormatException(string.Format("Story targets non-existent scene: {0}", storyAnchor.Href.Target));
story.FirstScene = storyAnchor.Href.Target;
return story;
}
@ -87,24 +79,31 @@
Description = string.Join("\n", block.Lines).Trim()
};
var sceneName = RegexLib.Anchors.Match(block.Name);
if (sceneName.Success)
try
{
scene.Name = sceneName.Groups["text"].Value.Trim();
IList<string> conditions;
try
{
Utilities.ParseHref(sceneName.Groups["href"].Value, out conditions);
}
catch (FormatException)
{
var sceneName = Utilities.ParseAnchor(block.Name);
scene.Name = sceneName.Title != null ? sceneName.Title.Trim() : sceneName.Text.Trim();
scene.Key = Utilities.NormalizeString(sceneName.Text);
if(sceneName.Href.Target != null || sceneName.Href.Toggles != null)
throw new FormatException(string.Format("Scene href should only have conditions: {0}", block.Name));
}
scene.Conditions = conditions;
scene.Conditions = sceneName.Href.Conditions;
}
catch(FormatException)
{
scene.Name = block.Name.Trim();
scene.Key = Utilities.NormalizeString(block.Name);
}
else scene.Name = block.Name;
return scene;
}
private Action BlockToAction(Block block)
{
return new Action
{
State = Utilities.NormalizeString(block.Name),
Description = string.Join("\n", block.Lines).Trim()
};
}
}
}

View File

@ -1,4 +1,4 @@
namespace Ficdown.Parser.Engine
namespace Ficdown.Parser.Parser
{
using System.Collections.Generic;
using Model.Parser;

View File

@ -0,0 +1,9 @@
namespace Ficdown.Parser.Parser
{
using System.Collections.Generic;
public interface IStateResolver
{
string Resolve(string description, IDictionary<string, bool> playState, bool firstSeen);
}
}

View File

@ -1,4 +1,4 @@
namespace Ficdown.Parser.Engine
namespace Ficdown.Parser.Parser
{
using System.Text;
using System.Text.RegularExpressions;
@ -7,8 +7,9 @@
{
public static Regex Anchors =
new Regex(
string.Format(@"(?<anchor>\[(?<text>{0})\]\([ ]*(?<href>{1})[ ]*\))", GetNestedBracketsPattern(),
GetNestedParensPattern()), RegexOptions.Singleline | RegexOptions.Compiled);
string.Format(@"(?<anchor>\[(?<text>{0})\]\([ ]*(?<href>{1})[ ]*((['""])(?<title>.*?)\2[ ]*)?\))",
GetNestedBracketsPattern(), GetNestedParensPattern()),
RegexOptions.Singleline | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);
public static Regex ConditionalText = new Regex(@"^(?<true>([^|\\]|\\.)*)(\|(?<false>([^|\\]|\\.)+))?$",
RegexOptions.Singleline | RegexOptions.Compiled);
@ -17,14 +18,21 @@
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 RegexHrefConditions = string.Format(@"\?((!?{0})(&!?{0})*)?", RegexValidName);
private static readonly string RegexHrefToggles = string.Format(@"#(!?{0})(\+!?{0})*", RegexValidName);
public static Regex Href =
new Regex(
string.Format(@"^(?<target>{0})?(?<conditions>{1})?(?<toggles>{2})?$", RegexHrefTarget,
RegexHrefConditions, RegexHrefToggles), RegexOptions.Compiled);
public static Regex EmptyListItem = new Regex(@"^[ ]*-\s*([\r\n]+|$)",
RegexOptions.Multiline | RegexOptions.Compiled);
public static Regex BlockQuotes = new Regex(@"((^[ ]*>[ ]?.+\n(.+\n)*\n*)+)",
RegexOptions.Multiline | RegexOptions.Compiled);
public static Regex BlockQuoteToken = new Regex(@"^[ ]*>[ ]?", RegexOptions.Multiline | RegexOptions.Compiled);
private const int _nestDepth = 6;
@ -41,7 +49,7 @@
{
if (_nestedBracketsPattern == null)
_nestedBracketsPattern =
RepeatString(@"(?>[^\[\]]+|\[", _nestDepth) + RepeatString(@"\])*", _nestDepth);
RepeatString(@"(?:[^\[\]]+|\[", _nestDepth) + RepeatString(@"\])*", _nestDepth);
return _nestedBracketsPattern;
}
@ -50,7 +58,7 @@
{
if (_nestedParensPattern == null)
_nestedParensPattern =
RepeatString(@"(?>[^()\s]+|\(", _nestDepth) + RepeatString(@"\))*", _nestDepth);
RepeatString(@"(?:[^()\s]+|\(", _nestDepth) + RepeatString(@"\))*", _nestDepth);
return _nestedParensPattern;
}
}

View File

@ -0,0 +1,36 @@
namespace Ficdown.Parser.Parser
{
using System.Collections.Generic;
public class StateResolver : IStateResolver
{
public string Resolve(string description, IDictionary<string, bool> playerState, bool firstSeen)
{
foreach (var anchor in Utilities.ParseAnchors(description))
{
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);
}
}
}
return firstSeen
? RegexLib.BlockQuoteToken.Replace(description, string.Empty)
: RegexLib.BlockQuotes.Replace(description, string.Empty);
}
}
}

View File

@ -0,0 +1,106 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Ficdown.Parser.Tests")]
namespace Ficdown.Parser.Parser
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Model.Parser;
internal static class Utilities
{
public static string NormalizeString(string raw)
{
return Regex.Replace(Regex.Replace(raw.ToLower(), @"^\W+|\W+$", string.Empty), @"\W+", "-");
}
private static Href ParseHref(string href)
{
var match = RegexLib.Href.Match(href);
if (match.Success)
{
var ttstr = match.Groups["target"].Value;
var cstr = match.Groups["conditions"].Value;
var tstr = match.Groups["toggles"].Value;
return new Href
{
Original = href,
Target = !string.IsNullOrEmpty(ttstr) ? ttstr.TrimStart('/') : null,
Conditions =
!string.IsNullOrEmpty(cstr)
? new List<string>(cstr.TrimStart('?').Split('&').Select(c => c.Trim().ToLower()))
.ToDictionary(c => c.TrimStart('!'), c => !c.StartsWith("!"))
: null,
Toggles =
!string.IsNullOrEmpty(tstr)
? new List<string>(tstr.TrimStart('#').Split('+').Select(t => t.Trim().ToLower()))
.ToDictionary(t => t.TrimStart('!'), t => !t.StartsWith("!"))
: null
};
}
throw new FormatException(string.Format("Invalid href: {0}", href));
}
public static Anchor ParseAnchor(string anchorText)
{
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)
};
}
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();
}
public static IDictionary<bool, string> ParseConditionalText(string text)
{
var match = RegexLib.ConditionalText.Match(text);
if (!match.Success) throw new FormatException(string.Format(@"Invalid conditional text: {0}", text));
return new Dictionary<bool, string>
{
{true, match.Groups["true"].Value},
{false, match.Groups["false"].Value}
};
}
public static string ToHrefString(this IDictionary<string, bool> values, string separator)
{
return values != null
? string.Join(separator,
values.Where(v => !v.Key.StartsWith(">"))
.Select(v => string.Format("{0}{1}", v.Value ? null : "!", v.Key))
.ToArray())
: null;
}
public static bool ConditionsMet(IDictionary<string, bool> playerState, IDictionary<string, bool> conditions)
{
return
conditions.All(
c =>
(!c.Value && (!playerState.ContainsKey(c.Key) || !playerState[c.Key])) ||
(playerState.ContainsKey(c.Key) && playerState[c.Key]));
}
}
}

View File

@ -0,0 +1,44 @@
namespace Ficdown.Parser.Player
{
using System.IO;
using System.Threading;
using Model.Story;
internal class GameTraverser
{
private IRenderer _renderer;
public IRenderer Renderer
{
get
{
return _renderer ??
(_renderer =
new HtmlRenderer {Template = File.ReadAllText(@"C:\Users\Rudis\Desktop\template.html")});
}
set { _renderer = value; }
}
private volatile int _page = 0;
private string _template;
public void ExportStaticStory(Story story, string templateFile, string outputDirectory)
{
_template = File.ReadAllText(templateFile);
var dir = new DirectoryInfo(outputDirectory);
if (!dir.Exists) dir.Create();
else
{
foreach (var finfo in dir.GetFileSystemInfos())
{
finfo.Delete();
}
}
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"));
}
}
}

View File

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

View File

@ -0,0 +1,14 @@
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,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="MarkdownSharp" version="1.13.0.0" targetFramework="net45" />
</packages>