From 3a0a118a4e08fc203eac8d90b0698a58b1b83852 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:41:11 +0200 Subject: [PATCH 1/4] refactor(elo): Refactor EloData to use primary constructor - Added primary constructor - Point other constructions to primary constructor to ensure parameter initialization is centralized to increase maintainability - Replaced initialization of properties with constants from EloConfig. --- GenOnlineService/ELO.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/GenOnlineService/ELO.cs b/GenOnlineService/ELO.cs index 73bba83..4191a5d 100644 --- a/GenOnlineService/ELO.cs +++ b/GenOnlineService/ELO.cs @@ -31,23 +31,20 @@ public static class EloConfig public static int HighEloThreshold = 2000; } -public class EloData +public sealed class EloData(int rating, int monthlyRating, int matchCount) { - public int Rating { get; set; } = 1000; - public int NumMatches { get; set; } = 0; - public int MonthlyRating { get; set; } = 1000; + public int Rating { get; set; } = rating; + public int NumMatches { get; set; } = matchCount; + public int MonthlyRating { get; set; } = monthlyRating; - public EloData(int rating, int numMatches) + public EloData() + : this(EloConfig.BaseRating, EloConfig.BaseRating, 0) { - Rating = rating; - NumMatches = numMatches; } - public EloData(int rating, int monthlyRating, int numMatches) + public EloData(int rating, int numMatches) + : this(rating, EloConfig.BaseRating, numMatches) { - Rating = rating; - MonthlyRating = monthlyRating; - NumMatches = numMatches; } } From 2b3bb45f009643300aaf809bb1e1f11fd0bb7115 Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:45:42 +0200 Subject: [PATCH 2/4] refactor(elo): Refactor Elo logic for robustness - Renamed private methods to better reflect their function - Made ExpectedScore private as it is not publicly used. - Removed the use of MatchResult enum as it could introduce an error when the result value did not match a predefined enum value. - Refactored MatchHistory database to reduce code duplication. --- .../Database/Database.MatchHistory.cs | 57 ++++++++----------- GenOnlineService/ELO.cs | 50 ++++++++-------- 2 files changed, 51 insertions(+), 56 deletions(-) diff --git a/GenOnlineService/Database/Database.MatchHistory.cs b/GenOnlineService/Database/Database.MatchHistory.cs index aead105..4a51236 100644 --- a/GenOnlineService/Database/Database.MatchHistory.cs +++ b/GenOnlineService/Database/Database.MatchHistory.cs @@ -1135,16 +1135,7 @@ private static async Task UpdateCurrentEloAsync( continue; Console.WriteLine($"[ELO] Pairing a={a.user_id}(won={a.won}) vs b={b.user_id}(won={b.won}) → result={(a.won ? "PlayerAWins" : "PlayerBWins")}"); - ref EloData A = ref CollectionsMarshal.GetValueRefOrAddDefault( - dictElo, a.user_id, out _); - - ref EloData B = ref CollectionsMarshal.GetValueRefOrAddDefault( - dictElo, b.user_id, out _); - - Elo.ApplyResult( - ref A, - ref B, - a.won ? MatchResult.PlayerAWins : MatchResult.PlayerBWins); + UpdateElo(dictElo, a.user_id, b.user_id, a.won); } } @@ -1214,26 +1205,9 @@ private static async Task UpdatePeriodEloAndLeaderboardsAsync( if (a.user_id >= b.user_id) continue; - // Daily - { - ref EloData A = ref CollectionsMarshal.GetValueRefOrAddDefault(daily, a.user_id, out _); - ref EloData B = ref CollectionsMarshal.GetValueRefOrAddDefault(daily, b.user_id, out _); - Elo.ApplyResult(ref A, ref B, a.won ? MatchResult.PlayerAWins : MatchResult.PlayerBWins); - } - - // Monthly - { - ref EloData A = ref CollectionsMarshal.GetValueRefOrAddDefault(monthly, a.user_id, out _); - ref EloData B = ref CollectionsMarshal.GetValueRefOrAddDefault(monthly, b.user_id, out _); - Elo.ApplyResult(ref A, ref B, a.won ? MatchResult.PlayerAWins : MatchResult.PlayerBWins); - } - - // Yearly - { - ref EloData A = ref CollectionsMarshal.GetValueRefOrAddDefault(yearly, a.user_id, out _); - ref EloData B = ref CollectionsMarshal.GetValueRefOrAddDefault(yearly, b.user_id, out _); - Elo.ApplyResult(ref A, ref B, a.won ? MatchResult.PlayerAWins : MatchResult.PlayerBWins); - } + UpdateElo(daily, a.user_id, b.user_id, a.won); + UpdateElo(monthly, a.user_id, b.user_id, a.won); + UpdateElo(yearly, a.user_id, b.user_id, a.won); } } @@ -1280,9 +1254,28 @@ await db.LeaderboardYearly } } + private static void UpdateElo(Dictionary dict, long firstPlayerId, long secondPlayerId, bool firstPlayerWon) + { + ref EloData? firstPlayer = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, firstPlayerId, out bool existsA); + if (!existsA) + { + firstPlayer = new EloData(); + } + ref EloData? secondPlayer = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, secondPlayerId, out bool existsB); + if (!existsB) + { + secondPlayer = new EloData(); + } - - + if (firstPlayerWon) + { + Elo.ApplyResult(firstPlayer, secondPlayer); + } + else + { + Elo.ApplyResult(secondPlayer, firstPlayer); + } + } } } diff --git a/GenOnlineService/ELO.cs b/GenOnlineService/ELO.cs index 4191a5d..7e1d125 100644 --- a/GenOnlineService/ELO.cs +++ b/GenOnlineService/ELO.cs @@ -15,9 +15,6 @@ ** You should have received a copy of the GNU Affero General Public License ** along with this program. If not, see . */ -using GenOnlineService; - -public enum MatchResult { PlayerAWins, PlayerBWins } public static class EloConfig { @@ -50,34 +47,39 @@ public EloData(int rating, int numMatches) public static class Elo { - public static double ExpectedScore(int ra, int rb) + public static void ApplyResult(EloData winner, EloData loser) { - // E_A = 1 / (1 + 10^((R_B - R_A)/400)) - return 1.0 / (1.0 + Math.Pow(10.0, (rb - ra) / 400.0)); + var winnerScore = GetExpectedScore(winner.Rating, loser.Rating); + var loserScore = 1.0 - winnerScore; + + var winnerKFactor = GetEffectiveKFactor(EloConfig.KFactor, winner.NumMatches); + var loserKFactor = GetEffectiveKFactor(EloConfig.KFactor, loser.NumMatches); + + winner.Rating += (int)Math.Round(winnerKFactor * (1.0 - winnerScore)); + loser.Rating -= (int)Math.Round(loserKFactor * loserScore); } - public static void ApplyResult(ref EloData playerDataA, ref EloData playerDataB, MatchResult result) + private static double GetExpectedScore(int player, int opponent) { - double ea = ExpectedScore(playerDataA.Rating, playerDataB.Rating); - double eb = 1.0 - ea; + return 1.0 / (1.0 + Math.Pow(10.0, (opponent - player) / 400.0)); + } - double sa = result switch + private static int GetEffectiveKFactor(int baseK, int numberOfGames) + { + // Brand new players get a higher K factor to + // allow their rating to adjust more quickly + if (numberOfGames < 10) { - MatchResult.PlayerAWins => 1.0, - MatchResult.PlayerBWins => 0.0, - _ => 0.5 - }; - double sb = 1.0 - sa; - - int kA = DynamicK(EloConfig.KFactor, playerDataA.NumMatches); - int kB = DynamicK(EloConfig.KFactor, playerDataB.NumMatches); + return baseK * 2; + } - playerDataA.Rating = playerDataA.Rating + (int)Math.Round(kA * (sa - ea)); + // Players with less than 100 games may still improve their game skill + // and therefore get a slightly higher K factor + if (numberOfGames< 100) + { + return (int) (baseK* 1.25); + } - playerDataB.Rating = playerDataB.Rating + (int)Math.Round(kB * (sb - eb)); + return baseK; } - - // note: higher K for new players; dampen after 100 games - private static int DynamicK(int baseK, int games) - => games < 10 ? baseK * 2 : (games < 100 ? (int)(baseK * 1.25) : baseK); } From f21dad9bc7548b1862686930f1fbd57e762d1bbe Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:16:07 +0200 Subject: [PATCH 3/4] refactor(elo): Use const instead of static for EloConfig - Replaced static with const and removed getters as the properties in EloConfig are not changed (and should not) change during runtime. This also increases performance as constants are read during compile time. --- GenOnlineService/ELO.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/GenOnlineService/ELO.cs b/GenOnlineService/ELO.cs index 7e1d125..62178e0 100644 --- a/GenOnlineService/ELO.cs +++ b/GenOnlineService/ELO.cs @@ -18,14 +18,12 @@ public static class EloConfig { - public static int BaseRating { get; } = 1000; - public static int KFactor { get; } = 24; // base for per game volatility, increases for first 10 matches, lower after that - - public static int EloExpansionValue_Standard = 50; - public static int EloExpansionValue_HighELO = 150; - public static int SecondsBetweenEloExpansionsInMatchmaking = 10; - - public static int HighEloThreshold = 2000; + public const int BaseRating = 1000; + public const int KFactor = 24; // base for per game volatility, increases for first 10 matches, lower after that + public const int EloExpansionValue_Standard = 50; + public const int EloExpansionValue_HighELO = 150; + public const int SecondsBetweenEloExpansionsInMatchmaking = 10; + public const int HighEloThreshold = 2000; } public sealed class EloData(int rating, int monthlyRating, int matchCount) From 94419be115c43b1c9392f5a6fc180d0faaa95e0b Mon Sep 17 00:00:00 2001 From: Skyaero <21192585+Skyaero42@users.noreply.github.com> Date: Fri, 21 Aug 2026 21:20:44 +0200 Subject: [PATCH 4/4] docs(elo): Add documentation text to public elements. --- GenOnlineService/ELO.cs | 67 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/GenOnlineService/ELO.cs b/GenOnlineService/ELO.cs index 62178e0..6fd6f87 100644 --- a/GenOnlineService/ELO.cs +++ b/GenOnlineService/ELO.cs @@ -16,35 +16,100 @@ ** along with this program. If not, see . */ +/// +/// Configuration settings for the Elo rating system. +/// public static class EloConfig { + /// + /// The base rating for new players in the Elo system. + /// public const int BaseRating = 1000; - public const int KFactor = 24; // base for per game volatility, increases for first 10 matches, lower after that + + /// + /// The K-factor used in the Elo rating calculation, + /// which determines the volatility of rating changes. + /// + /// + /// The KFactor is the maximum number of points a player can + /// gain or lose in a single match. The KFactor may be modified, e.g. + /// for new players or players with fewer matches, to allow for faster rating adjustments. + /// + public const int KFactor = 24; + + /// + /// The Elo expansion value for standard players, used to adjust ratings over time. + /// public const int EloExpansionValue_Standard = 50; + + /// + /// The Elo expansion value for high ELO players, used to adjust ratings over time. + /// public const int EloExpansionValue_HighELO = 150; + + /// + /// The number of seconds between Elo expansions in matchmaking, which controls how frequently ratings are adjusted. + /// public const int SecondsBetweenEloExpansionsInMatchmaking = 10; + + /// + /// The threshold rating that defines a high ELO player. Players with ratings above this value are considered high ELO players. + /// public const int HighEloThreshold = 2000; } +/// +/// Represents a player's Elo rating data. +/// +/// The player's current Elo rating. +/// The player's Elo rating for the current month. +/// The number of matches the player has played. public sealed class EloData(int rating, int monthlyRating, int matchCount) { + /// + /// Gets or sets the player's current Elo rating. + /// public int Rating { get; set; } = rating; + + /// + /// Gets or sets the number of matches the player has played. + /// public int NumMatches { get; set; } = matchCount; + + /// + /// Gets or sets the player's Elo rating for the current month. + /// public int MonthlyRating { get; set; } = monthlyRating; + /// + /// Initializes a new instance of the class with default values. + /// public EloData() : this(EloConfig.BaseRating, EloConfig.BaseRating, 0) { } + /// + /// Initializes a new instance of the class with the specified rating and number of matches. + /// + /// The player's current Elo rating. + /// The number of matches the player has played. public EloData(int rating, int numMatches) : this(rating, EloConfig.BaseRating, numMatches) { } } +/// +/// Provides methods for calculating and updating Elo ratings based on match results. +/// public static class Elo { + /// + /// Applies the result of a match between two players, updating their Elo ratings accordingly. + /// + /// The player who won the match. + /// The player who lost the match. public static void ApplyResult(EloData winner, EloData loser) { var winnerScore = GetExpectedScore(winner.Rating, loser.Rating);