Adapt dynamic question difficulty to learner Elo
This commit is contained in:
@@ -8,15 +8,21 @@ public sealed class DynamicQuestionService
|
||||
private readonly DynamicQuestionStore _store;
|
||||
private readonly DynamicQuestionGenerator _generator;
|
||||
private readonly DynamicQuestionTokenService _tokens;
|
||||
private readonly RatingStore _ratings;
|
||||
private readonly ServiceMetrics _metrics;
|
||||
|
||||
public DynamicQuestionService(
|
||||
DynamicQuestionStore store,
|
||||
DynamicQuestionGenerator generator,
|
||||
DynamicQuestionTokenService tokens)
|
||||
DynamicQuestionTokenService tokens,
|
||||
RatingStore ratings,
|
||||
ServiceMetrics metrics)
|
||||
{
|
||||
_store = store;
|
||||
_generator = generator;
|
||||
_tokens = tokens;
|
||||
_ratings = ratings;
|
||||
_metrics = metrics;
|
||||
}
|
||||
|
||||
public Task<DynamicQuestionStatusResponse> StatusAsync(string userId) => _store.StatusAsync(userId);
|
||||
@@ -53,6 +59,30 @@ public sealed class DynamicQuestionService
|
||||
var blueprint = selectedObjective
|
||||
.OrderByDescending(value => value.Version)
|
||||
.First();
|
||||
var ratingProfile = await _ratings.GetProfileAsync(userId);
|
||||
mastery.TryGetValue(blueprint.ObjectiveId, out var objectiveMastery);
|
||||
var category = RatingCategory.Normalize(
|
||||
blueprint.PerformanceCategory, blueprint.System, blueprint.Discipline);
|
||||
var categoryElo = ratingProfile.CategoryElos.GetValueOrDefault(category, ratingProfile.OverallElo);
|
||||
var effectiveAbility = DynamicDifficultyPolicy.EffectiveAbility(
|
||||
ratingProfile.OverallElo, categoryElo, objectiveMastery);
|
||||
var targetElo = DynamicDifficultyPolicy.TargetFamilyElo(effectiveAbility);
|
||||
var familyRatings = await _store.FamilyRatingsAsync(blueprint);
|
||||
var blueprintFamilyIds = blueprint.CaseFamilies.Select(value => value.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var recentForObjective = run.RecentCaseFamilyIds
|
||||
.Where(blueprintFamilyIds.Contains)
|
||||
.TakeLast(Math.Max(1, blueprint.CaseFamilies.Count / 4))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var selectedFamily = DynamicDifficultyPolicy.SelectFamily(
|
||||
blueprint.CaseFamilies,
|
||||
familyRatings,
|
||||
targetElo,
|
||||
recentForObjective,
|
||||
RandomNumberGenerator.GetInt32(int.MaxValue));
|
||||
var selectedFamilyElo = familyRatings.TryGetValue(selectedFamily.Id, out var selectedRating)
|
||||
? selectedRating.Elo
|
||||
: selectedFamily.NominalElo;
|
||||
_metrics.DynamicSelected(selectedFamily.DifficultyTier, selectedFamilyElo - targetElo);
|
||||
|
||||
var claims = new DynamicGenerationClaims
|
||||
{
|
||||
@@ -63,20 +93,14 @@ public sealed class DynamicQuestionService
|
||||
Seed = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(),
|
||||
Nonce = Guid.NewGuid().ToString("N"),
|
||||
GeneratorVersion = DynamicQuestionGenerator.CurrentVersion,
|
||||
CaseFamilyId = selectedFamily.Id,
|
||||
DifficultyTier = selectedFamily.DifficultyTier,
|
||||
EffectiveAbilityElo = effectiveAbility,
|
||||
FamilyElo = selectedFamilyElo,
|
||||
ExpiresUtc = DateTime.UtcNow.AddDays(7)
|
||||
};
|
||||
var generated = _generator.Generate(blueprint, claims.Seed, claims.Nonce, claims.GeneratorVersion);
|
||||
var familyIds = blueprint.CaseFamilies.Select(value => value.Id).ToHashSet(StringComparer.Ordinal);
|
||||
var recentForObjective = run.RecentCaseFamilyIds
|
||||
.Where(familyIds.Contains)
|
||||
.TakeLast(Math.Max(1, blueprint.CaseFamilies.Count - 1))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
for (var retry = 0; retry < 20 && recentForObjective.Contains(generated.CaseFamilyId); retry++)
|
||||
{
|
||||
claims.Seed = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
|
||||
generated = _generator.Generate(blueprint, claims.Seed, claims.Nonce);
|
||||
}
|
||||
claims.CaseFamilyId = generated.CaseFamilyId;
|
||||
var generated = _generator.Generate(
|
||||
blueprint, claims.Seed, claims.Nonce, claims.GeneratorVersion, claims.CaseFamilyId);
|
||||
run.SeenObjectiveIds.Add(blueprint.ObjectiveId);
|
||||
run.PreviousObjectiveId = blueprint.ObjectiveId;
|
||||
run.RecentCaseFamilyIds.Add(generated.CaseFamilyId);
|
||||
@@ -84,7 +108,7 @@ public sealed class DynamicQuestionService
|
||||
run.ExpiresUtc = DateTime.UtcNow.AddDays(7);
|
||||
return new DynamicQuestionEnvelope
|
||||
{
|
||||
Question = QuestionResponse.FromModel(generated.Question),
|
||||
Question = QuestionResponse.FromModel(generated.Question, questionElo: selectedFamilyElo),
|
||||
ObjectiveId = blueprint.ObjectiveId,
|
||||
ObjectiveTitle = blueprint.Title,
|
||||
GenerationToken = _tokens.SignGeneration(claims),
|
||||
@@ -109,7 +133,8 @@ public sealed class DynamicQuestionService
|
||||
throw new InvalidOperationException("This generated question version is no longer supported.");
|
||||
var blueprint = await _store.GetAsync(claims.BlueprintExternalId, claims.BlueprintVersion)
|
||||
?? throw new KeyNotFoundException("The question blueprint is no longer available.");
|
||||
var generated = _generator.Generate(blueprint, claims.Seed, claims.Nonce, claims.GeneratorVersion);
|
||||
var generated = _generator.Generate(
|
||||
blueprint, claims.Seed, claims.Nonce, claims.GeneratorVersion, claims.CaseFamilyId);
|
||||
if (!string.IsNullOrWhiteSpace(claims.CaseFamilyId)
|
||||
&& claims.CaseFamilyId != generated.CaseFamilyId)
|
||||
throw new InvalidOperationException("Generated case family does not match its signed descriptor.");
|
||||
|
||||
Reference in New Issue
Block a user