diff --git a/src/MrGameEng.Content/Genetics/Allele.cs b/src/MrGameEng.Content/Genetics/Allele.cs
new file mode 100644
index 0000000..a05de50
--- /dev/null
+++ b/src/MrGameEng.Content/Genetics/Allele.cs
@@ -0,0 +1,16 @@
+namespace MrGameEng.Genetics;
+
+///
+/// A diploid gene slot: the two allele values an individual carries for one gene. Stored as floats
+/// for both gene kinds — a gene simply holds integral variant
+/// indices. How the pair becomes a single phenotype value is decided by the gene's
+/// (see ).
+///
+public readonly record struct Allele(float A, float B)
+{
+ /// The average of the two alleles — the phenotype of a numeric gene.
+ public float Mean => (A + B) * 0.5f;
+
+ /// The lower (dominant) of the two alleles — the phenotype of a discrete gene.
+ public float Dominant => MathF.Min(A, B);
+}
diff --git a/src/MrGameEng.Content/Genetics/GeneDef.cs b/src/MrGameEng.Content/Genetics/GeneDef.cs
new file mode 100644
index 0000000..044fae1
--- /dev/null
+++ b/src/MrGameEng.Content/Genetics/GeneDef.cs
@@ -0,0 +1,97 @@
+using System.Text.Json.Serialization;
+using MrGameEng.Formulas;
+using MrGameEng.Mods;
+
+namespace MrGameEng.Genetics;
+
+/// How a gene's two alleles are stored and expressed into a phenotype value.
+public enum GeneKind
+{
+ /// A continuous value; the phenotype is the average of the two alleles (hybrid blending).
+ Numeric,
+
+ ///
+ /// A discrete allele index in [0, Variants); the lower index is dominant, so the
+ /// phenotype is min(a, b) — a higher (recessive) variant shows only when homozygous.
+ /// A two-variant discrete gene is effectively a flag.
+ ///
+ Discrete,
+}
+
+///
+/// An organism-agnostic gene definition — the unit the whole gene system is built from. A
+/// describes how to generate an individual's two alleles, how they mutate
+/// when bred, and how the gene contribute to named phenotype traits via
+/// expressions. Nothing here is plant-, animal- or human-specific, so the
+/// same machinery drives any organism and arbitrary hybrids (a genome can carry any mix of genes).
+///
+public sealed class GeneDef : Def
+{
+ /// Whether the gene is continuous or a discrete dominant/recessive allele.
+ public GeneKind Kind { get; init; } = GeneKind.Numeric;
+
+ /// Numeric: the central value an allele is generated around.
+ public float Default { get; init; }
+
+ /// Numeric: lower clamp for generated and mutated allele values.
+ public float Min { get; init; } = float.NegativeInfinity;
+
+ /// Numeric: upper clamp for generated and mutated allele values.
+ public float Max { get; init; } = float.PositiveInfinity;
+
+ /// Numeric: relative spread of generated alleles around (allele = Default ± Spread·|Default|).
+ public float Spread { get; init; }
+
+ /// Numeric: relative magnitude of a mutation step (value ± Magnitude·|value|).
+ public float MutationMagnitude { get; init; } = 0.1f;
+
+ /// Discrete: number of allele variants, valued 0..Variants-1.
+ public int Variants { get; init; } = 2;
+
+ ///
+ /// Discrete: relative weights for generating each variant (length ).
+ /// Empty means a uniform distribution.
+ ///
+ public float[] VariantWeights { get; init; } = [];
+
+ /// Probability, per allele, that a mutation occurs when this gene is passed to a child.
+ public float MutationChance { get; init; }
+
+ ///
+ /// The gene's contributions to phenotype traits: trait name → formula. Each formula may use the
+ /// variable value (this gene's expressed phenotype), any other gene's id (its expressed
+ /// value) and any environment variable the caller supplies. Contributions to the same trait
+ /// across genes are summed.
+ ///
+ public Dictionary Effects { get; init; } = new();
+
+ /// Free-form category tags for grouping genes (used by formula grouping and content tooling).
+ public string[] Tags { get; init; } = [];
+
+ private IReadOnlyDictionary? _compiled;
+
+ /// The compiled once into evaluable formulas (lazy, cached).
+ [JsonIgnore]
+ public IReadOnlyDictionary CompiledEffects => _compiled ??= CompileEffects();
+
+ private Dictionary CompileEffects()
+ {
+ var compiled = new Dictionary(StringComparer.Ordinal);
+ foreach (var (trait, expression) in Effects)
+ {
+ try
+ {
+ compiled[trait] = Formula.Compile(expression);
+ }
+ catch (FormulaException error)
+ {
+ throw new InvalidDataException(
+ $"Gene '{DefName}' effect on trait '{trait}' has an invalid formula "
+ + $"\"{expression}\": {error.Message}"
+ );
+ }
+ }
+
+ return compiled;
+ }
+}
diff --git a/src/MrGameEng.Content/Genetics/Genome.cs b/src/MrGameEng.Content/Genetics/Genome.cs
new file mode 100644
index 0000000..64ebbb9
--- /dev/null
+++ b/src/MrGameEng.Content/Genetics/Genome.cs
@@ -0,0 +1,190 @@
+namespace MrGameEng.Genetics;
+
+///
+/// An individual's managed genome: a variable-composition map from gene id to the
+/// pair it carries. Because composition is open, two organisms need not share
+/// the same gene set and a genome can gain "foreign" genes — the basis for arbitrary hybrids. The
+/// genome is generated from a set of s, bred meiotically with mutation, and
+/// expressed into phenotype values; all randomness flows through a caller-owned seeded
+/// so the simulation stays deterministic.
+///
+public sealed class Genome
+{
+ private readonly Dictionary _alleles;
+
+ /// Creates an empty genome.
+ public Genome() => _alleles = new Dictionary(StringComparer.Ordinal);
+
+ /// Creates a genome from an existing allele map (copied).
+ public Genome(IReadOnlyDictionary alleles) =>
+ _alleles = new Dictionary(alleles, StringComparer.Ordinal);
+
+ /// The carried genes and their allele pairs.
+ public IReadOnlyDictionary Alleles => _alleles;
+
+ /// Whether the genome carries the gene .
+ public bool Has(string geneId) => _alleles.ContainsKey(geneId);
+
+ /// Gets or sets the allele pair for .
+ public Allele this[string geneId]
+ {
+ get => _alleles[geneId];
+ set => _alleles[geneId] = value;
+ }
+
+ /// Removes a gene from the genome; returns whether it was present.
+ public bool Remove(string geneId) => _alleles.Remove(geneId);
+
+ ///
+ /// Expresses the gene's phenotype value: the mean of the alleles for a numeric gene, the
+ /// dominant (lower) allele for a discrete one. Throws if the genome does not carry the gene.
+ ///
+ public float Express(GeneDef gene)
+ {
+ if (!_alleles.TryGetValue(gene.DefName, out var allele))
+ {
+ throw new KeyNotFoundException($"Genome does not carry gene '{gene.DefName}'.");
+ }
+
+ return gene.Kind == GeneKind.Numeric ? allele.Mean : allele.Dominant;
+ }
+
+ /// Builds the allele map for serialization (a copy).
+ public Dictionary ToDictionary() => new(_alleles, StringComparer.Ordinal);
+
+ ///
+ /// Generates a fresh genome carrying every gene in , each allele drawn
+ /// independently around the gene's default with its spread (numeric) or from its variant
+ /// distribution (discrete).
+ ///
+ public static Genome Generate(IEnumerable genes, Random random)
+ {
+ var genome = new Genome();
+ foreach (var gene in genes)
+ {
+ genome._alleles[gene.DefName] = new Allele(
+ GenerateAllele(gene, random),
+ GenerateAllele(gene, random)
+ );
+ }
+
+ return genome;
+ }
+
+ ///
+ /// Breeds a child genome from two parents (meiosis): the child carries every gene either parent
+ /// has. For a gene both carry, one allele is drawn from each parent; for a gene only one parent
+ /// carries, it is inherited (from that parent, on both sides) with 50% probability. Every
+ /// inherited allele may then mutate per its .
+ /// supplies the def for each gene id; genes absent from it are skipped.
+ ///
+ public static Genome Breed(
+ Genome a,
+ Genome b,
+ IReadOnlyDictionary registry,
+ Random random
+ )
+ {
+ var child = new Genome();
+ foreach (var geneId in UnionKeys(a, b))
+ {
+ if (!registry.TryGetValue(geneId, out var gene))
+ {
+ continue;
+ }
+
+ var inA = a.Has(geneId);
+ var inB = b.Has(geneId);
+ if (inA && inB)
+ {
+ child._alleles[geneId] = new Allele(
+ Meiosis(gene, a[geneId], random),
+ Meiosis(gene, b[geneId], random)
+ );
+ }
+ else if (random.NextSingle() < 0.5f)
+ {
+ var parent = inA ? a : b;
+ child._alleles[geneId] = new Allele(
+ Meiosis(gene, parent[geneId], random),
+ Meiosis(gene, parent[geneId], random)
+ );
+ }
+ }
+
+ return child;
+ }
+
+ // One inherited allele: pick one of the parent slot's two alleles, then maybe mutate it.
+ private static float Meiosis(GeneDef gene, Allele parent, Random random)
+ {
+ var inherited = random.NextSingle() < 0.5f ? parent.A : parent.B;
+ if (random.NextSingle() >= gene.MutationChance)
+ {
+ return inherited;
+ }
+
+ if (gene.Kind == GeneKind.Discrete)
+ {
+ return PickVariant(gene, random);
+ }
+
+ var shifted =
+ inherited
+ + (random.NextSingle() * 2f - 1f) * gene.MutationMagnitude * MathF.Abs(inherited);
+ return Math.Clamp(shifted, gene.Min, gene.Max);
+ }
+
+ private static float GenerateAllele(GeneDef gene, Random random)
+ {
+ if (gene.Kind == GeneKind.Discrete)
+ {
+ return PickVariant(gene, random);
+ }
+
+ var value =
+ gene.Default + (random.NextSingle() * 2f - 1f) * gene.Spread * MathF.Abs(gene.Default);
+ return Math.Clamp(value, gene.Min, gene.Max);
+ }
+
+ private static float PickVariant(GeneDef gene, Random random)
+ {
+ var variants = Math.Max(1, gene.Variants);
+ if (gene.VariantWeights.Length != variants)
+ {
+ return random.Next(variants);
+ }
+
+ var total = 0f;
+ foreach (var w in gene.VariantWeights)
+ {
+ total += MathF.Max(0f, w);
+ }
+
+ if (total <= 0f)
+ {
+ return random.Next(variants);
+ }
+
+ var roll = random.NextSingle() * total;
+ for (var i = 0; i < variants; i++)
+ {
+ roll -= MathF.Max(0f, gene.VariantWeights[i]);
+ if (roll < 0f)
+ {
+ return i;
+ }
+ }
+
+ return variants - 1;
+ }
+
+ // Deterministic union of both parents' gene ids (ordered) so breeding is reproducible.
+ private static IEnumerable UnionKeys(Genome a, Genome b)
+ {
+ var keys = new SortedSet(StringComparer.Ordinal);
+ keys.UnionWith(a._alleles.Keys);
+ keys.UnionWith(b._alleles.Keys);
+ return keys;
+ }
+}
diff --git a/src/MrGameEng.Content/Genetics/Phenotype.cs b/src/MrGameEng.Content/Genetics/Phenotype.cs
new file mode 100644
index 0000000..ffe1dec
--- /dev/null
+++ b/src/MrGameEng.Content/Genetics/Phenotype.cs
@@ -0,0 +1,75 @@
+using MrGameEng.Formulas;
+
+namespace MrGameEng.Genetics;
+
+///
+/// Computes the trait layer — the phenotype the simulation actually reads — from a
+/// . Each gene's formulas are evaluated and their
+/// results summed per trait name, so systems never touch genes directly: one fruiting system reads
+/// a fruitYield trait whether it comes from a tree or a human carrying a "fruit" gene.
+/// Formulas see the variable value (the contributing gene's expressed phenotype), any other
+/// carried gene's id, and whatever environment variables the caller supplies.
+///
+public static class Phenotype
+{
+ ///
+ /// Evaluates every carried gene's effects against the genome and an optional
+ /// , summing contributions into a trait map. Genes missing from
+ /// are skipped.
+ ///
+ public static Dictionary Compute(
+ Genome genome,
+ IReadOnlyDictionary registry,
+ IFormulaContext? environment = null
+ )
+ {
+ var traits = new Dictionary(StringComparer.Ordinal);
+ var context = new GenomeContext(genome, registry, environment);
+ foreach (var geneId in genome.Alleles.Keys)
+ {
+ if (!registry.TryGetValue(geneId, out var gene) || gene.CompiledEffects.Count == 0)
+ {
+ continue;
+ }
+
+ context.Self = genome.Express(gene);
+ foreach (var (trait, formula) in gene.CompiledEffects)
+ {
+ traits[trait] = traits.GetValueOrDefault(trait) + formula.Evaluate(context);
+ }
+ }
+
+ return traits;
+ }
+
+ // Resolves formula variables for a gene effect: 'value' is the current gene's phenotype, any
+ // carried gene's id resolves to its phenotype, anything else falls through to the environment.
+ private sealed class GenomeContext(
+ Genome genome,
+ IReadOnlyDictionary registry,
+ IFormulaContext? environment
+ ) : IFormulaContext
+ {
+ public float Self;
+
+ public float Resolve(string name)
+ {
+ if (name == "value")
+ {
+ return Self;
+ }
+
+ if (genome.Has(name) && registry.TryGetValue(name, out var gene))
+ {
+ return genome.Express(gene);
+ }
+
+ if (environment is not null)
+ {
+ return environment.Resolve(name);
+ }
+
+ throw new FormulaException($"Unknown variable '{name}' while computing traits.");
+ }
+ }
+}
diff --git a/src/MrGameEng.Content/Mods/ModInfo.cs b/src/MrGameEng.Content/Mods/ModInfo.cs
index 948116f..63f2dd6 100644
--- a/src/MrGameEng.Content/Mods/ModInfo.cs
+++ b/src/MrGameEng.Content/Mods/ModInfo.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using System.Text.Json.Serialization;
namespace MrGameEng.Mods;
@@ -14,6 +15,7 @@ public sealed class ModInfo
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
WriteIndented = true,
+ Converters = { new JsonStringEnumConverter() },
};
/// Unique mod id, referenced by of other mods.
diff --git a/tests/MrGameEng.Content.Tests/Genetics/GeneDefLoadTests.cs b/tests/MrGameEng.Content.Tests/Genetics/GeneDefLoadTests.cs
new file mode 100644
index 0000000..1e2e61d
--- /dev/null
+++ b/tests/MrGameEng.Content.Tests/Genetics/GeneDefLoadTests.cs
@@ -0,0 +1,68 @@
+using MrGameEng.Genetics;
+using MrGameEng.Mods;
+using Xunit;
+
+namespace MrGameEng.Genetics.Tests;
+
+///
+/// Verifies loads through the real the way the game
+/// loads genes.json: string-named , parent inheritance and effect maps.
+///
+public sealed class GeneDefLoadTests : IDisposable
+{
+ private readonly string _root = Directory.CreateTempSubdirectory("mrge-gene-tests-").FullName;
+
+ public void Dispose() => Directory.Delete(_root, recursive: true);
+
+ private DefDatabase Load(string defsJson)
+ {
+ var modDir = Path.Combine(_root, "mod");
+ Directory.CreateDirectory(Path.Combine(modDir, "Defs"));
+ File.WriteAllText(Path.Combine(modDir, "Defs", "genes.json"), defsJson);
+ var database = new DefDatabase();
+ database.RegisterType("Gene");
+ database.Load([new Mod(new ModInfo { Id = "mod" }, modDir)]);
+ return database;
+ }
+
+ [Fact]
+ public void Load_NumericGeneWithParentAndEffects_Resolves()
+ {
+ var database = Load(
+ """
+ { "type": "Gene", "defs": [
+ { "defName": "BaseGene", "abstract": true, "spread": 0.1, "mutationChance": 0.05 },
+ { "defName": "GeneVigor", "parent": "BaseGene", "default": 1.0, "min": 0.1, "max": 3.0,
+ "tags": ["growth"], "effects": { "vigor": "value" } }
+ ]}
+ """
+ );
+
+ var gene = database.Get("GeneVigor");
+ Assert.Equal(GeneKind.Numeric, gene.Kind);
+ Assert.Equal(0.1f, gene.Spread); // inherited from parent
+ Assert.Equal(0.05f, gene.MutationChance); // inherited
+ Assert.Equal(3.0f, gene.Max);
+ Assert.Equal(["growth"], gene.Tags);
+ Assert.Equal("value", gene.Effects["vigor"]);
+ Assert.Single(gene.CompiledEffects); // formula compiles
+ }
+
+ [Fact]
+ public void Load_DiscreteKindByName_Parses()
+ {
+ var database = Load(
+ """
+ { "type": "Gene", "defs": [
+ { "defName": "GeneMorph", "kind": "Discrete", "variants": 2,
+ "variantWeights": [0.8, 0.2], "effects": { "variant": "value" } }
+ ]}
+ """
+ );
+
+ var gene = database.Get("GeneMorph");
+ Assert.Equal(GeneKind.Discrete, gene.Kind);
+ Assert.Equal(2, gene.Variants);
+ Assert.Equal([0.8f, 0.2f], gene.VariantWeights);
+ }
+}
diff --git a/tests/MrGameEng.Content.Tests/Genetics/GeneticsTests.cs b/tests/MrGameEng.Content.Tests/Genetics/GeneticsTests.cs
new file mode 100644
index 0000000..d59a619
--- /dev/null
+++ b/tests/MrGameEng.Content.Tests/Genetics/GeneticsTests.cs
@@ -0,0 +1,194 @@
+using MrGameEng.Formulas;
+using MrGameEng.Genetics;
+using Xunit;
+
+namespace MrGameEng.Genetics.Tests;
+
+public class GeneticsTests
+{
+ private static GeneDef Numeric(
+ string name,
+ float def,
+ float spread = 0f,
+ float min = float.NegativeInfinity,
+ float max = float.PositiveInfinity,
+ float mutationChance = 0f,
+ float mutationMagnitude = 0.1f,
+ Dictionary? effects = null
+ ) =>
+ new()
+ {
+ DefName = name,
+ Kind = GeneKind.Numeric,
+ Default = def,
+ Spread = spread,
+ Min = min,
+ Max = max,
+ MutationChance = mutationChance,
+ MutationMagnitude = mutationMagnitude,
+ Effects = effects ?? new(),
+ };
+
+ private static GeneDef Discrete(string name, int variants = 2, float mutationChance = 0f) =>
+ new()
+ {
+ DefName = name,
+ Kind = GeneKind.Discrete,
+ Variants = variants,
+ MutationChance = mutationChance,
+ };
+
+ private static Dictionary Registry(params GeneDef[] genes) =>
+ genes.ToDictionary(g => g.DefName, g => g, StringComparer.Ordinal);
+
+ [Fact]
+ public void Generate_NumericAlleles_StayWithinSpreadAndClamp()
+ {
+ var gene = Numeric("vigor", def: 1f, spread: 0.2f, min: 0f, max: 2f);
+ var random = new Random(7);
+
+ for (var i = 0; i < 200; i++)
+ {
+ var genome = Genome.Generate([gene], random);
+ var allele = genome["vigor"];
+ Assert.InRange(allele.A, 0.8f, 1.2f);
+ Assert.InRange(allele.B, 0.8f, 1.2f);
+ }
+ }
+
+ [Fact]
+ public void Generate_SameSeed_IsDeterministic()
+ {
+ var genes = new[] { Numeric("a", 1f, 0.3f), Discrete("morph", 2) };
+
+ var first = Genome.Generate(genes, new Random(42));
+ var second = Genome.Generate(genes, new Random(42));
+
+ Assert.Equal(first["a"], second["a"]);
+ Assert.Equal(first["morph"], second["morph"]);
+ }
+
+ [Fact]
+ public void Express_Numeric_IsAlleleMean()
+ {
+ var gene = Numeric("opt", 0f);
+ var genome = new Genome { ["opt"] = new Allele(0.4f, 0.8f) };
+
+ Assert.Equal(0.6f, genome.Express(gene), 5);
+ }
+
+ [Fact]
+ public void Express_Discrete_DominantIsLowerIndex()
+ {
+ var gene = Discrete("morph", 2);
+
+ Assert.Equal(0f, new Genome { ["morph"] = new Allele(0f, 1f) }.Express(gene)); // heterozygous → dominant 0
+ Assert.Equal(1f, new Genome { ["morph"] = new Allele(1f, 1f) }.Express(gene)); // homozygous recessive
+ }
+
+ [Fact]
+ public void Breed_WithoutMutation_InheritsOneAlleleFromEachParent()
+ {
+ var gene = Numeric("g", 0f, mutationChance: 0f);
+ var registry = Registry(gene);
+ var a = new Genome { ["g"] = new Allele(1f, 2f) };
+ var b = new Genome { ["g"] = new Allele(3f, 4f) };
+
+ var child = Genome.Breed(a, b, registry, new Random(1));
+
+ Assert.Contains(child["g"].A, new[] { 1f, 2f }); // first allele from parent a
+ Assert.Contains(child["g"].B, new[] { 3f, 4f }); // second allele from parent b
+ }
+
+ [Fact]
+ public void Breed_UnionOfGenes_ProducesHybridComposition()
+ {
+ // a carries only "leaf", b carries only "root" — a child can carry both (hybrid).
+ var registry = Registry(Numeric("leaf", 1f), Numeric("root", 1f));
+ var a = new Genome { ["leaf"] = new Allele(1f, 1f) };
+ var b = new Genome { ["root"] = new Allele(2f, 2f) };
+
+ var carriedBoth = false;
+ for (var seed = 0; seed < 50 && !carriedBoth; seed++)
+ {
+ var child = Genome.Breed(a, b, registry, new Random(seed));
+ carriedBoth = child.Has("leaf") && child.Has("root");
+ }
+
+ Assert.True(carriedBoth, "single-parent genes should sometimes both be inherited");
+ }
+
+ [Fact]
+ public void Breed_HighMutation_DiscreteCanFlipVariant()
+ {
+ var gene = Discrete("morph", variants: 2, mutationChance: 1f);
+ var registry = Registry(gene);
+ var parent = new Genome { ["morph"] = new Allele(0f, 0f) };
+
+ var sawOne = false;
+ for (var seed = 0; seed < 50 && !sawOne; seed++)
+ {
+ var child = Genome.Breed(parent, parent, registry, new Random(seed));
+ var allele = child["morph"];
+ sawOne = allele.A == 1f || allele.B == 1f;
+ }
+
+ Assert.True(sawOne, "with full mutation a 0/0 parent should sometimes yield variant 1");
+ }
+
+ [Fact]
+ public void Compute_GeneEffect_UsesValueVariable()
+ {
+ var gene = Numeric("vigor", 0f, effects: new() { ["growth"] = "value * 2" });
+ var genome = new Genome { ["vigor"] = new Allele(1.5f, 2.5f) }; // mean 2
+
+ var traits = Phenotype.Compute(genome, Registry(gene));
+
+ Assert.Equal(4f, traits["growth"], 5); // 2 * 2
+ }
+
+ [Fact]
+ public void Compute_MultipleGenes_SumContributionsPerTrait()
+ {
+ var a = Numeric("a", 0f, effects: new() { ["yield"] = "value" });
+ var b = Numeric("b", 0f, effects: new() { ["yield"] = "value" });
+ var genome = new Genome { ["a"] = new Allele(3f, 3f), ["b"] = new Allele(4f, 4f) };
+
+ var traits = Phenotype.Compute(genome, Registry(a, b));
+
+ Assert.Equal(7f, traits["yield"], 5);
+ }
+
+ [Fact]
+ public void Compute_FormulaReadsEnvironmentAndOtherGenes()
+ {
+ var opt = Numeric("optLight", 0.5f);
+ var vigor = Numeric(
+ "vigor",
+ 1f,
+ effects: new() { ["rate"] = "value * (1 - abs(light - optLight))" }
+ );
+ var genome = new Genome
+ {
+ ["optLight"] = new Allele(0.5f, 0.5f),
+ ["vigor"] = new Allele(1f, 1f),
+ };
+ var env = new DelegateFormulaContext(n =>
+ n == "light" ? 0.7f : throw new FormulaException(n)
+ );
+
+ var traits = Phenotype.Compute(genome, Registry(opt, vigor), env);
+
+ Assert.Equal(0.8f, traits["rate"], 5); // 1 * (1 - |0.7 - 0.5|)
+ }
+
+ [Fact]
+ public void CompiledEffects_InvalidFormula_ThrowsWithGeneAndTrait()
+ {
+ var gene = Numeric("bad", 0f, effects: new() { ["t"] = "value *" });
+
+ var error = Assert.Throws(() => _ = gene.CompiledEffects);
+ Assert.Contains("bad", error.Message);
+ Assert.Contains("t", error.Message);
+ }
+}