Add Suitability.Gaussian for optimum-tolerance bell curves
CI / build-test (push) Successful in 1m28s

A small AI helper computing how well a value matches a preferred optimum: a
Gaussian bell in [0,1], 1 at the optimum, e^-0.5 one tolerance away, over an
arbitrary input scale (unlike ResponseCurve's monotonic [0,1] shaping). The
building block for environment-suitability growth. Covered by tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-12 17:52:11 +03:00
co-authored by Claude Opus 4.8
parent a684c23cd9
commit d0104df304
2 changed files with 77 additions and 0 deletions
@@ -0,0 +1,26 @@
namespace MrGameEng.AI;
/// <summary>
/// Suitability curves: how well an environmental value matches a preferred optimum. Unlike
/// <see cref="ResponseCurve"/> (monotonic shaping over <c>[0,1]</c>), 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.
/// </summary>
public static class Suitability
{
/// <summary>
/// Gaussian bell in <c>[0, 1]</c>: 1 when <paramref name="value"/> equals <paramref name="optimum"/>,
/// falling off as it departs, reaching <c>e^-0.5 ≈ 0.607</c> one <paramref name="tolerance"/> away.
/// A non-positive <paramref name="tolerance"/> degenerates to an exact match (1 at the optimum, else 0).
/// </summary>
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);
}
}
@@ -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));
}
}