mirror of
				https://github.com/furyfire/trueskill.git
				synced 2025-11-04 10:12:28 +01: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