Skip to content

Commit

Permalink
Merge pull request #44 from Atralupus/feat/ranking
Browse files Browse the repository at this point in the history
Implement ranking system with redis sortedset
  • Loading branch information
Atralupus authored Jan 8, 2025
2 parents 7d8acbf + 9a0732c commit 4ffb0ed
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 37 deletions.
1 change: 1 addition & 0 deletions ArenaService/ArenaService.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="StrawberryShake.Server" Version="14.3.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion ArenaService/Controllers/BattleController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task<
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(UnauthorizedHttpResult), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(NotFound<string>), StatusCodes.Status404NotFound)]
public Results<UnauthorizedHttpResult, NotFound<string>, Ok<string>> ResultBattle(
public Results<UnauthorizedHttpResult, NotFound<string>, Ok<string>> RequestBattle(
string txId,
int logId
)
Expand Down
27 changes: 20 additions & 7 deletions ArenaService/Controllers/LeaderboardController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ public class LeaderboardController : ControllerBase
{
private readonly ILeaderboardRepository _leaderboardRepo;
private readonly IParticipantRepository _participantRepo;
private readonly IRankingRepository _rankingRepository;

public LeaderboardController(
ILeaderboardRepository leaderboardRepo,
IParticipantRepository participantRepo
IParticipantRepository participantRepo,
IRankingRepository rankingRepository
)
{
_leaderboardRepo = leaderboardRepo;
_participantRepo = participantRepo;
_rankingRepository = rankingRepository;
}

// [HttpGet]
Expand Down Expand Up @@ -51,15 +54,25 @@ string avatarAddress
return TypedResults.NotFound("Not participant user.");
}

var leaderboardEntry = await _leaderboardRepo.GetMyRankAsync(seasonId, participant.Id);
var rankingKey = $"ranking:season:{seasonId}";

if (leaderboardEntry == null)
var rank = await _rankingRepository.GetRankAsync(rankingKey, participant.Id);
var score = await _rankingRepository.GetScoreAsync(rankingKey, participant.Id);

if (rank is null || score is null)
{
return TypedResults.NotFound("No leaderboardEntry found.");
return TypedResults.NotFound("Not participant user.");
}

leaderboardEntry.Participant = participant;

return TypedResults.Ok(leaderboardEntry.ToResponse());
return TypedResults.Ok(
new LeaderboardEntryResponse
{
AvatarAddress = participant.AvatarAddress,
NameWithHash = participant.NameWithHash,
PortraitId = participant.PortraitId,
Rank = rank.Value,
TotalScore = score.Value,
}
);
}
}
24 changes: 0 additions & 24 deletions ArenaService/Extensions/LeaderboardExtensions.cs

This file was deleted.

3 changes: 1 addition & 2 deletions ArenaService/Models/LeaderboardEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace ArenaService.Models;

[Table("leaderboard")]
// score board
public class LeaderboardEntry
{
public int Id { get; set; }
Expand All @@ -16,7 +17,5 @@ public class LeaderboardEntry
public int SeasonId { get; set; }
public Season Season { get; set; } = null!;

[Required]
public int Rank { get; set; }
public int TotalScore { get; set; } = 1000;
}
4 changes: 3 additions & 1 deletion ArenaService/Options/RedisOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ public class RedisOptions
public const string SectionName = "Redis";
public string Host { get; set; } = "127.0.0.1";
public string Port { get; set; } = "6379";
public string Prefix { get; set; } = "arena_hangfire:";
public string HangfirePrefix { get; set; } = "arena_hangfire:";
public string HangfireDbNumber { get; set; } = "0";
public string RankingDbNumber { get; set; } = "1";
}
Empty file.
130 changes: 130 additions & 0 deletions ArenaService/Repositories/RankingRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using StackExchange.Redis;

namespace ArenaService.Repositories;

public interface IRankingRepository
{
Task UpdateScoreAsync(string leaderboardKey, int participantId, int scoreChange);

Task<int?> GetRankAsync(string leaderboardKey, int participantId);

Task<int?> GetScoreAsync(string leaderboardKey, int participantId);

Task<List<(int Rank, int ParticipantId, int Score)>> GetTopRankingsAsync(
string leaderboardKey,
int topN
);

Task<List<(int Rank, int ParticipantId, int Score)>> GetRankingsWithPaginationAsync(
string leaderboardKey,
int pageNumber,
int pageSize
);

Task SyncLeaderboardAsync(string leaderboardKey, List<(int ParticipantId, int Score)> entries);
}

public class RankingRepository : IRankingRepository
{
private readonly IDatabase _redis;

public RankingRepository(IConnectionMultiplexer redis)
{
_redis = redis.GetDatabase();
}

public async Task UpdateScoreAsync(string leaderboardKey, int participantId, int scoreChange)
{
await _redis.SortedSetIncrementAsync(
leaderboardKey,
$"participant:{participantId}",
scoreChange
);
}

public async Task<int?> GetRankAsync(string leaderboardKey, int participantId)
{
var rank = await _redis.SortedSetRankAsync(
leaderboardKey,
$"participant:{participantId}",
Order.Descending
);
return rank.HasValue ? (int)rank.Value + 1 : null;
}

public async Task<int?> GetScoreAsync(string leaderboardKey, int participantId)
{
var score = await _redis.SortedSetScoreAsync(
leaderboardKey,
$"participant:{participantId}"
);
return score.HasValue ? (int)score.Value : null;
}

public async Task<List<(int Rank, int ParticipantId, int Score)>> GetTopRankingsAsync(
string leaderboardKey,
int topN
)
{
var topRankings = await _redis.SortedSetRangeByRankWithScoresAsync(
leaderboardKey,
0,
topN - 1,
Order.Descending
);

return topRankings
.Select(
(entry, index) =>
{
var participantId = int.Parse(entry.Element.ToString().Split(':')[1]);
return (Rank: index + 1, ParticipantId: participantId, Score: (int)entry.Score);
}
)
.ToList();
}

public async Task<
List<(int Rank, int ParticipantId, int Score)>
> GetRankingsWithPaginationAsync(string leaderboardKey, int pageNumber, int pageSize)
{
int start = (pageNumber - 1) * pageSize;
int end = start + pageSize - 1;

var rankedParticipants = await _redis.SortedSetRangeByRankWithScoresAsync(
leaderboardKey,
start,
end,
Order.Descending
);

return rankedParticipants
.Select(
(entry, index) =>
{
var participantId = int.Parse(entry.Element.ToString().Split(':')[1]);
return (
Rank: start + index + 1,
ParticipantId: participantId,
Score: (int)entry.Score
);
}
)
.ToList();
}

public async Task SyncLeaderboardAsync(
string leaderboardKey,
List<(int ParticipantId, int Score)> entries
)
{
foreach (var entry in entries)
{
await _redis.SortedSetAddAsync(
leaderboardKey,
$"participant:{entry.ParticipantId}",
entry.Score
);
}
}
}
15 changes: 13 additions & 2 deletions ArenaService/Setup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace ArenaService;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using StackExchange.Redis;

public class Startup
{
Expand Down Expand Up @@ -68,6 +69,15 @@ public void ConfigureServices(IServiceCollection services)
.UseNpgsql(Configuration.GetConnectionString("DefaultConnection"))
.UseSnakeCaseNamingConvention()
);

services.AddSingleton<IConnectionMultiplexer>(provider =>
{
var redisOptions = provider.GetRequiredService<IOptions<RedisOptions>>().Value;
return ConnectionMultiplexer.Connect(
$"{redisOptions.Host}:{redisOptions.Port},defaultDatabase={redisOptions.RankingDbNumber}"
);
});

services.AddEndpointsApiExplorer();

services.AddSwaggerGen(options =>
Expand All @@ -94,6 +104,7 @@ public void ConfigureServices(IServiceCollection services)
options.OperationFilter<AuthorizeCheckOperationFilter>();
});

services.AddScoped<IRankingRepository, RankingRepository>();
services.AddScoped<ISeasonRepository, SeasonRepository>();
services.AddScoped<IParticipantRepository, ParticipantRepository>();
services.AddScoped<IAvailableOpponentRepository, AvailableOpponentRepository>();
Expand All @@ -113,8 +124,8 @@ public void ConfigureServices(IServiceCollection services)
{
var redisOptions = provider.GetRequiredService<IOptions<RedisOptions>>().Value;
config.UseRedisStorage(
$"{redisOptions.Host}:{redisOptions.Port}",
new RedisStorageOptions { Prefix = redisOptions.Prefix }
$"{redisOptions.Host}:{redisOptions.Port},defaultDatabase={redisOptions.HangfireDbNumber}",
new RedisStorageOptions { Prefix = redisOptions.HangfirePrefix }
);
}
);
Expand Down

0 comments on commit 4ffb0ed

Please sign in to comment.