using System; using Content.Server.AI.WorldState; using Content.Server.AI.WorldState.States.Utility; using JetBrains.Annotations; using Robust.Shared.Maths; namespace Content.Server.AI.Utility.Considerations { public abstract class Consideration { protected abstract float GetScore(Blackboard context); private float GetAdjustedScore(Blackboard context) { var score = GetScore(context); /* * Now using the geometric mean * for n scores you take the n-th root of the scores multiplied * e.g. a, b, c scores you take Math.Pow(a * b * c, 1/3) * To get the ACTUAL geometric mean at any one stage you'd need to divide by the running consideration count * however, the downside to this is it will fluctuate up and down over time. * For our purposes if we go below the minimum threshold we want to cut it off, thus we take a * "running geometric mean" which can only ever go down (and by the final value will equal the actual geometric mean). */ // Previously we used a makeupvalue method although the geometric mean is less punishing for more considerations var considerationsCount = context.GetState().GetValue(); var adjustedScore = MathF.Pow(score, 1 / (float) considerationsCount); return FloatMath.Clamp(adjustedScore, 0.0f, 1.0f); } [Pure] private static float BoolCurve(float x) { // ReSharper disable once CompareOfFloatsByEqualityOperator return x > 0.0f ? 1.0f : 0.0f; } public Func BoolCurve(Blackboard context) { float Result() { var adjustedScore = GetAdjustedScore(context); return BoolCurve(adjustedScore); } return Result; } [Pure] private static float InverseBoolCurve(float x) { // ReSharper disable once CompareOfFloatsByEqualityOperator return x == 0.0f ? 1.0f : 0.0f; } public Func InverseBoolCurve(Blackboard context) { float Result() { var adjustedScore = GetAdjustedScore(context); return InverseBoolCurve(adjustedScore); } return Result; } [Pure] private static float LogisticCurve(float x, float slope, float exponent, float yOffset, float xOffset) { return MathHelper.Clamp( exponent * (1 / (1 + (float) Math.Pow(Math.Log(1000) * slope, -1 * x + xOffset))) + yOffset, 0.0f, 1.0f); } public Func LogisticCurve(Blackboard context, float slope, float exponent, float yOffset, float xOffset) { float Result() { var adjustedScore = GetAdjustedScore(context); return LogisticCurve(adjustedScore, slope, exponent, yOffset, xOffset); } return Result; } [Pure] private static float QuadraticCurve(float x, float slope, float exponent, float yOffset, float xOffset) { return MathHelper.Clamp(slope * (float) Math.Pow(x - xOffset, exponent) + yOffset, 0.0f, 1.0f); } public Func QuadraticCurve(Blackboard context, float slope, float exponent, float yOffset, float xOffset) { float Result() { var adjustedScore = GetAdjustedScore(context); return QuadraticCurve(adjustedScore, slope, exponent, yOffset, xOffset); } return Result; } /// /// For any curves that are re-used across actions so you only need to update it once. /// /// /// /// /// public Func PresetCurve(Blackboard context, PresetCurve preset) { float Result() { var adjustedScore = GetAdjustedScore(context); switch (preset) { case Considerations.PresetCurve.Distance: return QuadraticCurve(adjustedScore, -1.0f, 1.0f, 1.0f, 0.02f); case Considerations.PresetCurve.Nutrition: return QuadraticCurve(adjustedScore, 2.0f, 1.0f, -1.0f, -0.2f); case Considerations.PresetCurve.TargetHealth: return QuadraticCurve(adjustedScore, 1.0f, 0.4f, 0.0f, -0.02f); default: throw new ArgumentOutOfRangeException(nameof(preset), preset, null); } } return Result; } } /// /// Preset response curves for considerations /// public enum PresetCurve { Distance, Nutrition, TargetHealth, } }