From d360093be1a9352fd2f531dde620a400c83a5d71 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 04:34:32 +0300 Subject: [PATCH] Add Trapezoidal suitability function and corresponding tests Introduced a new Trapezoid method in the Suitability class to model suitability with a hard tolerance band and a plateau. The method returns suitability values based on defined limits and optimal ranges. Added comprehensive unit tests to validate the functionality, including edge cases for flat plateaus, linear ramps, and hard limits. Co-Authored-By: Claude Opus 4.8 --- src/MrGameEng.Simulation/AI/Suitability.cs | 37 +++++++++++++++++ .../AI/SuitabilityTests.cs | 41 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/MrGameEng.Simulation/AI/Suitability.cs b/src/MrGameEng.Simulation/AI/Suitability.cs index 557a164..4e60e96 100644 --- a/src/MrGameEng.Simulation/AI/Suitability.cs +++ b/src/MrGameEng.Simulation/AI/Suitability.cs @@ -23,4 +23,41 @@ public static class Suitability var z = (value - optimum) / tolerance; return MathF.Exp(-0.5f * z * z); } + + /// + /// Trapezoidal suitability in [0, 1]: 0 at or beyond the hard limits + /// /, ramping linearly up to 1 at + /// , flat 1 across the comfortable plateau to + /// , then ramping back down to 0 at . + /// Models a hard tolerance band with a plateau (RimWorld-style plant growth vs. temperature: + /// dormant below or above ). A degenerate edge + /// ( or ≥ + /// ) becomes a hard step on that side. The four bounds are expected + /// ordered (min ≤ optimalLow ≤ optimalHigh ≤ max); pass ordered values. + /// + public static float Trapezoid( + float value, + float min, + float optimalLow, + float optimalHigh, + float max + ) + { + if (value <= min || value >= max) + { + return 0f; + } + + if (value < optimalLow) + { + return optimalLow > min ? (value - min) / (optimalLow - min) : 1f; + } + + if (value > optimalHigh) + { + return max > optimalHigh ? (max - value) / (max - optimalHigh) : 1f; + } + + return 1f; + } } diff --git a/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs b/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs index d61fe99..cc50cbd 100644 --- a/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs +++ b/tests/MrGameEng.Simulation.Tests/AI/SuitabilityTests.cs @@ -48,4 +48,45 @@ public class SuitabilityTests Assert.Equal(1f, Suitability.Gaussian(5f, 5f, 0f)); Assert.Equal(0f, Suitability.Gaussian(6f, 5f, 0f)); } + + [Fact] + public void Trapezoid_IsFlatAcrossThePlateau() + { + Assert.Equal(1f, Suitability.Trapezoid(10f, 0f, 10f, 42f, 58f), 5); + Assert.Equal(1f, Suitability.Trapezoid(25f, 0f, 10f, 42f, 58f), 5); + Assert.Equal(1f, Suitability.Trapezoid(42f, 0f, 10f, 42f, 58f), 5); + } + + [Fact] + public void Trapezoid_RampsLinearlyOnEachShoulder() + { + Assert.Equal(0.5f, Suitability.Trapezoid(5f, 0f, 10f, 42f, 58f), 5); // halfway up the cold ramp + Assert.Equal(0.5f, Suitability.Trapezoid(50f, 0f, 10f, 42f, 58f), 5); // halfway down the heat ramp + } + + [Fact] + public void Trapezoid_IsZeroAtAndBeyondTheHardLimits() + { + Assert.Equal(0f, Suitability.Trapezoid(0f, 0f, 10f, 42f, 58f)); + Assert.Equal(0f, Suitability.Trapezoid(-5f, 0f, 10f, 42f, 58f)); + Assert.Equal(0f, Suitability.Trapezoid(58f, 0f, 10f, 42f, 58f)); + Assert.Equal(0f, Suitability.Trapezoid(70f, 0f, 10f, 42f, 58f)); + } + + [Fact] + public void Trapezoid_DegenerateShoulder_IsAHardStep() + { + // optimalLow == min: full suitability immediately above the lower limit, zero at/below it. + Assert.Equal(1f, Suitability.Trapezoid(5f, 0f, 0f, 10f, 20f)); + Assert.Equal(0f, Suitability.Trapezoid(0f, 0f, 0f, 10f, 20f)); + } + + [Fact] + public void Trapezoid_StaysWithinUnitRange() + { + for (var v = -50f; v <= 80f; v += 1f) + { + Assert.InRange(Suitability.Trapezoid(v, 0f, 10f, 42f, 58f), 0f, 1f); + } + } }