Regex tooling: formula gene grouping, content patches, def validation
CI / build-test (push) Successful in 1m12s
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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ead22517ee
commit
d044cafad9
@@ -143,4 +143,73 @@ public sealed class DefDatabaseTests : IDisposable
|
||||
Assert.Throws<KeyNotFoundException>(() => database.Get<AnimalDef>("Dodo"));
|
||||
Assert.False(database.TryGet<AnimalDef>("Dodo", out _));
|
||||
}
|
||||
|
||||
private Mod WriteMod(string fileName, string json)
|
||||
{
|
||||
var id = $"mod{_modCounter++:D2}";
|
||||
var modDir = Path.Combine(_root, id);
|
||||
Directory.CreateDirectory(Path.Combine(modDir, "Defs"));
|
||||
File.WriteAllText(Path.Combine(modDir, "Defs", fileName), json);
|
||||
return new Mod(new ModInfo { Id = id }, modDir);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Patch_SetsFields_OnDefsMatchingNamePattern()
|
||||
{
|
||||
var defs = WriteDefsMod(
|
||||
"""
|
||||
{ "type": "Animal", "defs": [
|
||||
{ "defName": "Wolf", "speed": 9 },
|
||||
{ "defName": "WolfPup", "speed": 4 },
|
||||
{ "defName": "Bear", "speed": 6 }
|
||||
]}
|
||||
"""
|
||||
);
|
||||
var patch = WriteMod(
|
||||
"patches.json",
|
||||
"""{ "type": "Patch", "patches": [ { "defType": "Animal", "match": "Wolf.*", "set": { "legs": 6 } } ] }"""
|
||||
);
|
||||
|
||||
var database = LoadAnimals(defs, patch);
|
||||
|
||||
Assert.Equal(6, database.Get<AnimalDef>("Wolf").Legs); // matched
|
||||
Assert.Equal(6, database.Get<AnimalDef>("WolfPup").Legs); // matched
|
||||
Assert.Equal(4, database.Get<AnimalDef>("Bear").Legs); // unmatched → default
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Patch_UnknownDefType_Throws()
|
||||
{
|
||||
var patch = WriteMod(
|
||||
"patches.json",
|
||||
"""{ "type": "Patch", "patches": [ { "defType": "Ghost", "match": ".*", "set": {} } ] }"""
|
||||
);
|
||||
|
||||
Assert.Throws<InvalidDataException>(() => LoadAnimals(patch));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_RejectsField_NotMatchingPattern()
|
||||
{
|
||||
var database = new DefDatabase();
|
||||
database.RegisterType<AnimalDef>("Animal");
|
||||
database.RegisterValidator("Animal", "defName", "^[A-Z]");
|
||||
var bad = WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "wolf" } ] }""");
|
||||
|
||||
var error = Assert.Throws<InvalidDataException>(() => database.Load([bad]));
|
||||
Assert.Contains("wolf", error.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validator_Passes_WhenFieldMatches()
|
||||
{
|
||||
var database = new DefDatabase();
|
||||
database.RegisterType<AnimalDef>("Animal");
|
||||
database.RegisterValidator("Animal", "defName", "^[A-Z]");
|
||||
database.Load([
|
||||
WriteDefsMod("""{ "type": "Animal", "defs": [ { "defName": "Wolf", "speed": 7 } ] }"""),
|
||||
]);
|
||||
|
||||
Assert.Equal(7f, database.Get<AnimalDef>("Wolf").Speed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,4 +109,66 @@ public class FormulaTests
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user