mirror of
https://github.com/furyfire/trueskill.git
synced 2025-04-18 11:54:28 +00:00
Initial version of Moserware.Skills TrueSkill calculator to go along with my Computing Your Skill blog post
This commit is contained in:
138
Skills/Elo/DuellingEloCalculator.cs
Normal file
138
Skills/Elo/DuellingEloCalculator.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
public class DuellingEloCalculator : SkillCalculator
|
||||
{
|
||||
private readonly TwoPlayerEloCalculator _TwoPlayerEloCalc;
|
||||
|
||||
public DuellingEloCalculator(TwoPlayerEloCalculator twoPlayerEloCalculator)
|
||||
: base(SupportedOptions.None, TeamsRange.AtLeast(2), PlayersRange.AtLeast(1))
|
||||
{
|
||||
_TwoPlayerEloCalc = twoPlayerEloCalculator;
|
||||
}
|
||||
|
||||
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams, params int[] teamRanks)
|
||||
{
|
||||
// On page 6 of the TrueSkill paper, the authors write:
|
||||
// "When we had to process a team game or a game with more than two teams we used
|
||||
// the so-called *duelling* heuristic: For each player, compute the Δ's in comparison
|
||||
// to all other players based on the team outcome of the player and every other player and
|
||||
// perform an update with the average of the Δ's."
|
||||
// This implements that algorithm.
|
||||
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
RankSorter.Sort(ref teams, ref teamRanks);
|
||||
|
||||
var teamsList = teams.ToList();
|
||||
|
||||
var deltas = new Dictionary<TPlayer, IDictionary<TPlayer, double>>();
|
||||
|
||||
for(int ixCurrentTeam = 0; ixCurrentTeam < teamsList.Count; ixCurrentTeam++)
|
||||
{
|
||||
for(int ixOtherTeam = 0; ixOtherTeam < teamsList.Count; ixOtherTeam++)
|
||||
{
|
||||
if(ixOtherTeam == ixCurrentTeam)
|
||||
{
|
||||
// Shouldn't duel against ourself ;)
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentTeam = teamsList[ixCurrentTeam];
|
||||
var otherTeam = teamsList[ixOtherTeam];
|
||||
|
||||
// Remember that bigger numbers mean worse rank (e.g. other-current is what we want)
|
||||
var comparison = (PairwiseComparison) Math.Sign(teamRanks[ixOtherTeam] - teamRanks[ixCurrentTeam]);
|
||||
|
||||
foreach(var currentTeamPlayerRatingPair in currentTeam)
|
||||
{
|
||||
foreach(var otherTeamPlayerRatingPair in otherTeam)
|
||||
{
|
||||
UpdateDuels<TPlayer>(gameInfo, deltas,
|
||||
currentTeamPlayerRatingPair.Key, currentTeamPlayerRatingPair.Value,
|
||||
otherTeamPlayerRatingPair.Key, otherTeamPlayerRatingPair.Value,
|
||||
comparison);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new Dictionary<TPlayer, Rating>();
|
||||
|
||||
foreach(var currentTeam in teamsList)
|
||||
{
|
||||
foreach(var currentTeamPlayerPair in currentTeam)
|
||||
{
|
||||
var currentPlayerAverageDuellingDelta = deltas[currentTeamPlayerPair.Key].Values.Average();
|
||||
result[currentTeamPlayerPair.Key] = new EloRating(currentTeamPlayerPair.Value.Mean + currentPlayerAverageDuellingDelta);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void UpdateDuels<TPlayer>(GameInfo gameInfo,
|
||||
IDictionary<TPlayer, IDictionary<TPlayer, double>> duels,
|
||||
TPlayer player1, Rating player1Rating,
|
||||
TPlayer player2, Rating player2Rating,
|
||||
PairwiseComparison weakToStrongComparison)
|
||||
{
|
||||
|
||||
var duelOutcomes = _TwoPlayerEloCalc.CalculateNewRatings(gameInfo,
|
||||
Teams.Concat(
|
||||
new Team<TPlayer>(player1, player1Rating),
|
||||
new Team<TPlayer>(player2, player2Rating)),
|
||||
(weakToStrongComparison == PairwiseComparison.Win) ? new int[] { 1, 2 }
|
||||
: (weakToStrongComparison == PairwiseComparison.Lose) ? new int[] { 2, 1 }
|
||||
: new int[] { 1, 1});
|
||||
|
||||
|
||||
UpdateDuelInfo(duels, player1, player1Rating, duelOutcomes[player1], player2);
|
||||
UpdateDuelInfo(duels, player2, player2Rating, duelOutcomes[player2], player1);
|
||||
}
|
||||
|
||||
private static void UpdateDuelInfo<TPlayer>(IDictionary<TPlayer, IDictionary<TPlayer, double>> duels,
|
||||
TPlayer self, Rating selfBeforeRating, Rating selfAfterRating,
|
||||
TPlayer opponent )
|
||||
{
|
||||
IDictionary<TPlayer, double> selfToOpponentDuelDeltas;
|
||||
|
||||
if(!duels.TryGetValue(self, out selfToOpponentDuelDeltas))
|
||||
{
|
||||
selfToOpponentDuelDeltas = new Dictionary<TPlayer, double>();
|
||||
duels[self] = selfToOpponentDuelDeltas;
|
||||
}
|
||||
|
||||
selfToOpponentDuelDeltas[opponent] = selfAfterRating.Mean - selfBeforeRating.Mean;
|
||||
}
|
||||
|
||||
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
// HACK! Need a better algorithm, this is just to have something there and it isn't good
|
||||
double minQuality = 1.0;
|
||||
|
||||
var teamList = teams.ToList();
|
||||
|
||||
for(int ixCurrentTeam = 0; ixCurrentTeam < teamList.Count; ixCurrentTeam++)
|
||||
{
|
||||
EloRating currentTeamAverageRating = new EloRating(teamList[ixCurrentTeam].Values.Average(r => r.Mean));
|
||||
var currentTeam = new Team(new Player(ixCurrentTeam), currentTeamAverageRating);
|
||||
|
||||
for(int ixOtherTeam = ixCurrentTeam + 1; ixOtherTeam < teamList.Count; ixOtherTeam++)
|
||||
{
|
||||
EloRating otherTeamAverageRating = new EloRating(teamList[ixOtherTeam].Values.Average(r => r.Mean));
|
||||
var otherTeam = new Team(new Player(ixOtherTeam), otherTeamAverageRating);
|
||||
|
||||
minQuality = Math.Min(minQuality,
|
||||
_TwoPlayerEloCalc.CalculateMatchQuality(gameInfo,
|
||||
Teams.Concat(currentTeam, otherTeam)));
|
||||
}
|
||||
}
|
||||
|
||||
return minQuality;
|
||||
}
|
||||
}
|
||||
}
|
14
Skills/Elo/EloRating.cs
Normal file
14
Skills/Elo/EloRating.cs
Normal file
@ -0,0 +1,14 @@
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
/// <summary>
|
||||
/// An Elo rating represented by a single number (mean).
|
||||
/// </summary>
|
||||
public class EloRating : Rating
|
||||
{
|
||||
public EloRating(double rating)
|
||||
: base(rating, 0)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
31
Skills/Elo/FideEloCalculator.cs
Normal file
31
Skills/Elo/FideEloCalculator.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
// Including ELO's scheme as a simple comparison.
|
||||
// See http://en.wikipedia.org/wiki/Elo_rating_system#Theory
|
||||
// for more details
|
||||
public class FideEloCalculator : TwoPlayerEloCalculator
|
||||
{
|
||||
public FideEloCalculator()
|
||||
: this(new FideKFactor())
|
||||
{
|
||||
}
|
||||
|
||||
public FideEloCalculator(FideKFactor kFactor)
|
||||
: base(kFactor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double GetPlayerWinProbability(GameInfo gameInfo, double playerRating, double opponentRating)
|
||||
{
|
||||
double ratingDifference = opponentRating - playerRating;
|
||||
|
||||
return 1.0
|
||||
/
|
||||
(
|
||||
1.0 + Math.Pow(10.0, ratingDifference / (2 * gameInfo.Beta))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
36
Skills/Elo/FideKFactor.cs
Normal file
36
Skills/Elo/FideKFactor.cs
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
// see http://ratings.fide.com/calculator_rtd.phtml for details
|
||||
public class FideKFactor : KFactor
|
||||
{
|
||||
public FideKFactor()
|
||||
{
|
||||
}
|
||||
|
||||
public override double GetValueForRating(double rating)
|
||||
{
|
||||
if (rating < 2400)
|
||||
{
|
||||
return 15;
|
||||
}
|
||||
|
||||
return 10;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates someone who has played less than 30 games.
|
||||
/// </summary>
|
||||
public class Provisional : FideKFactor
|
||||
{
|
||||
public Provisional()
|
||||
{
|
||||
}
|
||||
|
||||
public override double GetValueForRating(double rating)
|
||||
{
|
||||
return 25;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
Skills/Elo/GaussianEloCalculator.cs
Normal file
27
Skills/Elo/GaussianEloCalculator.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
public class GaussianEloCalculator : TwoPlayerEloCalculator
|
||||
{
|
||||
// From the paper
|
||||
private static readonly KFactor StableKFactor = new KFactor(24);
|
||||
|
||||
public GaussianEloCalculator()
|
||||
: base(StableKFactor)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double GetPlayerWinProbability(GameInfo gameInfo, double playerRating, double opponentRating)
|
||||
{
|
||||
double ratingDifference = playerRating - opponentRating;
|
||||
|
||||
// See equation 1.1 in the TrueSkill paper
|
||||
return GaussianDistribution.CumulativeTo(
|
||||
ratingDifference
|
||||
/
|
||||
(Math.Sqrt(2) * gameInfo.Beta));
|
||||
}
|
||||
}
|
||||
}
|
20
Skills/Elo/GaussianKFactor.cs
Normal file
20
Skills/Elo/GaussianKFactor.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
public class GaussianKFactor : KFactor
|
||||
{
|
||||
// From paper
|
||||
const double StableDynamicsKFactor = 24.0;
|
||||
|
||||
public GaussianKFactor()
|
||||
: base(StableDynamicsKFactor)
|
||||
{
|
||||
}
|
||||
|
||||
public GaussianKFactor(GameInfo gameInfo, double latestGameWeightingFactor)
|
||||
: base(latestGameWeightingFactor * gameInfo.Beta * Math.Sqrt(Math.PI))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
Skills/Elo/KFactor.cs
Normal file
22
Skills/Elo/KFactor.cs
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
public class KFactor
|
||||
{
|
||||
private double _Value;
|
||||
|
||||
protected KFactor()
|
||||
{
|
||||
}
|
||||
|
||||
public KFactor(double exactKFactor)
|
||||
{
|
||||
_Value = exactKFactor;
|
||||
}
|
||||
|
||||
public virtual double GetValueForRating(double rating)
|
||||
{
|
||||
return _Value;
|
||||
}
|
||||
}
|
||||
}
|
79
Skills/Elo/TwoPlayerEloCalculator.cs
Normal file
79
Skills/Elo/TwoPlayerEloCalculator.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills.Elo
|
||||
{
|
||||
public abstract class TwoPlayerEloCalculator : SkillCalculator
|
||||
{
|
||||
protected readonly KFactor _KFactor;
|
||||
|
||||
protected TwoPlayerEloCalculator(KFactor kFactor)
|
||||
: base(SupportedOptions.None, TeamsRange.Exactly(2), PlayersRange.Exactly(1))
|
||||
{
|
||||
_KFactor = kFactor;
|
||||
}
|
||||
|
||||
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams, params int[] teamRanks)
|
||||
{
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
RankSorter.Sort(ref teams, ref teamRanks);
|
||||
|
||||
var result = new Dictionary<TPlayer, Rating>();
|
||||
bool isDraw = (teamRanks[0] == teamRanks[1]);
|
||||
|
||||
var player1 = teams.First().First();
|
||||
var player2 = teams.Last().First();
|
||||
|
||||
var player1Rating = player1.Value.Mean;
|
||||
var player2Rating = player2.Value.Mean;
|
||||
|
||||
result[player1.Key] = CalculateNewRating(gameInfo, player1Rating, player2Rating, isDraw ? PairwiseComparison.Draw : PairwiseComparison.Win);
|
||||
result[player2.Key] = CalculateNewRating(gameInfo, player2Rating, player1Rating, isDraw ? PairwiseComparison.Draw : PairwiseComparison.Lose);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual EloRating CalculateNewRating(GameInfo gameInfo, double selfRating, double opponentRating, PairwiseComparison selfToOpponentComparison)
|
||||
{
|
||||
double expectedProbability = GetPlayerWinProbability(gameInfo, selfRating, opponentRating);
|
||||
double actualProbability = GetScoreFromComparison(selfToOpponentComparison);
|
||||
double k = _KFactor.GetValueForRating(selfRating);
|
||||
double ratingChange = k * (actualProbability - expectedProbability);
|
||||
double newRating = selfRating + ratingChange;
|
||||
|
||||
return new EloRating(newRating);
|
||||
}
|
||||
|
||||
private static double GetScoreFromComparison(PairwiseComparison comparison)
|
||||
{
|
||||
switch (comparison)
|
||||
{
|
||||
case PairwiseComparison.Win:
|
||||
return 1;
|
||||
case PairwiseComparison.Draw:
|
||||
return 0.5;
|
||||
case PairwiseComparison.Lose:
|
||||
return 0;
|
||||
default:
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract double GetPlayerWinProbability(GameInfo gameInfo, double playerRating, double opponentRating);
|
||||
|
||||
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
double player1Rating = teams.First().First().Value.Mean;
|
||||
double player2Rating = teams.Last().First().Value.Mean;
|
||||
double ratingDifference = player1Rating - player2Rating;
|
||||
|
||||
// The TrueSkill paper mentions that they used s1 - s2 (rating difference) to
|
||||
// determine match quality. I convert that to a percentage as a delta from 50%
|
||||
// using the cumulative density function of the specific curve being used
|
||||
double deltaFrom50Percent = Math.Abs(GetPlayerWinProbability(gameInfo, player1Rating, player2Rating) - 0.5);
|
||||
return (0.5 - deltaFrom50Percent) / 0.5;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user