Scale unique answer tracking

This commit is contained in:
hz
2026-07-13 02:11:54 -05:00
parent 700da4e415
commit f2c59388eb
23 changed files with 1064 additions and 131 deletions
+82 -43
View File
@@ -11,14 +11,10 @@ public class RatingStore
private readonly IMongoCollection<UserRating> _users;
private readonly IMongoCollection<QuestionRating> _questions;
public RatingStore(IConfiguration cfg)
public RatingStore(MongoDatabaseProvider mongo)
{
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
var client = new MongoClient(cs);
var db = client.GetDatabase(dbName);
_users = db.GetCollection<UserRating>("UserRatings");
_questions = db.GetCollection<QuestionRating>("QuestionRatings");
_users = mongo.Database.GetCollection<UserRating>("UserRatings");
_questions = mongo.Database.GetCollection<QuestionRating>("QuestionRatings");
_users.Indexes.CreateOne(new CreateIndexModel<UserRating>(
Builders<UserRating>.IndexKeys.Ascending(r => r.UserId),
@@ -89,64 +85,107 @@ public class RatingStore
var categoryDelta = (int)Math.Round(userK * (actual - categoryExpected));
var questionDelta = (int)Math.Round(questionK * (overallExpected - actual));
userRating.OverallElo = Math.Max(MinElo, oldOverall + overallDelta);
userRating.CategoryElos[category] = Math.Max(MinElo, oldCategory + categoryDelta);
userRating.RatedQuestionCount++;
userRating.UpdatedUtc = DateTime.UtcNow;
var now = DateTime.UtcNow;
var categoryPath = $"CategoryElos.{category}";
var userUpdate = Builders<UserRating>.Update
.Inc(rating => rating.OverallElo, overallDelta)
.Inc(categoryPath, categoryDelta)
.Inc(rating => rating.RatedQuestionCount, 1)
.Set(rating => rating.UpdatedUtc, now);
await _users.UpdateOneAsync(rating => rating.UserId == userId, userUpdate);
await _users.UpdateOneAsync(
rating => rating.UserId == userId,
Builders<UserRating>.Update
.Max(rating => rating.OverallElo, MinElo)
.Max(categoryPath, MinElo));
questionRating.Elo = Math.Max(MinElo, oldQuestion + questionDelta);
questionRating.AttemptCount++;
questionRating.UpdatedUtc = DateTime.UtcNow;
var questionUpdate = Builders<QuestionRating>.Update
.Inc(rating => rating.Elo, questionDelta)
.Inc(rating => rating.AttemptCount, 1)
.Set(rating => rating.UpdatedUtc, now);
await _questions.UpdateOneAsync(rating => rating.QuestionExternalId == question.ExternalId, questionUpdate);
await _questions.UpdateOneAsync(
rating => rating.QuestionExternalId == question.ExternalId,
Builders<QuestionRating>.Update.Max(rating => rating.Elo, MinElo));
await _users.ReplaceOneAsync(r => r.UserId == userRating.UserId, userRating, new ReplaceOptions { IsUpsert = true });
await _questions.ReplaceOneAsync(r => r.QuestionExternalId == questionRating.QuestionExternalId, questionRating, new ReplaceOptions { IsUpsert = true });
var updatedUser = await _users.Find(rating => rating.UserId == userId).FirstAsync();
var updatedQuestion = await _questions.Find(rating => rating.QuestionExternalId == question.ExternalId).FirstAsync();
return new RatingChangeResponse
{
Rated = true,
Category = category,
UserOverallElo = userRating.OverallElo,
UserOverallDelta = userRating.OverallElo - oldOverall,
UserCategoryElo = userRating.CategoryElos[category],
UserCategoryDelta = userRating.CategoryElos[category] - oldCategory,
QuestionElo = questionRating.Elo,
QuestionEloDelta = questionRating.Elo - oldQuestion
UserOverallElo = updatedUser.OverallElo,
UserOverallDelta = overallDelta,
UserCategoryElo = updatedUser.CategoryElos.GetValueOrDefault(category, BaseElo),
UserCategoryDelta = categoryDelta,
QuestionElo = updatedQuestion.Elo,
QuestionEloDelta = questionDelta
};
}
private async Task<UserRating> GetOrCreateUserRatingAsync(string userId)
{
var rating = await _users.Find(r => r.UserId == userId).FirstOrDefaultAsync();
if (rating is null)
var create = Builders<UserRating>.Update
.SetOnInsert(rating => rating.UserId, userId)
.SetOnInsert(rating => rating.OverallElo, BaseElo)
.SetOnInsert(rating => rating.RatedQuestionCount, 0)
.SetOnInsert(rating => rating.CategoryElos, DefaultCategoryElos())
.SetOnInsert(rating => rating.UpdatedUtc, DateTime.UtcNow);
UserRating rating;
try
{
rating = new UserRating { UserId = userId, CategoryElos = DefaultCategoryElos() };
await _users.InsertOneAsync(rating);
return rating;
rating = await _users.FindOneAndUpdateAsync(
item => item.UserId == userId,
create,
new FindOneAndUpdateOptions<UserRating>
{
IsUpsert = true,
ReturnDocument = ReturnDocument.After
});
}
catch (MongoWriteException exception) when (exception.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
rating = await _users.Find(item => item.UserId == userId).FirstAsync();
}
var changed = false;
foreach (var category in RatingCategory.Categories)
var missingCategories = RatingCategory.Categories
.Where(category => !rating.CategoryElos.ContainsKey(category))
.ToList();
if (missingCategories.Count > 0)
{
if (!rating.CategoryElos.ContainsKey(category))
{
rating.CategoryElos[category] = BaseElo;
changed = true;
}
var updates = missingCategories
.Select(category => Builders<UserRating>.Update.Set($"CategoryElos.{category}", BaseElo))
.ToList();
if (updates.Count > 0)
await _users.UpdateOneAsync(item => item.UserId == userId, Builders<UserRating>.Update.Combine(updates));
rating = await _users.Find(item => item.UserId == userId).FirstAsync();
}
if (changed)
await _users.ReplaceOneAsync(r => r.UserId == userId, rating);
return rating;
}
private async Task<QuestionRating> GetOrCreateQuestionRatingAsync(string questionExternalId)
{
var rating = await _questions.Find(r => r.QuestionExternalId == questionExternalId).FirstOrDefaultAsync();
if (rating is not null)
return rating;
rating = new QuestionRating { QuestionExternalId = questionExternalId };
await _questions.InsertOneAsync(rating);
return rating;
var create = Builders<QuestionRating>.Update
.SetOnInsert(rating => rating.QuestionExternalId, questionExternalId)
.SetOnInsert(rating => rating.Elo, BaseElo)
.SetOnInsert(rating => rating.AttemptCount, 0)
.SetOnInsert(rating => rating.UpdatedUtc, DateTime.UtcNow);
try
{
return await _questions.FindOneAndUpdateAsync(
rating => rating.QuestionExternalId == questionExternalId,
create,
new FindOneAndUpdateOptions<QuestionRating>
{
IsUpsert = true,
ReturnDocument = ReturnDocument.After
});
}
catch (MongoWriteException exception) when (exception.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
return await _questions.Find(rating => rating.QuestionExternalId == questionExternalId).FirstAsync();
}
}
private static Dictionary<string, int> DefaultCategoryElos() =>