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); } }