Initial version of Moserware.Skills TrueSkill calculator to go along with my Computing Your Skill blog post

This commit is contained in:
Jeff Moser
2010-03-18 07:39:48 -04:00
commit cb46631ff8
77 changed files with 6172 additions and 0 deletions

View 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
View 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)
{
}
}
}

View 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
View 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;
}
}
}
}

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

View 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
View 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;
}
}
}

View 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;
}
}
}