Last active
August 22, 2018 15:20
-
-
Save kkismd/ce31253cf8369f605080d083c38cdaf1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using UnityEngine; | |
// TODO: UnityEngineへの依存を消したい | |
/// <summary> | |
/// コマンドのインターフェイス | |
/// TODO: インターフェイスいらないかもしれない? | |
/// </summary> | |
public interface ICommand | |
{ | |
void Start(TextController controller); | |
float Duration { get; } | |
bool IsFinished(); | |
} | |
/// <summary> | |
/// 共通のメソッドを書くところ | |
/// </summary> | |
public abstract class GenericCommand : ICommand | |
{ | |
protected float startedAt; | |
abstract public float Duration { get; } | |
public void Start(TextController controller) | |
{ | |
startedAt = Time.time; | |
handleCallBack(controller); | |
} | |
abstract public void handleCallBack(TextController controller); | |
/// <summary> | |
/// コマンドの実行時間を過ぎたか? | |
/// </summary> | |
/// <returns></returns> | |
public bool IsFinished() | |
{ | |
return Time.time - startedAt > Duration; | |
} | |
} | |
/// <summary> | |
/// テキストウィンドウにセリフを表示する | |
/// </summary> | |
public class DisplayCommand : GenericCommand | |
{ | |
private float interval; | |
public int LastLength { set; get; } | |
public string Text { get; } | |
public Action<string> Updator { set; get; } | |
/// <summary> | |
/// 表示開始したときにすでにセリフ欄に表示されていたテキスト | |
/// </summary> | |
public string RemainText { get; set; } | |
public DisplayCommand(string text, float interval) | |
{ | |
this.Text = text.Trim().Replace("\\n", "\n"); | |
this.interval = interval; | |
} | |
public override float Duration => Text.Length * interval; | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.DisplayCommandStarted(); | |
} | |
public IEnumerator StartUpdate() | |
{ | |
while (true) | |
{ | |
OnChange(); | |
yield return new WaitForSeconds(interval); | |
} | |
} | |
public void OnChange() | |
{ | |
if (IsTextChanged()) | |
{ | |
Updator(RemainText + currentText()); | |
} | |
TextUpdated(); | |
} | |
/// <summary> | |
/// 表示開始から経過した時間が想定表示時間の何%か確認し、表示文字数を出す | |
/// </summary> | |
/// <returns>表示文字数</returns> | |
public int currentLength() | |
{ | |
if (Text.Length == 0) { return 0; } | |
var rate = Mathf.Clamp01((Time.time - startedAt) / Duration); | |
var len = (int)(rate * Text.Length) + 1; | |
return Math.Min(len, Text.Length); | |
} | |
/// <summary> | |
/// 経過時間に応じた現在の表示すべき部分文字列 | |
/// </summary> | |
/// <returns>表示すべき部分文字列</returns> | |
public string currentText() | |
{ | |
return Text.Substring(0, currentLength()); | |
} | |
/// <summary> | |
/// 表示すべき文字列が時間経過で変化したか? | |
/// </summary> | |
/// <returns>変化したらTrue</returns> | |
public bool IsTextChanged() | |
{ | |
return LastLength != currentLength(); | |
} | |
/// <summary> | |
/// 表示を更新した | |
/// </summary> | |
public void TextUpdated() | |
{ | |
LastLength = currentLength(); | |
} | |
} | |
/// <summary> | |
/// 指定秒数間待つコマンド | |
/// </summary> | |
public class PauseCommand : GenericCommand | |
{ | |
public readonly float seconds; | |
public PauseCommand(float seconds) | |
{ | |
this.seconds = seconds; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.PauseCommandStarted(); | |
} | |
public override float Duration => seconds; | |
} | |
/// <summary> | |
/// 表示テキストをクリアする | |
/// </summary> | |
public class PageCommand : GenericCommand | |
{ | |
public override float Duration => 0f; | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.PageCommandStarted(); | |
} | |
} | |
public class CharaAllCommand : GenericCommand | |
{ | |
public ColorParam ColorParam { get; } | |
public string Label { get; } | |
public CharaAllCommand(ColorParam colorParam, string label) | |
{ | |
ColorParam = colorParam; | |
Label = label; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.CharaAllCommandStarted(); | |
} | |
public override float Duration => 0f; | |
} | |
/// <summary> | |
/// 色や透明度を変えるパラメータ | |
/// </summary> | |
public enum ColorParam { White, Gray, Hide } | |
/// <summary> | |
/// キャラの表示を入れ替える | |
/// TODO: とりあえず1秒固定にしておく | |
/// </summary> | |
public class CharaCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public Option<string> Face { get; } | |
public CharaCommand(string name) | |
{ | |
Name = name; | |
Face = Option<string>.None; | |
} | |
public CharaCommand(string name, Option<string> face) | |
{ | |
Name = name; | |
Face = face; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.CharaCommandStarted(); | |
} | |
public override float Duration => 1f; | |
} | |
/// <summary> | |
/// 立ち絵を入れ替えるコマンド | |
/// </summary> | |
public class FaceCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public string Face { get; } | |
public FaceCommand(string name, string face) | |
{ | |
Name = name; | |
Face = face; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.FaceCommandStarted(); | |
} | |
public override float Duration => 0f; | |
} | |
/// <summary> | |
/// すべてのコマンドの実行が終わるとこれが入る | |
/// </summary> | |
public class StopCommand : GenericCommand | |
{ | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.StopCommandStarted(this); | |
} | |
public override float Duration => float.PositiveInfinity; | |
} | |
public class DefineCharaCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public string ObjName { get; } | |
public string FullName { get; } | |
public DefineCharaCommand(string name, string objName, string fullName) | |
{ | |
Name = name; | |
ObjName = objName; | |
FullName = fullName; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.DefineCharaCommandStarted(); | |
} | |
public override float Duration => 0f; | |
} | |
public class DefineObjectCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public string ObjName { get; } | |
public string FullName { get; } | |
public DefineObjectCommand(string name, string objName, string fullName) | |
{ | |
Name = name; | |
ObjName = objName; | |
FullName = fullName; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.DefineObjectCommandStarted(); | |
} | |
public override float Duration => 0f; | |
} | |
/// <summary> | |
/// 音声リソースを読み込んで名前をつける | |
/// </summary> | |
public class LoadSoundCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public string ObjPath { get; } | |
public LoadSoundCommand(string name, string objPath) | |
{ | |
Name = name; | |
ObjPath = objPath; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.LoadSoundCommandStarted(this); | |
} | |
public override float Duration => 0f; | |
} | |
public class PlaySoundCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public PlaySoundCommand(string name) | |
{ | |
Name = name; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.PlaySoundCommandStarted(this); | |
} | |
public override float Duration => 0f; | |
} | |
public class ObjChangeCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public string Face { get; } | |
public readonly float duration; | |
public ObjChangeCommand(string name, string face, float duration) | |
{ | |
Name = name; | |
Face = face; | |
this.duration = duration; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.ObjChangeCommandStarted(); | |
} | |
public override float Duration => duration; | |
} | |
public class ObjAlphaCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public float Alpha { get; } | |
public readonly float duration; | |
public ObjAlphaCommand(string name, float alpha, float duration) | |
{ | |
Name = name; | |
Alpha = alpha; | |
this.duration = duration; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.ObjAlphaCommandStarted(); | |
} | |
public override float Duration => duration; | |
} | |
public class NameLabelCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public NameLabelCommand(string name) | |
{ | |
Name = name; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.NameLabelCommandStarted(); | |
} | |
public override float Duration => 0f; | |
} | |
/// <summary> | |
/// パースできなかったコマンド `@xxx` という形式 | |
/// </summary> | |
public class UnknownCommand : GenericCommand | |
{ | |
public string Name { get; } | |
public UnknownCommand(string name) | |
{ | |
Name = name; | |
} | |
override public void handleCallBack(TextController controller) | |
{ | |
controller.UnknownCommandStarted(this); | |
} | |
public override float Duration => 0f; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NUnit.Framework; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using UnityEngine; | |
using static ParserCombinator; | |
public class NewTestScript | |
{ | |
public ScenarioParser commandParser = new ScenarioParser(0.1f, msg => Debug.Log(msg)); | |
[SetUp] | |
public void Init() | |
{ | |
} | |
[Test] | |
public void TestToken() | |
{ | |
var hoge = Token("hoge"); | |
var parsed = hoge("hoge", 0); | |
parsed.Each((value, position) => | |
{ | |
Assert.That(position, Is.EqualTo(4)); | |
Assert.That(value, Is.EqualTo("hoge")); | |
} | |
); | |
} | |
[Test] | |
public void TestMany() | |
{ | |
var hoges = Many(Token("hoge")); | |
var parsed = hoges("hogehogefoo", 0); | |
parsed.Each((values, position) => | |
{ | |
Assert.AreEqual(list("hoge", "hoge"), values); | |
Assert.AreEqual(8, position); | |
} | |
); | |
} | |
[Test] | |
public void TestChoice() | |
{ | |
var fooOrBar = Choice(Token("foo"), Token("bar")); | |
fooOrBar("foo", 0).Each((value, position) => | |
Assert.AreEqual("foo", value) | |
); | |
fooOrBar("bar", 0).Each((value, position) => | |
Assert.AreEqual("bar", value) | |
); | |
Assert.False(fooOrBar("baz", 0).IsSuccess()); | |
} | |
[Test] | |
public void TestSeq() | |
{ | |
var foobar = Seq(Token("foo"), Token("bar")); | |
foobar("foobar", 0).Each((value, position) => | |
Assert.AreEqual("foobar", value) | |
); | |
Assert.True(foobar("fooba", 0).IsFailed()); | |
Assert.True(foobar("fooba", 0).IsFailed()); | |
} | |
[Test] | |
public void TestSeq2() | |
{ | |
var a = Token("a"); | |
var b = Chars("b"); | |
var ab = Seq(a, b); | |
Assert.That(a("ab", 0).IsSuccess()); | |
Assert.That(a(" a", 1).IsSuccess()); | |
Assert.That(b("b", 0).IsSuccess()); | |
Assert.That(b(" b", 1).IsSuccess()); | |
Assert.That(ab("ab", 0).IsSuccess()); | |
} | |
[Test] | |
public void TestMix() | |
{ | |
var foobarbaz = Seq(Token("foo"), Choice(Token("bar"), Token("baz"))); | |
foobarbaz("foobar", 0).Each((value, position) => | |
Assert.AreEqual("foobar", value) | |
); | |
foobarbaz("foobaz", 0).Each((value, position) => | |
Assert.AreEqual("foobaz", value) | |
); | |
} | |
[Test] | |
public void TestOpt() | |
{ | |
Parser<List<string>> foobar = Tupled(Token("foo"), Opt(Token("bar"))).Map(tpl => | |
new List<string> { tpl.Item1, tpl.Item2.GetOr("") } | |
); | |
foobar("foobar", 0).Each((values, position) => | |
{ | |
Assert.AreEqual(6, position); | |
Assert.AreEqual(list("foo", "bar"), values); | |
} | |
); | |
foobar("foo", 0).Each((values, position) => | |
{ | |
Assert.AreEqual(3, position); | |
Assert.AreEqual(list("foo", ""), values); | |
} | |
); | |
foobar("foohoge", 0).Each((values, position) => | |
{ | |
Assert.AreEqual(3, position); | |
Assert.AreEqual(list("foo", ""), values); | |
} | |
); | |
} | |
[Test] | |
public void TestOpt2() | |
{ | |
var foobar = Tupled(Token("foo"), Opt(Token("bar"))); | |
var r = foobar("foobar", 0); | |
Assert.True(r.IsSuccess()); | |
r.Each((tpl, pos) => | |
{ | |
Assert.AreEqual(tpl.Item1, "foo"); | |
Assert.AreEqual(tpl.Item2, Option<string>.Some("bar")); | |
} | |
); | |
var r2 = foobar("foo", 0); | |
Assert.True(r2.IsSuccess()); | |
r2.Each((tpl, pos) => | |
{ | |
Assert.AreEqual(tpl.Item1, "foo"); | |
Assert.False(tpl.Item2.HasValue); | |
Assert.AreEqual(tpl.Item2, Option<string>.None); | |
} | |
); | |
} | |
[Test] | |
public void TestRegexp() | |
{ | |
Parser<string> barbaz = Regexp("[1-9][0-9]*"); | |
barbaz("1", 0).Each((value, position) => | |
Assert.AreEqual("1", value) | |
); | |
barbaz("1234.234", 0).Each((value, position) => | |
Assert.AreEqual("1234", value) | |
); | |
Assert.True(barbaz("012", 0).IsFailed()); | |
} | |
[Test] | |
public void TestRegexpOptions() | |
{ | |
Parser<string> barbaz = Regexp("ba[rz]", System.Text.RegularExpressions.RegexOptions.IgnoreCase); | |
IParseResult<string> result; | |
result = barbaz("Bar", 0); | |
Assert.True(result.IsSuccess()); | |
result = barbaz("bAz", 0); | |
Assert.True(result.IsSuccess()); | |
result = barbaz("baG", 0); | |
Assert.True(result.IsFailed()); | |
} | |
[Test] | |
public void TestMap() | |
{ | |
Parser<string> hoge = Map(Token("hoge"), str => str.ToUpper()); | |
hoge("hoge wawawa", 0).Each((value, position) => | |
Assert.AreEqual("HOGE", value) | |
); | |
} | |
[Test] | |
public void TestChars() | |
{ | |
Parser<string> abcd = Chars("abcd"); | |
IParseResult<string> result; | |
result = abcd("a", 0); | |
Assert.True(result.IsSuccess()); | |
result = abcd("b", 0); | |
Assert.True(result.IsSuccess()); | |
result = abcd("z", 0); | |
Assert.True(result.IsFailed()); | |
} | |
[Test] | |
public void TestAny() | |
{ | |
Parser<string> anychar = Any; | |
IParseResult<string> result; | |
result = anychar("hoge", 0); | |
result.Each((value, position) => | |
{ | |
Assert.AreEqual("h", value); | |
Assert.AreEqual(1, position); | |
} | |
); | |
result = anychar("", 0); | |
Assert.True(result.IsFailed()); | |
} | |
[Test] | |
public void TestScenarioEmpty() | |
{ | |
var scenario = commandParser.Scenario(); | |
IParseResult<List<ICommand>> result; | |
result = scenario("", 0); | |
result.Each((values, position) => | |
{ | |
Assert.That(values, Is.Empty); | |
Assert.That(position, Is.Zero); | |
} | |
); | |
} | |
[Test] | |
public void TestScenarioPause() | |
{ | |
var scenario = commandParser.Scenario(); | |
IParseResult<List<ICommand>> result; | |
result = scenario("@pause(12.3)", 0); | |
result.Each((values, position) => | |
{ | |
Assert.AreEqual(1, values.Count); | |
Assert.That(values[0], Is.TypeOf(typeof(PauseCommand))); | |
var pauseCommand = values[0] as PauseCommand; | |
Assert.That(pauseCommand.seconds, Is.EqualTo(12.3f)); | |
Assert.That(position, Is.EqualTo(12)); | |
} | |
); | |
} | |
[Test] | |
public void TestScenario1() | |
{ | |
DoScenarioAction((values, position) => | |
{ | |
Assert.That(values.Count, Is.EqualTo(3)); | |
}); | |
} | |
[Test] | |
public void TestScenario2() | |
{ | |
DoScenarioAction((values, position) => | |
Assert.That(values[0], Is.TypeOf(typeof(DisplayCommand))) | |
); | |
} | |
[Test] | |
public void TestScenario3() | |
{ | |
DoScenarioAction((values, position) => | |
{ | |
var command = values[0] as DisplayCommand; | |
Assert.That(command.Text, Is.EqualTo("こんばんわ、佐々木千枝です")); | |
}); | |
} | |
[Test] | |
public void TestScenario4() | |
{ | |
DoScenarioAction(((values, position) => | |
{ | |
var command = values[1]; | |
Assert.That(command, Is.TypeOf(typeof(PauseCommand))); | |
var pauseCommand = command as PauseCommand; | |
Assert.That(pauseCommand.seconds, Is.EqualTo(12.34f)); | |
} | |
)); | |
} | |
[Test] | |
public void TestScenario5() | |
{ | |
DoScenarioAction((values, position) => | |
Assert.That(values[2], Is.TypeOf(typeof(DisplayCommand))) | |
); | |
} | |
[Test] | |
public void TestScenario6() | |
{ | |
DoScenarioAction((values, position) => | |
{ | |
var command = values[2] as DisplayCommand; | |
Assert.That(command.Text, Is.EqualTo("…聞こえてますか?")); | |
} | |
); | |
} | |
[Test] | |
public void TestScenario7() | |
{ | |
DoScenarioAction((values, position) => | |
Assert.That(position, Is.EqualTo(35)) | |
); | |
} | |
private void DoScenarioAction(Action<List<ICommand>, int> action) | |
{ | |
var scenario = commandParser.Scenario(); | |
scenario("こんばんわ、佐々木千枝です@pause(12.34)…聞こえてますか?", 0).Each(action); | |
} | |
[Test] | |
public void TestNumber() | |
{ | |
Number("123.44", 0).Each((value, position) => | |
{ | |
Assert.That(value, Is.EqualTo(123.44f)); | |
} | |
); | |
} | |
[Test] | |
public void TestSepBy() | |
{ | |
SepBy(Token("a"), Token(","))("a,a,a,aa", 0).Each((values, position) => | |
{ | |
Assert.That(values, Is.EqualTo(list("a", "a", "a", "a"))); | |
} | |
); | |
} | |
[Test] | |
public void TestWithout() | |
{ | |
// Many(Choice(Without("\""), Seq(Token("\\"), Chars("nr\"")))), | |
Parser<string> hoge; | |
hoge = Without("\""); | |
Assert.True(hoge(@"""", 0).IsFailed()); | |
Assert.True(hoge("a", 0).IsSuccess()); | |
hoge = Token("\\"); | |
Assert.True(hoge("\\", 0).IsSuccess()); | |
hoge = Chars("nr\""); | |
Assert.True(hoge("n", 0).IsSuccess()); | |
Assert.True(hoge("r", 0).IsSuccess()); | |
Assert.True(hoge("\"", 0).IsSuccess()); | |
Assert.True(hoge("x", 0).IsFailed()); | |
hoge = Choice(Seq(Without("\"")), Seq(Token("\""), Chars("nr\""))).Map(x => string.Join("", x)); | |
Assert.True(hoge("a", 0).IsSuccess()); | |
Assert.True(hoge("\\n", 0).IsSuccess()); // \n | |
Assert.True(hoge("\\\"", 0).IsSuccess()); // \" | |
} | |
[Test] | |
public void TestQuated() | |
{ | |
var quo = QuotedStr; | |
var s = @" ""this is a pen"" "; | |
var r = quo(s, 1); | |
Assert.True(r.IsSuccess()); | |
r.Each((value, position) => | |
{ | |
Assert.That(value, Is.EqualTo("this is a pen")); | |
Assert.That(position, Is.EqualTo(16)); | |
} | |
); | |
} | |
[Test] | |
public void TestTupled() | |
{ | |
var hoge = Tupled(Token("hoge:"), Number); | |
var s = "hoge:12.34"; | |
var r = hoge(s, 0); | |
Assert.True(r.IsSuccess()); | |
r.Each((val, pos) => | |
{ | |
Assert.That(pos, Is.EqualTo(10)); | |
Assert.That(val.Item1, Is.EqualTo("hoge:")); | |
Assert.That(val.Item2, Is.EqualTo(12.34f)); | |
}); | |
} | |
[Test] | |
public void TestOr() | |
{ | |
var hoge = Token("hoge").Or(Token("fuga")); | |
Assert.True(hoge("hoge", 0).IsSuccess()); | |
Assert.True(hoge("fuga", 0).IsSuccess()); | |
Assert.True(hoge("ffff", 0).IsFailed()); | |
} | |
[Test] | |
public void TestPrevNext() | |
{ | |
Token("hoge").Next(Token("fuga"))("hogefuga", 0).Each((value, pos) => | |
{ | |
Assert.That(value, Is.EqualTo("fuga")); | |
Assert.That(pos, Is.EqualTo(8)); | |
}); | |
Token("hoge").Prev(Token("fuga"))("hogefuga", 0).Each((val, pos) => | |
{ | |
Assert.That(val, Is.EqualTo("hoge")); | |
Assert.That(pos, Is.EqualTo(8)); | |
}); | |
} | |
[Test] | |
public void TestComment() | |
{ | |
Regexp("#.*")("#comment...\naaa", 0).Each((val, pos) => | |
{ | |
Assert.That(val, Is.EqualTo("#comment...")); | |
Assert.AreEqual(pos, 11); | |
} | |
); | |
} | |
[Test] | |
public void TestEof() | |
{ | |
Assert.IsTrue( | |
Eof("", 0).IsSuccess() | |
); | |
Assert.IsTrue( | |
Token("hoge").Prev(Eof)("hoge", 0).IsSuccess() | |
); | |
} | |
// リストを簡単に作るヘルパー | |
private List<T> list<T>(params T[] items) | |
{ | |
return items.ToList<T>(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text.RegularExpressions; | |
using static ParserCombinator; | |
public static class ParserCombinator | |
{ | |
// TODO: 全体的にstaticメソッドにしたい | |
#region パーサー | |
/// <summary> | |
/// 指定した文字列を受け入れるパーサー | |
/// </summary> | |
/// <param name="str"></param> | |
/// <returns></returns> | |
public static Parser<string> Token(string str) | |
{ | |
var len = str.Length; | |
return (string source, int position) => | |
{ | |
if (position + len > source.Length) | |
{ | |
return fail<string>(position); | |
} | |
if (source.Substring(position, len) == str) | |
{ | |
return success(str, position + len); | |
} | |
else | |
{ | |
return fail<string>(position); | |
} | |
}; | |
} | |
/// <summary> | |
/// 正規表現を受け入れるパーサー | |
/// </summary> | |
/// <param name="pattern"></param> | |
/// <param name="options"></param> | |
/// <returns></returns> | |
public static Parser<string> Regexp(string pattern, RegexOptions options = RegexOptions.None) | |
{ | |
string pat = pattern.StartsWith("^") ? pattern : "^" + pattern; | |
Regex regex = new Regex(pat, options); | |
return (string source, int position) => | |
{ | |
Match m = regex.Match(source.Substring(position)); | |
if (m.Success) | |
{ | |
return success(m.Value, position + m.Length); | |
} | |
else | |
{ | |
return fail<string>(position); | |
} | |
}; | |
} | |
/// <summary> | |
/// 数字を受け入れるパーサー | |
/// </summary> | |
/// <returns></returns> | |
public static Parser<int> Digit => Map(Regexp("[1-9][0-9]*"), x => int.Parse(x)); | |
/// <summary> | |
/// 実数を受け入れるパーサー | |
/// </summary> | |
/// <returns></returns> | |
public static Parser<float> Number => | |
Map(Regexp("-?[0-9]+\\.[0-9]+|[1-9][0-9]*\\."), x => float.Parse(x)); | |
/// <summary> | |
/// 引用符 " ではさまれた文字列 | |
/// </summary> | |
/// <returns></returns> | |
public static Parser<string> QuotedStr => Between( | |
Token("\""), | |
Many(Choice(Without("\""), Seq(Token("\""), Chars("nr\"")))), | |
Token("\"") | |
).Map(x => string.Join("", x)); | |
/// <summary> | |
/// 0個以上の空白文字を受け入れるパーサー | |
/// </summary> | |
/// <returns></returns> | |
public static Parser<string> Spaces => Regexp("\\s+"); | |
/// <summary> | |
/// 列挙した文字を受け入れるパーサー | |
/// </summary> | |
/// <param name="chars"></param> | |
/// <returns></returns> | |
public static Parser<string> Chars(string chars) | |
{ | |
return (string source, int position) => | |
{ | |
string letter = source[position].ToString(); | |
if (chars.Contains(letter)) | |
{ | |
return success(letter, position + 1); | |
} | |
else | |
{ | |
return fail<string>(position); | |
} | |
}; | |
} | |
public static Parser<string> Without(string chars) | |
{ | |
return Regexp($"[^{chars}]"); | |
} | |
// なんでも1文字受け入れるパーサー | |
public static Parser<string> Any => (string source, int position) => | |
{ | |
if (source.Length > position) | |
{ | |
return success(source.Substring(position, 1), position + 1); | |
} | |
else | |
{ | |
return fail<string>(position); | |
} | |
}; | |
public static Parser<Option<string>> Eof => (string source, int position) => | |
{ | |
if (source.Length == position) | |
{ | |
return success(Option<string>.None, position); | |
} | |
else | |
{ | |
return fail<Option<string>>($"unexpected end of file. source remaining: {source.Substring(position)}", position); | |
} | |
}; | |
/// <summary> | |
/// 単純に引数の値を返すだけのパーザー (Monad.returnに使用する) | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="value"></param> | |
/// <returns></returns> | |
public static Parser<T> Value<T>(T value) | |
{ | |
return (string source, int position) => | |
{ | |
return success(value, position); | |
}; | |
} | |
#endregion | |
#region コンビネータ | |
/// <summary> | |
/// 繰り返しのコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="parser"></param> | |
/// <returns></returns> | |
public static Parser<List<T>> Many<T>(Parser<T> parser) | |
{ | |
return (string source, int position) => | |
{ | |
var result = new List<T> { }; | |
var newPosition = position; | |
while (true) | |
{ | |
var parsed = parser(source, newPosition); | |
if (parsed.IsFailed()) | |
{ | |
break; | |
} | |
parsed.Each((value, pos) => | |
{ | |
result.Add(value); | |
newPosition = pos; | |
} | |
); | |
} | |
return success(result, newPosition); | |
}; | |
} | |
/// <summary> | |
/// セパレータではさまれたパーサーの連続 | |
/// </summary> | |
/// <typeparam name="T">パーサーの型</typeparam> | |
/// <typeparam name="U">セパレータの型</typeparam> | |
/// <param name="parser">パーサー</param> | |
/// <param name="seperator">セパレータ</param> | |
/// <returns></returns> | |
public static Parser<List<T>> SepBy<T, U>(Parser<T> parser, Parser<U> seperator) | |
{ | |
return Cons(parser, Many(DropFirst(seperator, parser))); | |
} | |
/// <summary> | |
/// パーサーを連結する | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="parser"></param> | |
/// <param name="parsers"></param> | |
/// <returns></returns> | |
public static Parser<List<T>> Cons<T>(Parser<T> parser, Parser<List<T>> parsers) | |
{ | |
return (string source, int position) => | |
{ | |
return parser(source, position).Match( | |
onFailed: msg1 => fail<List<T>>(msg1, position), | |
onSuccess: (v1, pos1) => parsers(source, pos1).Match( | |
onFailed: msg2 => fail<List<T>>(msg2, position), | |
onSuccess: (v2, pos2) => success<List<T>>(unshift(v1, v2), pos2) | |
) | |
); | |
}; | |
} | |
/// <summary> | |
/// 1個以上の繰り返しのコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="parser"></param> | |
/// <returns></returns> | |
public static Parser<List<T>> Many1<T>(Parser<T> parser) | |
{ | |
return Cons(parser, Many(parser)); | |
} | |
// 選択のコンビネータ | |
public static Parser<T> Choice<T>(params Parser<T>[] parsers) | |
{ | |
return (string source, int position) => | |
{ | |
foreach (Parser<T> parser in parsers) | |
{ | |
var parsed = parser(source, position); | |
if (parsed.IsSuccess()) | |
{ | |
return parsed; | |
} | |
} | |
return fail<T>(position); | |
}; | |
} | |
// 連続のコンビネータ | |
public static Parser<List<T>> Seq<T>(params Parser<T>[] parsers) | |
{ | |
return (string source, int position) => | |
{ | |
var values = new List<T>() { }; | |
int newPosition = position; | |
foreach (Parser<T> parser in parsers) | |
{ | |
var parsed = parser(source, newPosition); | |
if (parsed.IsFailed()) return fail<List<T>>(position); | |
parsed.Each((val, pos) => | |
{ | |
values.Add(val); | |
newPosition = pos; | |
} | |
); | |
} | |
return success(values, newPosition); | |
}; | |
} | |
/// <summary> | |
/// Parser<string>型同士の連接の場合、結果のstringを連結する | |
/// </summary> | |
/// <param name="parsers"></param> | |
/// <returns></returns> | |
public static Parser<string> Seq(params Parser<string>[] parsers) | |
{ | |
return Seq<string>(parsers).Map(str => string.Join("", str)); | |
} | |
/// <summary> | |
/// 2つのパーサーが成功したとき2番めの結果を返すコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="U"></typeparam> | |
/// <param name="first"></param> | |
/// <param name="second"></param> | |
/// <returns></returns> | |
public static Parser<U> DropFirst<T, U>(Parser<T> first, Parser<U> second) | |
{ | |
return (string source, int position) => | |
{ | |
return first(source, position).Bind((val1, pos1) => | |
second(source, pos1).Bind((val2, pos2) => | |
success(val2, pos2) | |
) | |
); | |
}; | |
} | |
/// <summary> | |
/// 2つのパーサーが成功したとき1番目の結果を返すコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="U"></typeparam> | |
/// <param name="first"></param> | |
/// <param name="second"></param> | |
/// <returns></returns> | |
public static Parser<T> DropSecond<T, U>(Parser<T> first, Parser<U> second) | |
{ | |
return (string source, int position) => | |
{ | |
return first(source, position).Bind((val1, pos1) => | |
second(source, pos1).Bind((val2, pos2) => | |
success(val1, pos2) | |
) | |
); | |
}; | |
} | |
/// <summary> | |
/// 3つのパーサーが成功したとき真ん中の結果を返すコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="U"></typeparam> | |
/// <typeparam name="V"></typeparam> | |
/// <param name="a"></param> | |
/// <param name="b"></param> | |
/// <param name="c"></param> | |
/// <returns></returns> | |
public static Parser<U> Between<T, U, V>(Parser<T> a, Parser<U> b, Parser<V> c) | |
{ | |
return DropSecond(DropFirst(a, b), c); | |
} | |
// 省略可能のコンビネータ | |
public static Parser<Option<T>> Opt<T>(Parser<T> parser) | |
{ | |
return (string source, int position) => | |
{ | |
var parsed = parser(source, position); | |
return parsed.Match( | |
onSuccess: (val, pos) => success(Option<T>.Some(val), pos), | |
onFailed: _ => success(Option<T>.None, position) | |
); | |
}; | |
} | |
/// <summary> | |
/// 2つの違う型のパーサーをタプルにするコンビネータ | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <typeparam name="U"></typeparam> | |
/// <param name="parserT"></param> | |
/// <param name="parserU"></param> | |
/// <returns></returns> | |
public static Parser<Tuple<T, U>> Tupled<T, U>(Parser<T> parserT, Parser<U> parserU) | |
{ | |
return (string source, int position) => | |
{ | |
Tuple<T, U> valueTU = null; | |
int nextPosition = 0; | |
var ret1 = parserT(source, position); | |
if (ret1.IsFailed()) return fail<Tuple<T, U>>(position); | |
ret1.Each((val, pos) => | |
{ | |
parserU(source, pos).Each((val2, pos2) => | |
{ | |
valueTU = Tuple.Create(val, val2); | |
nextPosition = pos2; | |
}); | |
}); | |
if (valueTU != null) | |
return new ParseSuccess<Tuple<T, U>>(valueTU, nextPosition); | |
else | |
return fail<Tuple<T, U>>(position); | |
}; | |
} | |
// 結果を加工するコンビネータ | |
public static Parser<R> Map<T, R>(Parser<T> parser, Func<T, R> fun) | |
{ | |
return (string source, int position) => | |
{ | |
return parser(source, position).Map(t => fun(t)); | |
}; | |
} | |
#endregion | |
#region 結果を返すプライペートメソッド | |
private static IParseResult<T> success<T>(T value, int position) | |
{ | |
return new ParseSuccess<T>(value, position); | |
} | |
private static IParseResult<T> fail<T>(int position) | |
{ | |
return new ParseFailed<T>(position); | |
} | |
private static IParseResult<T> fail<T>(string message, int position) | |
{ | |
return new ParseFailed<T>(message, position); | |
} | |
// リストを簡単に作るヘルパー | |
private static List<T> list<T>(params T[] items) | |
{ | |
return items.ToList<T>(); | |
} | |
private static List<T> unshift<T>(T a, List<T> b) | |
{ | |
var lst = list(a); | |
lst.AddRange(b); | |
return lst; | |
} | |
#endregion | |
} | |
#region その他付随するクラス | |
/// <summary> | |
/// コンビネーター演算子風の拡張メソッドを定義する場所 | |
/// </summary> | |
public static class ParserExtention | |
{ | |
public static Parser<T> Or<T>(this Parser<T> parser1, Parser<T> parser2) | |
{ | |
return Choice(parser1, parser2); | |
} | |
public static Parser<T> Prev<T, U>(this Parser<T> prev, Parser<U> next) | |
{ | |
return DropSecond(prev, next); | |
} | |
public static Parser<U> Next<T, U>(this Parser<T> prev, Parser<U> next) | |
{ | |
return DropFirst(prev, next); | |
} | |
public static Parser<Tuple<T,U>> Tpl<T,U>(this Parser<T> parser1, Parser<U> parser2) | |
{ | |
return Tupled(parser1, parser2); | |
} | |
public static Parser<U> Map<T, U>(this Parser<T> parser, Func<T,U> func) | |
{ | |
return ParserCombinator.Map(parser, func); | |
} | |
} | |
// パーサーを表す関数型: (string, int) => IParseResult<T> | |
public delegate IParseResult<T> Parser<T>(string source, int position); | |
/// <summary> | |
/// パース結果を表す直和型 | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
public interface IParseResult<T> | |
{ | |
bool IsSuccess(); | |
bool IsFailed(); | |
void Each(Action<T, int> action); | |
IParseResult<U> Map<U>(Func<T, U> func); | |
U Match<U>(Func<T, int, U> onSuccess, Func<string, U> onFailed); | |
IParseResult<U> Bind<U>(Func<T, int, IParseResult<U>> func); | |
} | |
public class ParseSuccess<T> : IParseResult<T> | |
{ | |
public T Value { private set; get; } | |
public int Position { private set; get; } | |
public ParseSuccess(T value, int position) | |
{ | |
Value = value; | |
Position = position; | |
} | |
public bool IsSuccess() { return true; } | |
public bool IsFailed() { return false; } | |
public void Each(Action<T, int> action) | |
{ | |
action(Value, Position); | |
} | |
public IParseResult<U> Map<U>(Func<T, U> func) | |
{ | |
return new ParseSuccess<U>(func(Value), Position); | |
} | |
public U Match<U>(Func<T, int, U> onSuccess, Func<string, U> onFailed) | |
{ | |
return onSuccess(Value, Position); | |
} | |
public IParseResult<U> Bind<U>(Func<T, int, IParseResult<U>> func) | |
{ | |
return func(Value, Position); | |
} | |
} | |
public class ParseFailed<T> : IParseResult<T> | |
{ | |
public string Message { private set; get; } | |
public int Position { private set; get; } | |
public ParseFailed(string message, int position) | |
{ | |
Message = message; | |
Position = position; | |
} | |
public ParseFailed(int position) | |
{ | |
Message = ""; | |
Position = position; | |
} | |
public bool IsSuccess() { return false; } | |
public bool IsFailed() { return true; } | |
public void Each(Action<T, int> action) | |
{ | |
} | |
public IParseResult<U> Map<U>(Func<T, U> func) | |
{ | |
return new ParseFailed<U>(Message, Position); | |
} | |
public U Match<U>(Func<T, int, U> onSuccess, Func<string, U> onFailed) | |
{ | |
return onFailed($"position = {Position}, message = {Message}"); | |
} | |
public IParseResult<U> Bind<U>(Func<T, int, IParseResult<U>> func) | |
{ | |
return new ParseFailed<U>(Message, Position); | |
} | |
} | |
#endregion |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using UnityEngine; | |
using static ParserCombinator; | |
public class ScenarioParser | |
{ | |
// TODO: イベント処理をコマンド側に移す | |
// TODO: エラー表示を何行目何文字目にする | |
// TODO: コルーチンにする | |
// TODO: 実装したいコマンド | |
// * マクロを定義して呼び出す機能 | |
private float displayInterval; | |
private Action<object> logging; | |
/// <summary> | |
/// コンストラクタ | |
/// </summary> | |
/// <param name="displayInterval">セリフ1文字の表示にかかる時間</param> | |
/// <param name="logging">ログ出力のハンドラ</param> | |
public ScenarioParser(float displayInterval, Action<object> logging) | |
{ | |
this.displayInterval = displayInterval; | |
this.logging = logging; | |
} | |
// シナリオのパース処理本体 | |
public List<ICommand> Parse(string text) | |
{ | |
var scenario = Scenario(); | |
return scenario(text, 0).Match( | |
onSuccess: (values, pos) => values, | |
onFailed: message => | |
{ | |
logging(message); | |
return new List<ICommand> { new StopCommand() }; | |
} | |
); | |
} | |
/// <summary> | |
/// シナリオ全体のパース | |
/// </summary> | |
/// <returns></returns> | |
public Parser<List<ICommand>> Scenario() => Many(Opt(Blanks).Next(Commands)).Prev(Opt(Spaces)).Prev(Eof); | |
/// <summary> | |
/// コマンド | |
/// TODO: これを自動登録できないか? | |
/// -> 汎用的なコマンド解釈と引数チェックに集約する | |
/// </summary> | |
/// <returns></returns> | |
public Parser<ICommand> Commands => Choice( | |
DefineChar, | |
DefineObj, | |
Display, | |
Pause, | |
Page, | |
Chara, | |
Face, | |
ObjAlpha, | |
ObjImage, | |
CharaAll, | |
Label, | |
LoadSound, | |
PlaySound, | |
Stop, | |
Unknown | |
); | |
/// <summary> | |
/// キャラを定義 | |
/// </summary> | |
public Parser<ICommand> DefineChar => Token("@define_char").Next(Args3(QuotedStr, QuotedStr, QuotedStr)) | |
.Map<Tuple<string, string, string>, ICommand>( | |
tpl => new DefineCharaCommand(tpl.Item1, tpl.Item2, tpl.Item3) | |
); | |
/// <summary> | |
/// オブジェクトを定義 | |
/// </summary> | |
public Parser<ICommand> DefineObj => Token("@define_obj").Next(Args3(QuotedStr, QuotedStr, QuotedStr)) | |
.Map<Tuple<string, string, string>, ICommand>( | |
tpl => new DefineObjectCommand(tpl.Item1, tpl.Item2, tpl.Item3) | |
); | |
/// <summary> | |
/// オーディオクリップをリソースから読み込み | |
/// </summary> | |
public Parser<ICommand> LoadSound => Token("@load_sound").Next(Args2(QuotedStr, QuotedStr)) | |
.Map<Tuple<string, string>, ICommand>( | |
tpl => new LoadSoundCommand(tpl.Item1, tpl.Item2) | |
); | |
/// <summary> | |
/// オーディオクリップを再生 | |
/// </summary> | |
public Parser<ICommand> PlaySound => Token("@sound").Next(Parentheses(QuotedStr)) | |
.Map<string, ICommand>(str => new PlaySoundCommand(str)); | |
/// <summary> | |
/// カッコではさまれたパーサー | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
/// <param name="parser"></param> | |
/// <returns></returns> | |
public Parser<T> Parentheses<T>(Parser<T> parser) => Between( | |
Token("(").Prev(Opt(Spaces)), | |
parser, | |
Opt(Spaces).Next(Token(")")) | |
); | |
/// <summary> | |
/// コンマ(前後に空白を許す) | |
/// </summary> | |
/// <returns></returns> | |
public Parser<string> Comma => Between(Opt(Spaces), Token(","), Opt(Spaces)); | |
/// <summary> | |
/// 2引数のリスト | |
/// </summary> | |
/// <returns></returns> | |
public Parser<Tuple<A, B>> Args2<A, B>(Parser<A> p1, Parser<B> p2) => | |
Parentheses(Tupled(p1.Prev(Comma), p2)); | |
/// <summary> | |
/// 3引数のリスト | |
/// </summary> | |
/// <returns></returns> | |
public Parser<Tuple<A, B, C>> Args3<A, B, C>(Parser<A> p1, Parser<B> p2, Parser<C> p3) => | |
Parentheses( | |
p1.Prev(Comma).Tpl(p2.Prev(Comma)).Tpl(p3) | |
).Map(tpltpl => | |
Tuple.Create(tpltpl.Item1.Item1, tpltpl.Item1.Item2, tpltpl.Item2) | |
); | |
/// <summary> | |
/// 引数リスト 文字列, オプション(文字列) | |
/// </summary> | |
public Parser<Tuple<string, Option<string>>> ArgStrOptStr => | |
Parentheses( | |
Tupled(QuotedStr, Opt(Comma.Next(QuotedStr))) | |
); | |
/// <summary> | |
/// DisplayCommand -- "@" と "#" 以外の文字の並び | |
/// </summary> | |
/// <returns></returns> | |
public Parser<ICommand> Display | |
{ | |
get | |
{ | |
Parser<string> notMeta = Regexp("[^@#]+"); | |
return Map<string, ICommand>(notMeta, str => new DisplayCommand(str, displayInterval)); | |
} | |
} | |
/// <summary> | |
/// コメントは'#'で始まり行末で終わる | |
/// </summary> | |
/// <returns></returns> | |
public Parser<string> Comment => Map(Regexp("^#.*"), x => ""); | |
/// <summary> | |
/// コメントと空白文字を読み飛ばす | |
/// </summary> | |
/// <returns></returns> | |
public Parser<string> Blanks => Many(Spaces.Or(Comment)).Map(_ => ""); | |
/// <summary> | |
/// PauseCommand -- "@pause(1.5)" / "@.." | |
/// </summary> | |
/// <returns></returns> | |
public Parser<ICommand> Pause => Map<float, ICommand>( | |
Choice( | |
Token("@pause").Next(Parentheses(Number)), | |
Token("@.").Map(_ => 2.5f), // 2.5秒固定で待つ | |
Regexp("@\\,+").Map(str => (str.Length - 1) * 0.5f) // コンマの数 * 0.5秒待つ | |
), | |
s => new PauseCommand(s) | |
); | |
/// <summary> | |
/// 改ページ | |
/// </summary> | |
public Parser<ICommand> Page => Map<string, ICommand>( | |
Token("@page"), | |
s => new PageCommand() | |
); | |
/// <summary> | |
/// キャラ切り替え | |
/// </summary> | |
public Parser<ICommand> Chara => Map<Tuple<string, Option<string>>, ICommand>( | |
Token("@chara").Next(ArgStrOptStr), | |
tpl => new CharaCommand(tpl.Item1, tpl.Item2) | |
); | |
/// <summary> | |
/// 画面オブジェクトの透明度 | |
/// </summary> | |
public Parser<ICommand> ObjAlpha => Map<Tuple<string, float, float>, ICommand>( | |
Token("@obj_alpha").Next(Args3(QuotedStr, Number, Number)), | |
tpl => new ObjAlphaCommand(tpl.Item1, tpl.Item2, tpl.Item3) | |
); | |
/// <summary> | |
/// 画面オブジェクトの画像切替 | |
/// </summary> | |
public Parser<ICommand> ObjImage => Map<Tuple<string, string, float>, ICommand>( | |
Token("@obj_change").Next(Args3(QuotedStr, QuotedStr, Number)), | |
tpl => new ObjChangeCommand(tpl.Item1, tpl.Item2, tpl.Item3) | |
); | |
/// <summary> | |
/// 全員の明るさ、透明度を変更 | |
/// 引数は "ON" / "OFF" / "HIDE" | |
/// 省略可能な引数としてラベルの文字を変えることが可能 | |
/// </summary> | |
public Parser<ICommand> CharaAll => Token("@all") | |
.Next(Parentheses( | |
Tupled( | |
Choice(Token("ON"), Token("OFF"), Token("HIDE")), | |
Opt(Comma.Next(QuotedStr)) | |
) | |
)) | |
.Map<Tuple<string, Option<string>>, ICommand>( | |
tpl => new CharaAllCommand(colorParams[tpl.Item1], tpl.Item2.GetOr("")) | |
); | |
public Parser<ICommand> Label => Token("@label") | |
.Next(Parentheses(QuotedStr)) | |
.Map<string, ICommand>(str => new NameLabelCommand(str)); | |
/// <summary> | |
/// キャラの表情を変える | |
/// (実際は、立ち絵のアニメーションを変更する) | |
/// </summary> | |
/// <returns></returns> | |
public Parser<ICommand> Face => Map<Tuple<string, string>, ICommand>( | |
Token("@face").Next(Args2(QuotedStr, QuotedStr)), | |
tuple => new FaceCommand(tuple.Item1, tuple.Item2) | |
); | |
/// <summary> | |
/// シナリオの実行を終了する | |
/// </summary> | |
public Parser<ICommand> Stop => Map<string, ICommand>(Token("@stop"), _ => new StopCommand()); | |
/// <summary> | |
/// UnknownCommand -- "@" で始まる未定義なフレーズ | |
/// </summary> | |
public Parser<ICommand> Unknown => Map<string, ICommand>( | |
Regexp("@[a-z_]+"), x => new UnknownCommand(x) | |
); | |
private Dictionary<string, ColorParam> colorParams = new Dictionary<string, ColorParam> | |
{ | |
{ "ON", ColorParam.White }, | |
{ "OFF", ColorParam.Gray }, | |
{ "HIDE", ColorParam.Hide } | |
}; | |
public Parser<IValue> StrLiteral => QuotedStr.Map<string, IValue>(s => new StringValue(s)); | |
public Parser<IValue> NumLiteral => Number.Map<float, IValue>(f => new FloatValue(f)); | |
public Parser<IValue> Arg => Choice(StrLiteral, NumLiteral); | |
/// <summary> | |
/// 一つ以上の引数リスト | |
/// </summary> | |
public Parser<List<IValue>> ArgList => Parentheses( | |
Arg.Tpl(Many(Comma.Next(Arg))).Map(tpl => | |
Cons(tpl.Item1, tpl.Item2) | |
)); | |
public Parser<ICommand> Command => | |
Regexp("@[^(]+").Tpl(Opt(ArgList)).Map(tpl => | |
MatchCommand(tpl.Item1, tpl.Item2.GetOr(new List<IValue>())) | |
); | |
public ICommand MatchCommand(string name, List<IValue> args) | |
{ | |
return new PauseCommand(1.0f); | |
} | |
private Map<string, Parser<ICommand>> hoge = new Map<string, Parser<ICommand>>() | |
{ | |
}; | |
private List<T> Cons<T>(T v, List<T> l) | |
{ | |
l.Insert(0, v); | |
return l; | |
} | |
} | |
public interface IValue | |
{ | |
} | |
public class StringValue : IValue | |
{ | |
public string Value { get; } | |
public StringValue(string value) | |
{ | |
Value = value; | |
} | |
} | |
public class FloatValue : IValue | |
{ | |
public float Value { get; } | |
public FloatValue(float value) | |
{ | |
Value = value; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment