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
57 changes: 25 additions & 32 deletions GenOnlineService/Database/Database.MatchHistory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -1280,9 +1254,28 @@ await db.LeaderboardYearly
}
}

private static void UpdateElo(Dictionary<long, EloData> 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);
}
}
}
}
144 changes: 103 additions & 41 deletions GenOnlineService/ELO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,72 +15,134 @@
** You should have received a copy of the GNU Affero General Public License
** along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
using GenOnlineService;

public enum MatchResult { PlayerAWins, PlayerBWins }

/// <summary>
/// Configuration settings for the Elo rating system.
/// </summary>
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
/// <summary>
/// The base rating for new players in the Elo system.
/// </summary>
public const int BaseRating = 1000;

/// <summary>
/// The K-factor used in the Elo rating calculation,
/// which determines the volatility of rating changes.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public const int KFactor = 24;

/// <summary>
/// The Elo expansion value for standard players, used to adjust ratings over time.
/// </summary>
public const int EloExpansionValue_Standard = 50;

/// <summary>
/// The Elo expansion value for high ELO players, used to adjust ratings over time.
/// </summary>
public const int EloExpansionValue_HighELO = 150;

public static int EloExpansionValue_Standard = 50;
public static int EloExpansionValue_HighELO = 150;
public static int SecondsBetweenEloExpansionsInMatchmaking = 10;
/// <summary>
/// The number of seconds between Elo expansions in matchmaking, which controls how frequently ratings are adjusted.
/// </summary>
public const int SecondsBetweenEloExpansionsInMatchmaking = 10;

public static int HighEloThreshold = 2000;
/// <summary>
/// The threshold rating that defines a high ELO player. Players with ratings above this value are considered high ELO players.
/// </summary>
public const int HighEloThreshold = 2000;
}

public class EloData
/// <summary>
/// Represents a player's Elo rating data.
/// </summary>
/// <param name="rating">The player's current Elo rating.</param>
/// <param name="monthlyRating">The player's Elo rating for the current month.</param>
/// <param name="matchCount">The number of matches the player has played.</param>
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;
/// <summary>
/// Gets or sets the player's current Elo rating.
/// </summary>
public int Rating { get; set; } = rating;

public EloData(int rating, int numMatches)
/// <summary>
/// Gets or sets the number of matches the player has played.
/// </summary>
public int NumMatches { get; set; } = matchCount;

/// <summary>
/// Gets or sets the player's Elo rating for the current month.
/// </summary>
public int MonthlyRating { get; set; } = monthlyRating;

/// <summary>
/// Initializes a new instance of the <see cref="EloData"/> class with default values.
/// </summary>
public EloData()
: this(EloConfig.BaseRating, EloConfig.BaseRating, 0)
{
Rating = rating;
NumMatches = numMatches;
}

public EloData(int rating, int monthlyRating, int numMatches)
/// <summary>
/// Initializes a new instance of the <see cref="EloData"/> class with the specified rating and number of matches.
/// </summary>
/// <param name="rating">The player's current Elo rating.</param>
/// <param name="numMatches">The number of matches the player has played.</param>
public EloData(int rating, int numMatches)
: this(rating, EloConfig.BaseRating, numMatches)
{
Rating = rating;
MonthlyRating = monthlyRating;
NumMatches = numMatches;
}
}

/// <summary>
/// Provides methods for calculating and updating Elo ratings based on match results.
/// </summary>
public static class Elo
{
public static double ExpectedScore(int ra, int rb)
/// <summary>
/// Applies the result of a match between two players, updating their Elo ratings accordingly.
/// </summary>
/// <param name="winner">The player who won the match.</param>
/// <param name="loser">The player who lost the match.</param>
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;
return baseK * 2;
}

int kA = DynamicK(EloConfig.KFactor, playerDataA.NumMatches);
int kB = DynamicK(EloConfig.KFactor, playerDataB.NumMatches);

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);
}
Loading