116 lines
4.7 KiB
C#
116 lines
4.7 KiB
C#
namespace MrGameEng.AI;
|
||
|
||
/// <summary>Shape of a <see cref="ResponseCurve"/> mapping a normalized input to a utility.</summary>
|
||
public enum CurveType
|
||
{
|
||
/// <summary>Straight line: <c>y = slope·(x − xShift) + yShift</c>.</summary>
|
||
Linear,
|
||
|
||
/// <summary>Power curve: <c>y = slope·(x − xShift)^exponent + yShift</c>; the exponent eases in/out.</summary>
|
||
Polynomial,
|
||
|
||
/// <summary>S-shaped logistic centred on <c>xShift</c>; <c>exponent</c> is the steepness.</summary>
|
||
Logistic,
|
||
|
||
/// <summary>Hermite smoothstep over <c>[xShift, xShift + 1/slope]</c>; flat ends, smooth middle.</summary>
|
||
SmoothStep,
|
||
}
|
||
|
||
/// <summary>
|
||
/// Maps a normalized input in <c>[0,1]</c> to a utility in <c>[0,1]</c> through one of a few
|
||
/// shapes. The input is clamped before evaluation and the output is clamped after, so a curve is
|
||
/// always safe to feed a raw normalized <see cref="Consideration{TContext}"/> value. Curves are
|
||
/// immutable value types — build them once and reuse them across evaluations.
|
||
/// </summary>
|
||
public readonly struct ResponseCurve
|
||
{
|
||
/// <summary>The shape applied by <see cref="Evaluate"/>.</summary>
|
||
public CurveType Type { get; }
|
||
|
||
/// <summary>Vertical scale / steepness (the <c>m</c> term). See <see cref="CurveType"/> per shape.</summary>
|
||
public float Slope { get; }
|
||
|
||
/// <summary>Power for <see cref="CurveType.Polynomial"/> and steepness for <see cref="CurveType.Logistic"/>.</summary>
|
||
public float Exponent { get; }
|
||
|
||
/// <summary>Horizontal shift of the curve (the <c>c</c> term): the input value mapped to the origin.</summary>
|
||
public float XShift { get; }
|
||
|
||
/// <summary>Vertical shift of the curve (the <c>b</c> term) added after scaling.</summary>
|
||
public float YShift { get; }
|
||
|
||
/// <summary>
|
||
/// Builds a curve from raw parameters. Prefer the named factories
|
||
/// (<see cref="Linear"/>, <see cref="Polynomial"/>, <see cref="Logistic"/>, <see cref="SmoothStep"/>)
|
||
/// which document the meaning of each term for their shape.
|
||
/// </summary>
|
||
public ResponseCurve(
|
||
CurveType type,
|
||
float slope = 1f,
|
||
float exponent = 1f,
|
||
float xShift = 0f,
|
||
float yShift = 0f
|
||
)
|
||
{
|
||
Type = type;
|
||
Slope = slope;
|
||
Exponent = exponent;
|
||
XShift = xShift;
|
||
YShift = yShift;
|
||
}
|
||
|
||
/// <summary>The identity curve: <c>y = x</c>. The default when a consideration needs no shaping.</summary>
|
||
public static ResponseCurve Identity => new(CurveType.Linear);
|
||
|
||
/// <summary>Straight line <c>y = slope·(x − xShift) + yShift</c>. A negative slope inverts the input.</summary>
|
||
public static ResponseCurve Linear(float slope = 1f, float xShift = 0f, float yShift = 0f) =>
|
||
new(CurveType.Linear, slope, 1f, xShift, yShift);
|
||
|
||
/// <summary>
|
||
/// Power curve <c>y = slope·(x − xShift)^exponent + yShift</c>. An exponent above 1 eases in
|
||
/// (slow start), below 1 eases out (fast start). Quadratic is <c>exponent = 2</c>.
|
||
/// </summary>
|
||
public static ResponseCurve Polynomial(
|
||
float exponent,
|
||
float slope = 1f,
|
||
float xShift = 0f,
|
||
float yShift = 0f
|
||
) => new(CurveType.Polynomial, slope, exponent, xShift, yShift);
|
||
|
||
/// <summary>
|
||
/// Logistic S-curve centred on <paramref name="midpoint"/>; <paramref name="steepness"/> controls how
|
||
/// sharp the transition is (≈10 gives a soft threshold, larger is more switch-like).
|
||
/// </summary>
|
||
public static ResponseCurve Logistic(float steepness = 10f, float midpoint = 0.5f) =>
|
||
new(CurveType.Logistic, 1f, steepness, midpoint);
|
||
|
||
/// <summary>
|
||
/// Hermite smoothstep rising from 0 to 1 over <c>[xShift, xShift + 1/slope]</c>: flat below the
|
||
/// start, flat above the end, smooth in between. Default rises across the whole <c>[0,1]</c> range.
|
||
/// </summary>
|
||
public static ResponseCurve SmoothStep(float slope = 1f, float xShift = 0f) =>
|
||
new(CurveType.SmoothStep, slope, 1f, xShift);
|
||
|
||
/// <summary>Evaluates the curve. <paramref name="x"/> is clamped to <c>[0,1]</c>; the result is clamped to <c>[0,1]</c>.</summary>
|
||
public float Evaluate(float x)
|
||
{
|
||
x = Math.Clamp(x, 0f, 1f);
|
||
var y = Type switch
|
||
{
|
||
CurveType.Linear => Slope * (x - XShift) + YShift,
|
||
CurveType.Polynomial => Slope * MathF.Pow(x - XShift, Exponent) + YShift,
|
||
CurveType.Logistic => 1f / (1f + MathF.Exp(-Exponent * (x - XShift))) * Slope + YShift,
|
||
CurveType.SmoothStep => SmoothStepValue(x),
|
||
_ => x,
|
||
};
|
||
|
||
return Math.Clamp(y, 0f, 1f);
|
||
}
|
||
|
||
private float SmoothStepValue(float x)
|
||
{
|
||
var t = Math.Clamp((x - XShift) * Slope, 0f, 1f);
|
||
return t * t * (3f - 2f * t) + YShift;
|
||
}
|
||
}
|