Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get player's rank, improvements in API #5

Merged
merged 9 commits into from
Mar 1, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@
<Description>andrey.games Leaderboards API Client</Description>
<PackageReleaseNotes>Fixing metadata for ProGet</PackageReleaseNotes>
<PackageTags>andrey.games leaderboards api</PackageTags>

<Version>1.10.0</Version>
<PackageVersion>1.10.0</PackageVersion>
<AssemblyVersion>1.10.0</AssemblyVersion>
<Version>1.11.0</Version>
<PackageVersion>1.11.0</PackageVersion>
<AssemblyVersion>1.11.0</AssemblyVersion>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/// <summary>
/// Get player's offset in the leaderboard
/// </summary>
public class GetPlayerRank : LeaderboardsCryptoRequest
public class GetPlayerRankRequest : LeaderboardsCryptoRequest
{
/// <summary>
/// The game
Expand All @@ -14,6 +14,11 @@ public class GetPlayerRank : LeaderboardsCryptoRequest
/// The name of the player.
/// </summary>
public string PlayerName { get; set; }

/// <summary>
/// If true, player will be searched in a case-insensitive way
/// </summary>
public bool CaseInsensitive { get; set; }

/// <summary>
/// Return only winners
Expand Down
5 changes: 5 additions & 0 deletions src/AndreyGames.Leaderboards.API/GetPlayerScoreRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,10 @@ public class GetPlayerScoreRequest : LeaderboardsCryptoRequest
/// Player name to return the score for
/// </summary>
public string PlayerName { get; set; }

/// <summary>
/// If true, player will be searched in a case-insensitive way
/// </summary>
public bool CaseInsensitive { get; set; }
}
}
15 changes: 11 additions & 4 deletions src/AndreyGames.Leaderboards.API/ILeaderboardsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,26 @@ public interface ILeaderboardsClient
/// </summary>
/// <exception cref="ApiException">Something went wrong on the server side.</exception>
Task AddLeaderboard(string game, CancellationToken token = default);

/// <summary>
/// Get player's score for the game
/// </summary>
/// <exception cref="ApiException">Something went wrong on the server side.</exception>
Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game, string playerName, CancellationToken token = default);
Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game,
string playerName,
bool caseInsensitive = false,
CancellationToken token = default);

/// <summary>
/// Returns player's rank position in the specified leaderboard.
/// </summary>
/// <exception cref="ApiException">Something went wrong on the server side.</exception>
Task<LeaderboardEntry> GetPlayerRank(string game, string playerName, bool winnersOnly = false,
TimeFrame? timeFrame = default, CancellationToken token = default);
Task<LeaderboardEntry> GetPlayerRank(string game,
string playerName,
bool caseInsensitive = false,
bool winnersOnly = false,
TimeFrame? timeFrame = default,
CancellationToken token = default);

/// <summary>
/// Get leaderboard for the game
Expand Down
13 changes: 9 additions & 4 deletions src/AndreyGames.Leaderboards.API/LeaderboardsClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ public Task AddLeaderboard(string game, CancellationToken token = default)
return AddLeaderboard(url, request, token);
}

public Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game, string playerName,
public Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game,
string playerName,
bool caseInsensitive = false,
CancellationToken token = default)
{
const string path = "/score/get";
Expand All @@ -100,7 +102,8 @@ public Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game, string pl
var json = SerializeJsonBytes(new GetPlayerScoreRequest
{
Game = game,
PlayerName = playerName
PlayerName = playerName,
CaseInsensitive = caseInsensitive,
});

var request = new LeaderboardsCryptoRequest
Expand All @@ -109,12 +112,13 @@ public Task<ICollection<LeaderboardEntry>> GetPlayerScore(string game, string pl
_password,
_seed),
};

return GetPlayerScore(url, request, token);
}

public Task<LeaderboardEntry> GetPlayerRank(string game,
string playerName,
bool caseInsensitive = false,
bool winnersOnly = false,
TimeFrame? timeFrame = default,
CancellationToken token = default)
Expand All @@ -126,10 +130,11 @@ public Task<LeaderboardEntry> GetPlayerRank(string game,
"Executing GetPlayerOffset command on URL '{0}', game=[{1}], winnersOnly=[{2}], timeFrame=[{3}]",
url, game, winnersOnly, timeFrame);

var json = SerializeJsonBytes(new GetPlayerRank
var json = SerializeJsonBytes(new GetPlayerRankRequest
{
Game = game,
PlayerName = playerName,
CaseInsensitive = caseInsensitive,
WinnersOnly = winnersOnly,
Time = timeFrame,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ Task<LeaderboardView> GetLeaderboard(string game,

Task<LeaderboardEntry> GetPlayerRank(string game,
string playerName,
bool caseInsensitive,
DateTime? start = default,
DateTime? end = default,
bool onlyWinners = false);

Task<ICollection<LeaderboardEntry>> GetScoreForPlayer(string game, string playerName);
Task<ICollection<LeaderboardEntry>> GetScoreForPlayer(string game, string playerName, bool caseInsensitive);

Task PutPlayerScore(string game, DateTime date, string playerName, long score, bool isWinner = false, bool isFraud = false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public async Task<LeaderboardApiResponse> GetLeaderboard([FromBody] GetLeaderboa
public async Task<LeaderboardApiResponse> GetPlayerScore([FromBody] GetPlayerScoreRequest request)
{
return new LeaderboardApiResponse(
await _leaderboardService.GetScoreForPlayer(request.Game, request.PlayerName));
await _leaderboardService.GetScoreForPlayer(request.Game, request.PlayerName, request.CaseInsensitive));
}

[HttpPost("score/put")]
Expand All @@ -78,13 +78,14 @@ await _leaderboardService.PutPlayerScore(request.Game,
}

[HttpPost("score/rank")]
public async Task<LeaderboardApiResponse> GetPlayerRank([FromBody] GetPlayerRank request)
public async Task<LeaderboardApiResponse> GetPlayerRank([FromBody] GetPlayerRankRequest request)
{
var startDate = _timeFrameConverter.GetStartDate(request.Time ?? TimeFrame.Infinite);
var endDate = _timeFrameConverter.GetEndDate(request.Time ?? TimeFrame.Infinite);

var data = await _leaderboardService.GetPlayerRank(request.Game,
request.PlayerName,
request.CaseInsensitive,
startDate,
endDate,
request.WinnersOnly);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ internal sealed class BasicQueryBuilder<TModel>
private readonly List<(string Field, object Value1, object Value2)> _andBetweens = new();

private string _template;
private string _envelope;

private int? _limit, _offset;

public static string WherePlaceholder => "%%WHERE%%";

public static string PagingPlaceholder => "%%PAGING%%";

private BasicQueryBuilder(DbContext dbContext)
{
Expand All @@ -36,6 +41,19 @@ public BasicQueryBuilder<TModel> WithArbitraryParameter(string name, object valu
_arbitraryParams[name] = value;
return this;
}

public BasicQueryBuilder<TModel> WithEnvelope(string formattedEnvelope)
{
_envelope = formattedEnvelope;
return this;
}

public BasicQueryBuilder<TModel> WithPaging(int? limit = default, int? offset = default)
{
_limit = limit;
_offset = offset;
return this;
}

public BasicQueryBuilder<TModel> And(string field, object value)
{
Expand Down Expand Up @@ -80,7 +98,23 @@ private string BuildQuery()
}

var where = string.Join(" AND ", all);
return _template.Replace(WherePlaceholder, where);

if (!string.IsNullOrEmpty(_envelope) && !string.IsNullOrWhiteSpace(where))
{
where = string.Format(_envelope, where);
}

var resultQuery = _template.Replace(WherePlaceholder, where);

if (_offset.HasValue || _limit.HasValue)
{
var paging = (_limit.HasValue ? $" LIMIT {_limit} " : "") +
(_offset.HasValue ? $" OFFSET {_offset} " : "");

resultQuery = resultQuery.Replace(PagingPlaceholder, paging);
}

return resultQuery;
}

private DynamicParameters BuildParameters()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,44 +64,42 @@ public async Task<LeaderboardView> GetLeaderboard(string game, DateTime? start =
var offsetValue = Math.Max(0, offset ?? 0);
var limitValue = Math.Max(1, limit ?? 20);

IEnumerable<Entry> entries = leaderboard.Entries;
var wherePlaceholder = BasicQueryBuilder<LeaderboardEntry>.WherePlaceholder;
var pagingPlaceholder = BasicQueryBuilder<LeaderboardEntry>.PagingPlaceholder;

if (start.HasValue)
{
entries = entries.Where(x => x.Timestamp >= start.Value);
}
var queryTemplate =
$@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @LeaderboardId {wherePlaceholder})
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"", ""PlayerName"" as ""Name"" FROM entries {pagingPlaceholder}";

var queryBuilder = BasicQueryBuilder<LeaderboardEntry>
.New(_context)
.WithQueryTemplate(queryTemplate)
.WithEnvelope("AND ({0})")
.WithArbitraryParameter("LeaderboardId", leaderboard.Id)
.WithPaging(limitValue, offsetValue);

if (end.HasValue)
if (onlyWinners)
{
entries = entries.Where(x => x.Timestamp < end.Value);
queryBuilder = queryBuilder.And("IsWinner", true);
}

if (onlyWinners)
if (start.HasValue && end.HasValue)
{
entries = entries.Where(x => x.IsWinner);
queryBuilder = queryBuilder.AndBetween("Timestamp", start.Value, end.Value);
}

entries = entries.OrderByDescending(x => x.Score)
.ThenByDescending(x => x.Timestamp)
.Skip(offsetValue)
.Take(limitValue);

var counter = offsetValue;
var results = await queryBuilder.Execute();
var entries = results.ToArray();

return new LeaderboardView
{
Offset = offsetValue,
Entries = entries.Select(x => new LeaderboardEntry
{
Name = x.PlayerName,
Score = x.Score,
Rank = ++counter,
IsWinner = x.IsWinner,
}).ToArray(),
Offset = entries.LastOrDefault()?.Rank ?? 0,
Entries = entries,
};
}

public async Task<LeaderboardEntry> GetPlayerRank(string game, string playerName, DateTime? start = default, DateTime? end = default,
public async Task<LeaderboardEntry> GetPlayerRank(string game, string playerName, bool caseInsensitive, DateTime? start = default, DateTime? end = default,
bool onlyWinners = false)
{
if (start.HasValue && end.HasValue)
Expand All @@ -123,14 +121,20 @@ public async Task<LeaderboardEntry> GetPlayerRank(string game, string playerName

var wherePlaceholder = BasicQueryBuilder<ScoreItem>.WherePlaceholder;

var queryTemplate =
var queryTemplateCaseSensitive =
$@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @LeaderboardId AND ({wherePlaceholder}))
FROM ""Entries"" WHERE ""LeaderboardId"" = @LeaderboardId {wherePlaceholder})
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"" FROM entries WHERE ""PlayerName"" = @PlayerName";

var queryTemplateCaseInsensitive =
$@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @LeaderboardId {wherePlaceholder})
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"" FROM entries WHERE LOWER(""PlayerName"") = LOWER(@PlayerName)";

var queryBuilder = BasicQueryBuilder<ScoreItem>
.New(_context)
.WithQueryTemplate(queryTemplate)
.WithQueryTemplate(caseInsensitive ? queryTemplateCaseInsensitive : queryTemplateCaseSensitive)
.WithEnvelope("AND ({0})")
.WithArbitraryParameter("LeaderboardId", leaderboard.Id)
.WithArbitraryParameter("PlayerName", playerName);

Expand All @@ -148,10 +152,16 @@ public async Task<LeaderboardEntry> GetPlayerRank(string game, string playerName

return results.Select(x => new LeaderboardEntry
{
IsWinner = x.IsWinner,
IsWinner = x.IsWinner,
Rank = x.Rank,
Score = x.Score,
Name = playerName,
}).DefaultIfEmpty(new LeaderboardEntry
{
IsWinner = false,
Name = playerName,
Rank = 0,
Score = 0,
}).FirstOrDefault();
}

Expand All @@ -162,7 +172,7 @@ private class ScoreItem
public bool IsWinner { get; set; }
}

public async Task<ICollection<LeaderboardEntry>> GetScoreForPlayer(string game, string playerName)
public async Task<ICollection<LeaderboardEntry>> GetScoreForPlayer(string game, string playerName, bool caseInsensitive)
{
var leaderboard = await _context
.Leaderboards
Expand All @@ -173,19 +183,29 @@ public async Task<ICollection<LeaderboardEntry>> GetScoreForPlayer(string game,
throw new LeaderboardNotFound(game);
}

var entry = leaderboard.Entries.FirstOrDefault(x => x.PlayerName == playerName);
var entry = caseInsensitive
? leaderboard.Entries.FirstOrDefault(x => x.PlayerName.ToLower() == playerName.ToLower())
: leaderboard.Entries.FirstOrDefault(x => x.PlayerName == playerName);

if (entry is null)
{
return ArraySegment<LeaderboardEntry>.Empty;
}

const string caseSensitiveQuery =
@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @id)
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"" FROM entries WHERE ""PlayerName"" = @name";

const string caseInsensitiveQuery =
@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @id)
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"" FROM entries WHERE LOWER(""PlayerName"") = LOWER(@name)";

var scores = await _context.Database
.GetDbConnection()
.QueryAsync<ScoreItem>(
@"WITH entries as (SELECT ""Entries"".*, row_number() OVER (ORDER BY ""Score"" DESC, ""Timestamp"" DESC) as ""RowNumber""
FROM ""Entries"" WHERE ""LeaderboardId"" = @id)
SELECT ""RowNumber"" as ""Rank"", ""Score"", ""IsWinner"" FROM entries WHERE ""PlayerName"" = @name",
caseInsensitive ? caseInsensitiveQuery : caseSensitiveQuery,
new
{
id = leaderboard.Id,
Expand Down
Loading
Loading