Skip to content

Commit f4f12a2

Browse files
committed
Start caching high-score ranks, implement GetScoresByUser DB method
1 parent 54e1f4b commit f4f12a2

4 files changed

Lines changed: 83 additions & 8 deletions

File tree

Refresh.Database/GameDatabaseContext.Leaderboard.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public GameScore SubmitScore(ISerializedScore score, GameUser user, GameLevel le
5858
this.GameScores.Add(newScore);
5959
});
6060

61+
this.RecalculateScoreStatistics(level.LevelId, score.ScoreType);
6162
this.CreateLevelScoreEvent(user, newScore);
6263

6364
// Notify the last #1 users that they've been overtaken
@@ -101,12 +102,12 @@ public DatabaseScoreList GetTopScoresForLevel(GameLevel level, int count, int sk
101102
scores = scores.Where(s => s.ScoreType == scoreType);
102103

103104
if (!showDuplicates)
104-
scores = scores.DistinctBy(s => s.PublisherId);
105+
scores = scores.Where(s => s.Rank != 0);
105106

106107
if (minAge != null)
107108
scores = scores.Where(s => s.ScoreSubmitted >= minAge);
108109

109-
return new(scores.ToArray().Select((s, i) => new ScoreWithRank(s, i + 1)), skip, count, user);
110+
return new(scores.ToArray().Select(s => new ScoreWithRank(s, s.Rank)), skip, count, user);
110111
}
111112

112113
public DatabaseScoreList GetRankedScoresAroundScore(GameScore score, int count, GameUser? user = null)
@@ -118,13 +119,12 @@ public DatabaseScoreList GetRankedScoresAroundScore(GameScore score, int count,
118119
List<GameScore> scores = this.GameScoresIncluded
119120
.Where(s => s.ScoreType == score.ScoreType && s.LevelId == score.LevelId)
120121
.OrderByDescending(s => s.Score)
121-
.ToArray()
122-
.DistinctBy(s => s.PublisherId)
122+
.Where(s => s.Rank != 0)
123123
.ToList();
124124

125125
return new
126126
(
127-
scores.Select((s, i) => new ScoreWithRank(s, i + 1)),
127+
scores.Select(s => new ScoreWithRank(s, s.Rank)),
128128
Math.Min(scores.Count, scores.IndexOf(score) - count / 2), // center user's score around other scores
129129
count, user
130130
);
@@ -140,8 +140,7 @@ public DatabaseScoreList GetLevelTopScoresByFriends(GameUser user, GameLevel lev
140140
IEnumerable<GameScore> scores = this.GameScoresIncluded
141141
.Where(s => s.LevelId == level.LevelId)
142142
.OrderByDescending(s => s.Score)
143-
.ToArray()
144-
.DistinctBy(s => s.PublisherId)
143+
.Where(s => s.Rank != 0)
145144
//TODO: THIS CALL IS EXTREMELY INEFFECIENT!!! once we are in postgres land, figure out a way to do this effeciently
146145
.Where(s => s.PlayerIds.Any(p => mutuals.Contains(p)));
147146

@@ -151,7 +150,22 @@ public DatabaseScoreList GetLevelTopScoresByFriends(GameUser user, GameLevel lev
151150
if (minAge != null)
152151
scores = scores.Where(s => s.ScoreSubmitted >= minAge);
153152

154-
return new(scores.Select((s, i) => new ScoreWithRank(s, i + 1)), skip, count, user);
153+
return new(scores.ToArray().Select(s => new ScoreWithRank(s, s.Rank)), skip, count, user);
154+
}
155+
156+
/// <param name="scoreType">0 = don't filter by type</param>
157+
/// <param name="rank">0 = show all high-scores regardless of rank</param>
158+
public DatabaseList<ScoreWithRank> GetScoresByUser(GameUser user, int skip, int count, byte scoreType, int rank)
159+
{
160+
IQueryable<GameScore> scores = this.GameScoresIncluded
161+
.Where(s => s.PublisherId == user.UserId)
162+
.Where(s => s.Rank != 0)
163+
.OrderBy(s => s.Rank);
164+
165+
if (scoreType != 0) scores = scores.Where(s => s.ScoreType == scoreType);
166+
if (rank != 0) scores = scores.Where(s => s.Rank == rank);
167+
168+
return new(scores.ToArray().Select(s => new ScoreWithRank(s, s.Rank)), skip, count);
155169
}
156170

157171
[Pure]

Refresh.Database/GameDatabaseContext.Statistics.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System.Diagnostics;
22
using System.Diagnostics.CodeAnalysis;
3+
using Refresh.Common;
34
using Refresh.Common.Constants;
45
using Refresh.Database.Models.Comments;
56
using Refresh.Database.Models.Levels;
7+
using Refresh.Database.Models.Levels.Scores;
68
using Refresh.Database.Models.Playlists;
79
using Refresh.Database.Models.Statistics;
810
using Refresh.Database.Models.Users;
@@ -214,6 +216,56 @@ private void MarkLevelStatisticsDirty(GameLevel level)
214216
}
215217
#endregion
216218

219+
#region Level Scores
220+
221+
public void RecalculateScoreStatistics(int levelId, byte scoreType, bool saveChanges = true)
222+
{
223+
List<GameScore> scores = this.GameScores
224+
#if DEBUG
225+
.Include(s => s.Publisher)
226+
#endif
227+
.Where(s => s.LevelId == levelId && s.ScoreType == scoreType)
228+
.OrderByDescending(s => s.Score)
229+
.ToList();
230+
231+
// Step 1: Reset all ranks
232+
foreach (GameScore score in scores)
233+
{
234+
this._logger.LogDebug(RefreshContext.Database, $"Reset {score.ScoreId} ({score.Score} by {score.Publisher}) rank to 0");
235+
score.Rank = 0;
236+
}
237+
238+
// Step 2: Assign ranks to high scores
239+
// Exclude all scores which are not high scores now
240+
int rank = 1;
241+
int? previousScore = null;
242+
foreach (GameScore score in scores.DistinctBy(s => s.PublisherId).ToList())
243+
{
244+
// Scores with the same value should have the same rank.
245+
// Increment rank if there was a previous score and it was higher.
246+
if (previousScore != null && previousScore > score.Score)
247+
rank++;
248+
249+
score.Rank = rank;
250+
this._logger.LogDebug(RefreshContext.Database, $"Set {score.ScoreId} ({score.Score} by {score.Publisher}) rank to {rank}");
251+
252+
previousScore = score.Score;
253+
}
254+
255+
this.SaveChanges();
256+
}
257+
258+
public void RecalculateScoreStatistics(GameLevel level)
259+
{
260+
this.RecalculateScoreStatistics(level.LevelId, 1, false);
261+
this.RecalculateScoreStatistics(level.LevelId, 2, false);
262+
this.RecalculateScoreStatistics(level.LevelId, 3, false);
263+
this.RecalculateScoreStatistics(level.LevelId, 4, false);
264+
this.SaveChanges();
265+
}
266+
267+
#endregion
268+
217269
#region Users
218270

219271
internal const int UserStatisticsVersion = 2;

Refresh.Database/Models/Levels/Scores/GameScore.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,11 @@ public partial class GameScore
3030
/// </summary>
3131
[ForeignKey(nameof(PublisherId)), Required] public GameUser Publisher { get; set; }
3232
[Required] public ObjectId PublisherId { get; set; }
33+
34+
/// <summary>
35+
/// The rank of this score among all scores for the level and score type. Not stored in a separate
36+
/// entity as that would add unnecessary complexity for now (this is the only stat so far).
37+
/// Scores which are no longer the high score of their publisher have a rank of 0.
38+
/// </summary>
39+
public int Rank { get; set; }
3340
}

Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameScoreResponse.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public class ApiGameScoreResponse : IApiResponse, IDataConvertableFrom<ApiGameSc
1414
public required ApiMinimalUserResponse Publisher { get; set; }
1515
public required DateTimeOffset ScoreSubmitted { get; set; }
1616
public required int Score { get; set; }
17+
public required int Rank { get; set; }
1718
public required byte ScoreType { get; set; }
1819

1920
public required TokenGame Game { get; set; }
@@ -32,6 +33,7 @@ public class ApiGameScoreResponse : IApiResponse, IDataConvertableFrom<ApiGameSc
3233
ScoreSubmitted = old.ScoreSubmitted,
3334
Score = old.Score,
3435
ScoreType = old.ScoreType,
36+
Rank = old.Rank,
3537
Game = old.Game,
3638
Platform = old.Platform,
3739
};

0 commit comments

Comments
 (0)