Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions ArenaService.Shared/Dtos/RankingSnapshotEntryResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Libplanet.Crypto;

namespace ArenaService.Shared.Dtos;

public class RankingSnapshotEntryResponse
{
public required Address AgentAddress { get; set; }
public required Address AvatarAddress { get; set; }
public required string NameWithHash { get; set; }
public required int Level { get; set; }
public required long Cp { get; set; }
public required int Score { get; set; }
}


32 changes: 32 additions & 0 deletions ArenaService.Shared/Repositories/RankingSnapshotRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ Task<int> GetRankingSnapshotsCount(
int roundId,
Func<IQueryable<RankingSnapshot>, IQueryable<RankingSnapshot>>? includeQuery = null
);

Task<List<ArenaService.Shared.Dtos.RankingSnapshotEntryResponse>> GetRankingSnapshotEntries(
int seasonId,
int roundId,
int skip = 0,
int take = 1000
);
}

public class RankingSnapshotRepository : IRankingSnapshotRepository
Expand Down Expand Up @@ -103,4 +110,29 @@ public async Task<int> GetRankingSnapshotsCount(

return await query.Where(r => r.SeasonId == seasonId && r.RoundId == roundId).CountAsync();
}

public async Task<List<ArenaService.Shared.Dtos.RankingSnapshotEntryResponse>> GetRankingSnapshotEntries(
int seasonId,
int roundId,
int skip = 0,
int take = 1000
Comment on lines +117 to +118
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will cause an error if value is negative.

)
{
var query =
from snapshot in _context.RankingSnapshots.AsNoTracking()
join user in _context.Users.AsNoTracking() on snapshot.AvatarAddress equals user.AvatarAddress
where snapshot.SeasonId == seasonId && snapshot.RoundId == roundId
orderby snapshot.Score descending
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FioX0 i think if the scores are tied, additional sorting is needed. is it ok?

select new ArenaService.Shared.Dtos.RankingSnapshotEntryResponse
{
AgentAddress = user.AgentAddress,
AvatarAddress = user.AvatarAddress,
NameWithHash = user.NameWithHash,
Level = user.Level,
Cp = user.Cp,
Score = snapshot.Score
};

return await query.Skip(skip).Take(take).ToListAsync();
}
}
53 changes: 52 additions & 1 deletion ArenaService.Tests/Controllers/LeaderboardControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class LeaderboardControllerTests
private readonly Mock<ISeasonService> _mockSeasonService;
private readonly Mock<ISeasonCacheRepository> _mockSeasonCacheRepo;
private readonly Mock<ISeasonRepository> _mockSeasonRepo;
private readonly Mock<IRankingSnapshotRepository> _mockRankingSnapshotRepo;
private readonly LeaderboardController _controller;

public LeaderboardControllerTests()
Expand All @@ -29,14 +30,16 @@ public LeaderboardControllerTests()
_mockSeasonService = new Mock<ISeasonService>();
_mockSeasonCacheRepo = new Mock<ISeasonCacheRepository>();
_mockSeasonRepo = new Mock<ISeasonRepository>();
_mockRankingSnapshotRepo = new Mock<IRankingSnapshotRepository>();

_controller = new LeaderboardController(
_mockAllClanRankingRepo.Object,
_mockRankingRepo.Object,
_mockLeaderboardRepo.Object,
_mockSeasonService.Object,
_mockSeasonCacheRepo.Object,
_mockSeasonRepo.Object
_mockSeasonRepo.Object,
_mockRankingSnapshotRepo.Object
);
}

Expand Down Expand Up @@ -175,4 +178,52 @@ public async Task GetCompletedArenaLeaderboard_WithException_ReturnsBadRequest()
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result.Result);
}

[Fact]
public async Task GetRankingSnapshot_RespectsPaginationParameters()
{
// Arrange
int seasonId = 1;
int roundId = 1;
var entries = Enumerable.Range(0, 1500).Select(i => new RankingSnapshotEntryResponse
{
AgentAddress = new Address($"0x{i.ToString("x").PadLeft(40, '0')}"),
AvatarAddress = new Address($"0x{(i + 10000).ToString("x").PadLeft(40, '0')}"),
NameWithHash = $"Player#{i}",
Level = i,
Cp = i * 100,
Score = 2000 - i
}).ToList();

_mockRankingSnapshotRepo
.Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 0, 1000))
.ReturnsAsync(entries.Take(1000).ToList());
_mockRankingSnapshotRepo
.Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 1000, 1000))
.ReturnsAsync(entries.Skip(1000).Take(1000).ToList());
_mockRankingSnapshotRepo
.Setup(x => x.GetRankingSnapshotEntries(seasonId, roundId, 500, 200))
.ReturnsAsync(entries.Skip(500).Take(200).ToList());

// Act
var firstPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 0, 1000);
var secondPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 1000, 1000);
var customPageResult = await _controller.GetRankingSnapshot(seasonId, roundId, 500, 200);

// Assert
var firstPage = Assert.IsType<OkObjectResult>(firstPageResult.Result);
var secondPage = Assert.IsType<OkObjectResult>(secondPageResult.Result);
var customPage = Assert.IsType<OkObjectResult>(customPageResult.Result);

var firstPageEntries = Assert.IsType<List<RankingSnapshotEntryResponse>>(firstPage.Value);
var secondPageEntries = Assert.IsType<List<RankingSnapshotEntryResponse>>(secondPage.Value);
var customPageEntries = Assert.IsType<List<RankingSnapshotEntryResponse>>(customPage.Value);

Assert.Equal(1000, firstPageEntries.Count);
Assert.Equal(500, secondPageEntries.Count);
Assert.Equal(200, customPageEntries.Count);
Assert.Equal(entries[0].AvatarAddress, firstPageEntries.First().AvatarAddress);
Assert.Equal(entries[1000].AvatarAddress, secondPageEntries.First().AvatarAddress);
Assert.Equal(entries[500].AvatarAddress, customPageEntries.First().AvatarAddress);
}
}
27 changes: 26 additions & 1 deletion ArenaService/Controllers/LeaderboardController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ public class LeaderboardController : ControllerBase
private readonly ISeasonService _seasonService;
private readonly ISeasonRepository _seasonRepo;
private readonly ISeasonCacheRepository _seasonCacheRepo;
private readonly IRankingSnapshotRepository _rankingSnapshotRepo;

public LeaderboardController(
IAllClanRankingRepository allClanRankingRepo,
IRankingRepository rankingRepo,
ILeaderboardRepository leaderboardRepo,
ISeasonService seasonService,
ISeasonCacheRepository seasonCacheRepo,
ISeasonRepository seasonRepo
ISeasonRepository seasonRepo,
IRankingSnapshotRepository rankingSnapshotRepo
)
{
_allClanRankingRepo = allClanRankingRepo;
Expand All @@ -33,6 +35,7 @@ ISeasonRepository seasonRepo
_seasonService = seasonService;
_seasonCacheRepo = seasonCacheRepo;
_seasonRepo = seasonRepo;
_rankingSnapshotRepo = rankingSnapshotRepo;
}

[HttpGet("count")]
Expand All @@ -44,6 +47,28 @@ public async Task<ActionResult<int>> GetRankingCount(int seasonId, int roundInde
return Ok(rankingCount);
}

[HttpGet("participants")]
[SwaggerResponse(
StatusCodes.Status200OK,
"Ranking ongoing participants",
typeof(List<ArenaService.Shared.Dtos.RankingSnapshotEntryResponse>)
)]
public async Task<ActionResult<List<ArenaService.Shared.Dtos.RankingSnapshotEntryResponse>>> GetRankingSnapshot(
[FromQuery] int seasonId,
[FromQuery] int roundId,
[FromQuery] int skip = 0,
[FromQuery] int take = 1000
)
{
// Limit the maximum page size to 1000
if (take > 1000)
{
take = 1000;
}
var entries = await _rankingSnapshotRepo.GetRankingSnapshotEntries(seasonId, roundId, skip, take);
return Ok(entries);
}

[HttpGet("completed")]
[SwaggerResponse(
StatusCodes.Status200OK,
Expand Down