using QuestionsApi.Models; namespace QuestionsApi.Services; public static class DynamicDifficultyPolicy { public const int MinimumElo = 600; public const int MaximumElo = 1800; public const int TargetOffset = 70; public static readonly IReadOnlyDictionary TierElos = new Dictionary(StringComparer.Ordinal) { ["foundation"] = 850, ["standard"] = 1050, ["advanced"] = 1250, ["expert"] = 1450 }; public static int EffectiveAbility( int overallElo, int categoryElo, DynamicObjectiveMastery? objective) { var bankAbility = (int)Math.Round((overallElo + categoryElo) / 2.0); if (objective is null || objective.Attempts <= 0) return Clamp(bankAbility); var objectiveWeight = Math.Min(0.8, objective.Attempts * 0.08); return Clamp((int)Math.Round(bankAbility * (1.0 - objectiveWeight) + objective.Elo * objectiveWeight)); } public static int TargetFamilyElo(int effectiveAbility) => Clamp(effectiveAbility - TargetOffset); public static double ExpectedScore(int playerElo, int familyElo) => 1.0 / (1.0 + Math.Pow(10, (familyElo - playerElo) / 400.0)); public static int LearnerDelta(int playerElo, int familyElo, bool correct, int attempts) => (int)Math.Round((attempts < 5 ? 40 : 24) * ((correct ? 1.0 : 0.0) - ExpectedScore(playerElo, familyElo))); public static int FamilyDelta(int playerElo, int familyElo, bool correct, int attempts) => (int)Math.Round((attempts < 20 ? 32 : 24) * (ExpectedScore(playerElo, familyElo) - (correct ? 1.0 : 0.0))); public static DynamicCaseFamily SelectFamily( IReadOnlyList families, IReadOnlyDictionary ratings, int targetElo, ISet recentFamilyIds, int tieBreaker) { if (families.Count == 0) throw new InvalidOperationException("No case families are available."); var eligible = families.Where(value => !recentFamilyIds.Contains(value.Id)).ToList(); if (eligible.Count == 0) eligible = families.ToList(); var ranked = eligible .Select(value => new { Family = value, Elo = ratings.TryGetValue(value.Id, out var rating) ? rating.Elo : value.NominalElo }) .OrderBy(value => Math.Abs(value.Elo - targetElo)) .ThenBy(value => value.Family.Id, StringComparer.Ordinal) .ToList(); var bestDistance = Math.Abs(ranked[0].Elo - targetElo); var tied = ranked.Where(value => Math.Abs(value.Elo - targetElo) == bestDistance).ToList(); return tied[Math.Abs(tieBreaker) % tied.Count].Family; } public static int Clamp(int elo) => Math.Clamp(elo, MinimumElo, MaximumElo); }