CI / build-test (push) Successful in 1m12s
Three regex-powered content tools (phase G5):
- Formula group functions gsum/gcount/gavg/gmin/gmax('regex') aggregate
over every context variable whose name matches the pattern, e.g.
gsum('leaf_.*'). Adds string literals to the formula grammar and an
IFormulaContext.ResolveMatching hook; GenomeContext enumerates matching
genes, so a trait can sum/average a gene group.
- DefDatabase content patches: a { "type": "Patch", patches:[{ defType,
match (regex on defName), set:{fields} }] } file sets fields on every
matching raw def before resolution — mods patch Core in bulk.
- DefDatabase.RegisterValidator(typeKey, field, regex): load-time check
that a string field matches a pattern, throwing otherwise (naming/format
conventions).
Covered by FormulaTests (group aggregation, composition, bad calls) and
DefDatabaseTests (patch set/match/unknown-type, validator pass/reject).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
175 lines
5.7 KiB
C#
175 lines
5.7 KiB
C#
using MrGameEng.Formulas;
|
|
using Xunit;
|
|
|
|
namespace MrGameEng.Core.Tests;
|
|
|
|
public class FormulaTests
|
|
{
|
|
private static IFormulaContext Vars(Dictionary<string, float> values) =>
|
|
new DelegateFormulaContext(name =>
|
|
values.TryGetValue(name, out var v)
|
|
? v
|
|
: throw new FormulaException($"unknown '{name}'")
|
|
);
|
|
|
|
[Theory]
|
|
[InlineData("1 + 2 * 3", 7f)] // precedence
|
|
[InlineData("(1 + 2) * 3", 9f)] // parentheses
|
|
[InlineData("10 - 2 - 3", 5f)] // left associativity
|
|
[InlineData("-2 + 5", 3f)] // unary minus
|
|
[InlineData("2 * -3", -6f)] // unary minus after operator
|
|
[InlineData("7 % 3", 1f)] // modulo
|
|
[InlineData("2.5 * 4", 10f)] // decimals
|
|
public void Evaluate_Arithmetic_RespectsPrecedence(string expr, float expected)
|
|
{
|
|
Assert.Equal(expected, Formula.Compile(expr).Evaluate(), 4);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("min(3, 5)", 3f)]
|
|
[InlineData("max(3, 5)", 5f)]
|
|
[InlineData("clamp(12, 0, 10)", 10f)]
|
|
[InlineData("clamp(-4, 0, 10)", 0f)]
|
|
[InlineData("lerp(0, 10, 0.25)", 2.5f)]
|
|
[InlineData("pow(2, 10)", 1024f)]
|
|
[InlineData("abs(-7)", 7f)]
|
|
[InlineData("floor(3.9)", 3f)]
|
|
[InlineData("ceil(3.1)", 4f)]
|
|
[InlineData("step(5, 7)", 1f)]
|
|
[InlineData("step(5, 2)", 0f)]
|
|
public void Evaluate_Functions_ComputeExpected(string expr, float expected)
|
|
{
|
|
Assert.Equal(expected, Formula.Compile(expr).Evaluate(), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Constants_AreBuiltIn()
|
|
{
|
|
Assert.Equal(MathF.PI, Formula.Compile("pi").Evaluate(), 5);
|
|
Assert.Equal(MathF.Tau, Formula.Compile("tau").Evaluate(), 5);
|
|
Assert.Equal(MathF.E, Formula.Compile("e").Evaluate(), 5);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("2 < 3", 1f)]
|
|
[InlineData("3 <= 3", 1f)]
|
|
[InlineData("3 > 5", 0f)]
|
|
[InlineData("4 == 4", 1f)]
|
|
[InlineData("4 != 4", 0f)]
|
|
[InlineData("1 && 0", 0f)]
|
|
[InlineData("0 || 2", 1f)]
|
|
[InlineData("!0", 1f)]
|
|
[InlineData("!5", 0f)]
|
|
public void Evaluate_LogicAndComparison_YieldBooleansAsOneOrZero(string expr, float expected)
|
|
{
|
|
Assert.Equal(expected, Formula.Compile(expr).Evaluate(), 4);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("3 > 2 ? 10 : 20", 10f)]
|
|
[InlineData("3 < 2 ? 10 : 20", 20f)]
|
|
[InlineData("1 ? 2 ? 3 : 4 : 5", 3f)] // nested ternary
|
|
public void Evaluate_Ternary_SelectsBranch(string expr, float expected)
|
|
{
|
|
Assert.Equal(expected, Formula.Compile(expr).Evaluate(), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_Variables_ResolvedThroughContext()
|
|
{
|
|
var ctx = Vars(new() { ["light"] = 0.8f, ["optimal"] = 0.5f });
|
|
var formula = Formula.Compile("clamp(1 - abs(light - optimal), 0, 1)");
|
|
Assert.Equal(0.7f, formula.Evaluate(ctx), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_SameFormulaTwice_IsDeterministic()
|
|
{
|
|
var formula = Formula.Compile("sin(t) * 2 + 1");
|
|
var ctx = Vars(new() { ["t"] = 1.234f });
|
|
Assert.Equal(formula.Evaluate(ctx), formula.Evaluate(ctx), 6);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("1 +")] // dangling operator
|
|
[InlineData("(1 + 2")] // unbalanced paren
|
|
[InlineData("1 2")] // trailing token
|
|
[InlineData("min(1)")] // wrong arity
|
|
[InlineData("nope(1)")] // unknown function
|
|
[InlineData("@")] // bad character
|
|
[InlineData("1 = 2")] // single equals
|
|
public void Compile_InvalidExpression_Throws(string expr)
|
|
{
|
|
Assert.Throws<FormulaException>(() => Formula.Compile(expr));
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_UnknownVariableWithoutContext_Throws()
|
|
{
|
|
var formula = Formula.Compile("x + 1");
|
|
Assert.Throws<FormulaException>(() => formula.Evaluate());
|
|
}
|
|
|
|
// --- Group functions (regex aggregation over matching variables) ---
|
|
|
|
private sealed class GroupContext(Dictionary<string, float> values) : IFormulaContext
|
|
{
|
|
public float Resolve(string name) => values[name];
|
|
|
|
public IEnumerable<float> ResolveMatching(Func<string, bool> matches)
|
|
{
|
|
foreach (var (name, value) in values)
|
|
{
|
|
if (matches(name))
|
|
{
|
|
yield return value;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GroupFunctions_AggregateMatchingVariables()
|
|
{
|
|
var ctx = new GroupContext(
|
|
new()
|
|
{
|
|
["leaf_a"] = 2f,
|
|
["leaf_b"] = 4f,
|
|
["leaf_c"] = 6f,
|
|
["root_a"] = 100f,
|
|
}
|
|
);
|
|
|
|
Assert.Equal(12f, Formula.Compile("gsum('leaf_.*')").Evaluate(ctx), 4); // 2+4+6
|
|
Assert.Equal(3f, Formula.Compile("gcount('leaf_.*')").Evaluate(ctx), 4);
|
|
Assert.Equal(4f, Formula.Compile("gavg('leaf_.*')").Evaluate(ctx), 4);
|
|
Assert.Equal(2f, Formula.Compile("gmin('leaf_.*')").Evaluate(ctx), 4);
|
|
Assert.Equal(6f, Formula.Compile("gmax('leaf_.*')").Evaluate(ctx), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GroupFunction_ComposesWithArithmetic()
|
|
{
|
|
var ctx = new GroupContext(new() { ["g1"] = 3f, ["g2"] = 5f });
|
|
Assert.Equal(16f, Formula.Compile("gsum('g.*') * 2").Evaluate(ctx), 4); // (3+5)*2
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_GroupFunction_NoMatches_IsZero()
|
|
{
|
|
var ctx = new GroupContext(new() { ["x"] = 1f });
|
|
Assert.Equal(0f, Formula.Compile("gsum('none_.*')").Evaluate(ctx), 4);
|
|
Assert.Equal(0f, Formula.Compile("gavg('none_.*')").Evaluate(ctx), 4);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("gsum(leaf)")] // pattern must be a quoted string
|
|
[InlineData("gsum('[')")] // invalid regex
|
|
[InlineData("gsum('a' 'b')")] // extra token
|
|
public void Compile_BadGroupCall_Throws(string expr)
|
|
{
|
|
Assert.Throws<FormulaException>(() => Formula.Compile(expr));
|
|
}
|
|
}
|