namespace MrGameEng.AI; /// /// A utility reasoner over a fixed set of s. Each evaluation /// scores every action for the given context and picks one. Scoring writes into a buffer owned by the /// reasoner, so repeated evaluations allocate nothing; keep one instance per agent kind (or one per /// system, reused across agents) and pass each agent's context in. Not thread-safe: the score buffer /// is shared between calls, so a single instance must not be evaluated from two threads at once. /// public sealed class UtilityAi { private readonly UtilityAction[] _actions; private readonly float[] _scores; /// Creates a reasoner choosing between (at least one required). /// is empty. public UtilityAi(params UtilityAction[] actions) { if (actions is null || actions.Length == 0) { throw new ArgumentException("A UtilityAi needs at least one action.", nameof(actions)); } _actions = actions; _scores = new float[actions.Length]; } /// The actions this reasoner chooses between, in evaluation order. public IReadOnlyList> Actions => _actions; /// /// The scores from the most recent / call, aligned /// with . Useful for debug overlays and console dumps. /// public ReadOnlySpan LastScores => _scores; /// /// Scores every action for and returns the highest, or null when no /// action scores strictly above . Ties resolve to the earliest action, /// so selection is fully deterministic for identical inputs. /// public UtilityAction? Select(TContext context, float threshold = 0f) { var best = -1; var bestScore = threshold; for (var i = 0; i < _actions.Length; i++) { var score = _actions[i].Score(context); _scores[i] = score; if (score > bestScore) { bestScore = score; best = i; } } return best >= 0 ? _actions[best] : null; } /// /// Scores every action and picks one at random in proportion to its score (roulette selection over /// the actions above ), giving believable variety while staying /// deterministic for a given sequence. Returns null when nothing /// qualifies. Pass a seeded owned by the calling system — never /// — to keep the simulation reproducible. /// public UtilityAction? SelectWeighted( TContext context, Random random, float threshold = 0f ) { var total = 0f; for (var i = 0; i < _actions.Length; i++) { var score = _actions[i].Score(context); _scores[i] = score; if (score > threshold) { total += score; } } if (total <= 0f) { return null; } var roll = (float)random.NextDouble() * total; for (var i = 0; i < _actions.Length; i++) { if (_scores[i] <= threshold) { continue; } roll -= _scores[i]; if (roll <= 0f) { return _actions[i]; } } // Floating-point slack can leave roll just above 0; fall back to the last qualifying action. for (var i = _actions.Length - 1; i >= 0; i--) { if (_scores[i] > threshold) { return _actions[i]; } } return null; } }