70 lines
2.9 KiB
C#
70 lines
2.9 KiB
C#
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<string, int> TierElos =
|
|
new Dictionary<string, int>(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<DynamicCaseFamily> families,
|
|
IReadOnlyDictionary<string, DynamicFamilyRating> ratings,
|
|
int targetElo,
|
|
ISet<string> 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);
|
|
}
|