Skip to content

Commit

Permalink
Merge pull request #21 from eduherminio/add-swiss-support
Browse files Browse the repository at this point in the history
* Add basic swiss support:
  * Include `TieBreaks` and `TotalTieBreaks` fields, which will be empty for Arena tournaments.
  * Use `TieBreaks` field as second tie break.
  * Show swiss points and total swiss points in `Scores` and `TotalScores`, as if/together with arena ones.

* Make API queries sequential, to avoid rate limit issues.

This closes #15.
  • Loading branch information
eduherminio authored Apr 16, 2021
2 parents 0b784fd + 73c35ed commit b38935c
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 33 deletions.
120 changes: 108 additions & 12 deletions LichessTournamentAggregator.Test/AggregatedResultTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,113 @@ public void TotalScores()
new TournamentResult()
{
Username = Username,
Score = 0
Score = 60_227_444
},
new TournamentResult()
{
Username = Username,
Score = 15
Score = 60_999_999
},
new TournamentResult()
{
Username = Username,
Score = 5
Score = 45_129_774
},
new TournamentResult()
{
Username = Username,
Score = 35_094_397
},
new TournamentResult()
{
Username = Username,
},
new TournamentResult()
{
Username = Guid.NewGuid().ToString(),
Score = 35_094_397
},
};

AggregatedResult aggregatedResult = new AggregatedResult(results.GroupBy(r => r.Username).Single(g => g.Key == Username));

Assert.Equal(20, aggregatedResult.TotalScores);
}

[Theory]
[InlineData(299_999_999, 29.5)]
[InlineData(258_772_232, 25.5)]
[InlineData(248_289_662, 24.5)]
[InlineData(243_922_155, 24)]
[InlineData(45_129_774, 4.5)]
[InlineData(30_036_709, 3)]
[InlineData(20_021_986, 2)]
[InlineData(10_001_592, 1)]
[InlineData(5_000_709, 0.5)]
[InlineData(1_720, 0)]
public void Scores(double lichessScore, double expectedCalculatedScore)
{
var result = new TournamentResult()
{
Username = Username,
Score = lichessScore
};

ICollection<TournamentResult> results = new[]
{
result,
new TournamentResult()
{
Username = $"{Username} ",
Score = 30_036_709
},
new TournamentResult()
{
Username = Guid.NewGuid().ToString(),
Score = 24_392_2155
},
new TournamentResult()
{
Username = Username,
Score = 260_001_592
},
new TournamentResult()
{
Username = Username,
Score = 50_001_592
},
new TournamentResult()
{
Username = Guid.NewGuid().ToString(),
Score = 1_720
},
};

var aggregatedResult = new AggregatedResult(results.GroupBy(r => r.Username).Single(g => g.Key == result.Username));

Assert.Equal(aggregatedResult.Username, result.Username);
Assert.Single(aggregatedResult.Scores, (score) => score == expectedCalculatedScore);
}

[Fact]
public void TotalTieBreaks()
{
ICollection<TournamentResult> results = new[]
{
new TournamentResult()
{
Username = Username,
TieBreak = 0
},
new TournamentResult()
{
Username = Username,
TieBreak = 15
},
new TournamentResult()
{
Username = Username,
TieBreak = 5
},
new TournamentResult()
{
Expand All @@ -64,47 +160,47 @@ public void TotalScores()
new TournamentResult()
{
Username = Guid.NewGuid().ToString(),
Score = 7
TieBreak = 7
},
};

AggregatedResult aggregatedResult = new AggregatedResult(results.GroupBy(r => r.Username).Single(g => g.Key == Username));

Assert.Equal(results.Where(r => r.Username == Username).Sum(r => r.Score), aggregatedResult.TotalScores);
Assert.Equal(results.Where(r => r.Username == Username).Sum(r => r.TieBreak), aggregatedResult.TotalTieBreaks);
}

[Fact]
public void Scores()
public void TieBreaks()
{
ICollection<TournamentResult> results = new[]
{
new TournamentResult()
{
Username = Username,
Score = 0
TieBreak = 0
},
new TournamentResult()
{
Username = Username,
Score = 15
TieBreak = 15
},
new TournamentResult()
{
Username = Username,
Score = 5
TieBreak = 5
},
new TournamentResult()
{
Username = Guid.NewGuid().ToString(),
Score = 7
TieBreak = 7
},
};

AggregatedResult aggregatedResult = new AggregatedResult(results.GroupBy(r => r.Username).Single(g => g.Key == Username));

foreach (var score in aggregatedResult.Scores)
foreach (var tieBreak in aggregatedResult.TieBreaks)
{
Assert.Single(results, (result) => result.Username == aggregatedResult.Username && result.Score == score);
Assert.Single(results, (result) => result.Username == aggregatedResult.Username && result.TieBreak == tieBreak);
}
}

Expand Down
30 changes: 27 additions & 3 deletions LichessTournamentAggregator/Model/AggregatedResult.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;

namespace LichessTournamentAggregator.Model
Expand All @@ -20,6 +21,11 @@ public class AggregatedResult
/// </summary>
public double TotalScores { get; set; }

/// <summary>
/// Sum of the Tie Breaks of all the tournaments
/// </summary>
public double TotalTieBreaks { get; set; }

/// <summary>
/// Maximum rating while playing in the tournaments
/// </summary>
Expand All @@ -35,6 +41,11 @@ public class AggregatedResult
/// </summary>
public IEnumerable<double> Scores { get; set; }

/// <summary>
/// Tie breaks in each tournament
/// </summary>
public IEnumerable<double> TieBreaks { get; set; }

/// <summary>
/// Average player performance in the tournaments.
/// </summary>
Expand All @@ -46,9 +57,22 @@ public AggregatedResult(IGrouping<string, TournamentResult> results)
Title = results.First().Title;
MaxRating = results.Max(p => p.Rating);
Ranks = results.Select(p => p.Rank);
Scores = results.Select(p => p.Score);
TotalScores = results.Select(p => p.Score).Sum();
Scores = results.Select(p => CalculatePoints(p.Score));
TieBreaks = results.Select(p => p.TieBreak);
TotalScores = Scores.Sum();
TotalTieBreaks = TieBreaks.Sum();
AveragePerformance = (double)results.Select(p => p.Performance).Sum() / results.Count(p => p.Rank != 0);
}

/// <summary>
/// Lichess score: https://github.com/lichess-org/api/issues/99
/// </summary>
/// <param name="lichessScore"></param>
/// <returns></returns>
private static double CalculatePoints(double lichessScore)
{
// Flooring to the nearest half, see https://stackoverflow.com/questions/1329426/how-do-i-round-to-the-nearest-0-5
return Math.Floor(2 * (lichessScore / 10_000_000)) / 2;
}
}
}
6 changes: 6 additions & 0 deletions LichessTournamentAggregator/Model/TournamentResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ public class TournamentResult
[JsonPropertyName("score")]
public double Score { get; set; }

/// <summary>
/// Only swiss
/// </summary>
[JsonPropertyName("tieBreak")]
public double TieBreak { get; set; }

[JsonPropertyName("rating")]
public int Rating { get; set; }

Expand Down
46 changes: 31 additions & 15 deletions LichessTournamentAggregator/TournamentAggregator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,25 @@ public class TournamentAggregator : ITournamentAggregator
{
public async IAsyncEnumerable<AggregatedResult> AggregateResults(IEnumerable<string> tournamentIdsOrUrls)
{
var urls = GetUrls(tournamentIdsOrUrls);
var results = (await Task.WhenAll(urls.Select(GetTournamentResults)).ConfigureAwait(false)).SelectMany(_ => _);
var tournamentResults = await GetTournamentResults(tournamentIdsOrUrls).ConfigureAwait(false);

foreach (var grouping in GroupResultsByPlayer(results))
foreach (var result in AggregateResults(tournamentResults))
{
yield return new AggregatedResult(grouping);
yield return result;
}
}

public IEnumerable<AggregatedResult> AggregateResults(IEnumerable<TournamentResult> tournamentResults)
{
foreach (var grouping in GroupResultsByPlayer(tournamentResults))
{
yield return new AggregatedResult(grouping);
}
return GroupResultsByPlayer(tournamentResults)
.Select(grouping => new AggregatedResult(grouping));
}

public async Task<FileStream> AggregateResultsAndExportToCsv(IEnumerable<string> tournamentIdsOrUrls, FileStream fileStream, string separator = ";")
{
var orderedResults = AggregateResults(tournamentIdsOrUrls)
.OrderByDescending(r => r.TotalScores)
.ThenByDescending(r => r.TotalTieBreaks)
.ThenByDescending(r => r.AveragePerformance);

return await PopulateCsvStreamAsync(fileStream, separator, orderedResults).ConfigureAwait(false);
Expand All @@ -43,6 +41,9 @@ public IEnumerable<AggregatedResult> AggregateResults(IEnumerable<TournamentResu
internal IEnumerable<Uri> GetUrls(IEnumerable<string> tournamentIdsOrUrls)
{
const string lichessTournamentUrl = "lichess.org/tournament/";
const string lichessSwissUrl = "lichess.org/swiss/";
string tournamentType = "tournament";

foreach (var item in tournamentIdsOrUrls.Select(str => str))
{
var tournamentId = item.AsSpan().Trim(new char[] { ' ', '/', '#' });
Expand All @@ -51,12 +52,26 @@ internal IEnumerable<Uri> GetUrls(IEnumerable<string> tournamentIdsOrUrls)
{
tournamentId = tournamentId.Slice(tournamentId.LastIndexOf('/') + 1);
}
else if (tournamentId.Contains(lichessSwissUrl.AsSpan(), StringComparison.InvariantCultureIgnoreCase))
{
tournamentId = tournamentId.Slice(tournamentId.LastIndexOf('/') + 1);
tournamentType = "swiss";
}

yield return new Uri($"https://lichess.org/api/tournament/{tournamentId.ToString()}/results");
yield return new Uri($"https://lichess.org/api/{tournamentType}/{tournamentId.ToString()}/results");
}
}

private async Task<IEnumerable<TournamentResult>> GetTournamentResults(Uri url)
protected async Task<List<TournamentResult>> GetTournamentResults(IEnumerable<string> tournamentIdsOrUrls)
{
return await GetUrls(tournamentIdsOrUrls)
.Select(GetTournamentResults)
.Aggregate((result, next) => result.Concat(next))
.ToListAsync()
.ConfigureAwait(false);
}

private async IAsyncEnumerable<TournamentResult> GetTournamentResults(Uri url)
{
var client = new HttpClient();

Expand All @@ -69,9 +84,10 @@ private async Task<IEnumerable<TournamentResult>> GetTournamentResults(Uri url)

var rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

var lines = rawContent.Split('\n').Where(str => !string.IsNullOrWhiteSpace(str));

return lines.Select(line => JsonSerializer.Deserialize<TournamentResult>(line));
foreach (var line in rawContent.Split('\n').Where(str => !string.IsNullOrWhiteSpace(str)))
{
yield return JsonSerializer.Deserialize<TournamentResult>(line);
}
}

private static IEnumerable<IGrouping<string, TournamentResult>> GroupResultsByPlayer(IEnumerable<TournamentResult> results)
Expand All @@ -83,7 +99,7 @@ private static IEnumerable<IGrouping<string, TournamentResult>> GroupResultsByPl

private static async Task<FileStream> PopulateCsvStreamAsync(FileStream fileStream, string separator, IOrderedAsyncEnumerable<AggregatedResult> aggregatedResults)
{
var headers = new List<string> { "#", "Username", "Total Score", "Average Performance", "Max Rating", "Title", "Ranks", "Scores" };
var headers = new List<string> { "#", "Username", "Total Score", "Total tie breaks", "Average Performance", "Max Rating", "Title", "Ranks", "Scores", "Tie breaks" };
using var sw = new StreamWriter(fileStream);
sw.WriteLine(string.Join(separator, headers));

Expand All @@ -93,7 +109,7 @@ private static async Task<FileStream> PopulateCsvStreamAsync(FileStream fileStre
await foreach (var aggregatedResult in aggregatedResults.Select((value, i) => new { i, value }))
{
var result = aggregatedResult.value;
var columns = new string[] { (aggregatedResult.i + 1).ToString(), result.Username, result.TotalScores.ToString(), result.AveragePerformance.ToString("F"), result.MaxRating.ToString(), result.Title, aggregate(result.Ranks), aggregate(result.Scores) };
var columns = new string[] { (aggregatedResult.i + 1).ToString(), result.Username, result.TotalScores.ToString(), result.TotalTieBreaks.ToString(), result.AveragePerformance.ToString("F"), result.MaxRating.ToString(), result.Title, aggregate(result.Ranks), aggregate(result.Scores), aggregate(result.TieBreaks) };
sw.WriteLine(string.Join(separator, columns));
}

Expand Down
6 changes: 3 additions & 3 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ stages:
- stage: CD
displayName: 'Generate and publish artifacts'
dependsOn: 'CI'
condition: and(succeeded('CI'), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
condition: and(succeeded('CI'), ne(variables['Build.Reason'], 'Schedule'))
jobs:
- job: cd
displayName: 'Generate and publish artifacts'
Expand Down Expand Up @@ -142,7 +142,7 @@ stages:

- task: NuGetCommand@2
displayName: 'Push NuGet package'
condition: ne(variables['Build.Reason'], 'Schedule')
condition: and(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
continueOnError: true
inputs:
command: 'push'
Expand All @@ -153,7 +153,7 @@ stages:

- task: NuGetCommand@2
displayName: 'Push GitHub package'
condition: ne(variables['Build.Reason'], 'Schedule')
condition: and(eq(variables['Build.Reason'], 'Manual'), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
continueOnError: true
inputs:
command: 'push'
Expand Down

0 comments on commit b38935c

Please sign in to comment.