acdream/tests/AcDream.Plugins.MossTank.Tests/MonsterExpressionTests.cs

164 lines
5.6 KiB
C#

namespace AcDream.Plugins.MossTank.Tests;
public sealed class MonsterExpressionTests
{
[Theory]
[InlineData("range>5", true)]
[InlineData("range<=5", false)]
[InlineData("range>5 && species==drudge", true)]
[InlineData("range>5 && species==drudge && name!=drudge ravener", true)]
[InlineData("hasshield && metastate==hunting", true)]
[InlineData("typeid==1234", true)]
[InlineData("maxhp>=450", true)]
public void DocumentedVariablesAndOperatorsEvaluateVerbatim(
string source,
bool expected)
{
MonsterExpression expression = MonsterExpression.Compile(source);
Assert.True(expression.TryEvaluate(
Context(),
out MonsterValue value,
out string? error),
error);
Assert.Equal(MonsterValueKind.Boolean, value.Kind);
Assert.Equal(expected, value.Boolean);
}
[Fact]
public void BareTextMatchesTheMonsterNameCaseInsensitively()
{
MonsterExpression expression = MonsterExpression.Compile("Drudge Ravener");
Assert.True(expression.IsMatch(Context(name: "drudge ravener"), out _));
Assert.False(expression.IsMatch(Context(name: "Drudge Lurker"), out _));
}
[Fact]
public void RegexUsesLeftAsInputAndRightAsPattern()
{
MonsterExpression expression = MonsterExpression.Compile(
"name#^drudge .\\+er$");
Assert.True(expression.IsMatch(Context(), out string? error), error);
}
[Fact]
public void EscapedOperatorAndDigitRemainLiteralStringCharacters()
{
MonsterExpression expression = MonsterExpression.Compile(
"name==Prototype \\#\\2");
Assert.True(expression.IsMatch(Context(name: "Prototype #2"), out _));
Assert.Throws<MonsterExpressionException>(() =>
MonsterExpression.Compile("name==Prototype 2"));
}
[Fact]
public void UsesVtankDocumentedNonstandardPrecedence()
{
MonsterExpression subtraction = MonsterExpression.Compile("10-3+1==6");
MonsterExpression modulo = MonsterExpression.Compile("20/6%4==10");
Assert.True(subtraction.IsMatch(Context(), out _));
Assert.True(modulo.IsMatch(Context(), out _));
}
[Fact]
public void BooleanOperatorsShortCircuitInvalidRightBranch()
{
MonsterExpression expression = MonsterExpression.Compile(
"false && 1/0==0");
Assert.True(expression.TryEvaluate(
Context(),
out MonsterValue value,
out string? error),
error);
Assert.False(value.Boolean);
}
[Fact]
public void SettingNamesAreCaseSensitiveAndMakeExpressionDynamic()
{
var settings = new Dictionary<string, MonsterValue>(StringComparer.Ordinal)
{
["DoJiggle"] = MonsterValue.FromBoolean(true),
};
var context = Context(setting: name =>
settings.TryGetValue(name, out MonsterValue value) ? value : null);
MonsterExpression correct = MonsterExpression.Compile("setting_DoJiggle");
MonsterExpression wrong = MonsterExpression.Compile("setting_dojiggle");
Assert.True(correct.IsDynamic);
Assert.True(correct.IsMatch(context, out _));
Assert.False(wrong.IsMatch(context, out _));
}
[Fact]
public void TypeMismatchIsAReportedNonMatchRatherThanAPluginCrash()
{
MonsterExpression expression = MonsterExpression.Compile("name==5");
Assert.False(expression.IsMatch(Context(), out string? error));
Assert.Contains("matching operand types", error, StringComparison.Ordinal);
}
[Fact]
public void ResolverChecksRowsInOrderAfterDefaultAndCarriesAllActions()
{
var fallback = new MonsterRule("DEFAULT", 0);
var first = new MonsterRule(
"species==drudge",
new MonsterRuleActions
{
Priority = 3,
Flags = MonsterActionFlags.Imperil | MonsterActionFlags.Attack,
DamageType = MonsterDamageType.Fire,
ExtraVulnerability = MonsterDamageType.Fire,
WeaponObjectId = 0x70000001u,
OffhandObjectId = 0x70000002u,
PetDamageType = MonsterDamageType.Cold,
});
var later = new MonsterRule("range>1", 4);
ResolvedMonsterRule resolved = MonsterRuleResolver.Resolve(
[fallback, first, later],
Context());
Assert.Same(first, resolved.Rule);
Assert.Equal(3, resolved.Priority);
Assert.Equal(
MonsterActionFlags.Imperil | MonsterActionFlags.Attack,
resolved.Actions.Flags);
Assert.Equal(MonsterDamageType.Fire, resolved.Actions.DamageType);
Assert.Equal(0x70000001u, resolved.Actions.WeaponObjectId);
Assert.Equal(0x70000002u, resolved.Actions.OffhandObjectId);
Assert.Equal(MonsterDamageType.Cold, resolved.Actions.PetDamageType);
}
[Fact]
public void ResolverFallsBackToDefaultAndClampsPriority()
{
var fallback = new MonsterRule("DEFAULT", 99);
ResolvedMonsterRule resolved = MonsterRuleResolver.Resolve(
[fallback, new MonsterRule("species==olthoi", 4)],
Context());
Assert.Same(fallback, resolved.Rule);
Assert.Equal(4, resolved.Priority);
}
private static MonsterExpressionContext Context(
string name = "Drudge Lurker",
Func<string, MonsterValue?>? setting = null) => new(
name,
TypeId: 1234u,
Species: "Drudge",
MaximumHealth: 450,
Range: 8f,
HasShield: true,
MetaState: "Hunting",
Setting: setting);
}