Scale unique answer tracking
This commit is contained in:
@@ -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() =>
|
||||
|
||||
Reference in New Issue
Block a user