diff --git a/src/MrGameEng.Simulation/AI/Suitability.cs b/src/MrGameEng.Simulation/AI/Suitability.cs new file mode 100644 index 0000000..557a164 --- /dev/null +++ b/src/MrGameEng.Simulation/AI/Suitability.cs @@ -0,0 +1,26 @@ +namespace MrGameEng.AI; + +/// +/// Suitability curves: how well an environmental value matches a preferred optimum. Unlike +/// (monotonic shaping over [0,1]), these are bell shapes around an +/// optimum on an arbitrary scale — the building block for "this organism likes ~18°C, tolerates ±12°". +/// Pure and GPU-free. +/// +public static class Suitability +{ + /// + /// Gaussian bell in [0, 1]: 1 when equals , + /// falling off as it departs, reaching e^-0.5 ≈ 0.607 one away. + /// A non-positive degenerates to an exact match (1 at the optimum, else 0). + /// + public static float Gaussian(float value, float optimum, float tolerance) + { + if (tolerance <= 0f) + { + return value == optimum ? 1f : 0f; + } + + var z = (value - optimum) / tolerance; + return MathF.Exp(-0.5f * z * z); + } +} diff --git a/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs b/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs new file mode 100644 index 0000000..d61fe99 --- /dev/null +++ b/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs @@ -0,0 +1,51 @@ +using MrGameEng.AI; +using Xunit; + +namespace MrGameEng.AI.Tests; + +public class SuitabilityTests +{ + [Fact] + public void Gaussian_PeaksAtOptimum() + { + Assert.Equal(1f, Suitability.Gaussian(18f, optimum: 18f, tolerance: 5f), 5); + } + + [Fact] + public void Gaussian_IsSymmetricAroundOptimum() + { + var below = Suitability.Gaussian(13f, optimum: 18f, tolerance: 5f); + var above = Suitability.Gaussian(23f, optimum: 18f, tolerance: 5f); + Assert.Equal(below, above, 5); + } + + [Fact] + public void Gaussian_AtOneToleranceAway_IsAboutPoint607() + { + Assert.Equal(MathF.Exp(-0.5f), Suitability.Gaussian(23f, 18f, 5f), 5); + } + + [Fact] + public void Gaussian_FallsOffWithDistance() + { + var near = Suitability.Gaussian(20f, 18f, 5f); + var far = Suitability.Gaussian(30f, 18f, 5f); + Assert.True(far < near && far > 0f); + } + + [Fact] + public void Gaussian_StaysWithinUnitRange() + { + for (var v = -50f; v <= 50f; v += 1f) + { + Assert.InRange(Suitability.Gaussian(v, 0f, 7f), 0f, 1f); + } + } + + [Fact] + public void Gaussian_NonPositiveTolerance_IsExactMatch() + { + Assert.Equal(1f, Suitability.Gaussian(5f, 5f, 0f)); + Assert.Equal(0f, Suitability.Gaussian(6f, 5f, 0f)); + } +}