Skip to content

Instantly share code, notes, and snippets.

@kkismd
Last active August 22, 2018 15:20
Show Gist options
  • Save kkismd/ce31253cf8369f605080d083c38cdaf1 to your computer and use it in GitHub Desktop.
Save kkismd/ce31253cf8369f605080d083c38cdaf1 to your computer and use it in GitHub Desktop.
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;
}
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>();
}
}
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&lt;string&gt;型同士の連接の場合、結果の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
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