mirror of
https://github.com/furyfire/trueskill.git
synced 2025-01-15 17:37:39 +00:00
Initial version of Moserware.Skills TrueSkill calculator to go along with my Computing Your Skill blog post
This commit is contained in:
26
Skills.sln
Normal file
26
Skills.sln
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 10.00
|
||||
# Visual Studio 2008
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Skills", "Skills\Skills.csproj", "{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{6F80946D-AC8B-4063-8588-96841C18BF0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6F80946D-AC8B-4063-8588-96841C18BF0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6F80946D-AC8B-4063-8588-96841C18BF0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6F80946D-AC8B-4063-8588-96841C18BF0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6F80946D-AC8B-4063-8588-96841C18BF0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
BIN
Skills.suo
Normal file
BIN
Skills.suo
Normal file
Binary file not shown.
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;
|
||||
}
|
||||
}
|
||||
}
|
94
Skills/FactorGraphs/Factor.cs
Normal file
94
Skills/FactorGraphs/Factor.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public abstract class Factor<TValue>
|
||||
{
|
||||
private readonly List<Message<TValue>> _Messages = new List<Message<TValue>>();
|
||||
|
||||
private readonly Dictionary<Message<TValue>, Variable<TValue>> _MessageToVariableBinding =
|
||||
new Dictionary<Message<TValue>, Variable<TValue>>();
|
||||
|
||||
private readonly string _Name;
|
||||
private readonly List<Variable<TValue>> _Variables = new List<Variable<TValue>>();
|
||||
|
||||
protected Factor(string name)
|
||||
{
|
||||
_Name = "Factor[" + name + "]";
|
||||
}
|
||||
|
||||
/// Returns the log-normalization constant of that factor
|
||||
public virtual double LogNormalization
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
/// Returns the number of messages that the factor has
|
||||
public int NumberOfMessages
|
||||
{
|
||||
get { return _Messages.Count; }
|
||||
}
|
||||
|
||||
protected ReadOnlyCollection<Variable<TValue>> Variables
|
||||
{
|
||||
get { return _Variables.AsReadOnly(); }
|
||||
}
|
||||
|
||||
protected ReadOnlyCollection<Message<TValue>> Messages
|
||||
{
|
||||
get { return _Messages.AsReadOnly(); }
|
||||
}
|
||||
|
||||
/// Update the message and marginal of the i-th variable that the factor is connected to
|
||||
public virtual double UpdateMessage(int messageIndex)
|
||||
{
|
||||
Guard.ArgumentIsValidIndex(messageIndex, _Messages.Count, "messageIndex");
|
||||
return UpdateMessage(_Messages[messageIndex], _MessageToVariableBinding[_Messages[messageIndex]]);
|
||||
}
|
||||
|
||||
protected virtual double UpdateMessage(Message<TValue> message, Variable<TValue> variable)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// Resets the marginal of the variables a factor is connected to
|
||||
public virtual void ResetMarginals()
|
||||
{
|
||||
foreach (var currentVariable in _MessageToVariableBinding.Values)
|
||||
{
|
||||
currentVariable.ResetToPrior();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the ith message to the marginal and returns the log-normalization constant
|
||||
public virtual double SendMessage(int messageIndex)
|
||||
{
|
||||
Guard.ArgumentIsValidIndex(messageIndex, _Messages.Count, "messageIndex");
|
||||
|
||||
Message<TValue> message = _Messages[messageIndex];
|
||||
Variable<TValue> variable = _MessageToVariableBinding[message];
|
||||
return SendMessage(message, variable);
|
||||
}
|
||||
|
||||
protected abstract double SendMessage(Message<TValue> message, Variable<TValue> variable);
|
||||
|
||||
public abstract Message<TValue> CreateVariableToMessageBinding(Variable<TValue> variable);
|
||||
|
||||
protected Message<TValue> CreateVariableToMessageBinding(Variable<TValue> variable, Message<TValue> message)
|
||||
{
|
||||
int index = _Messages.Count;
|
||||
_Messages.Add(message);
|
||||
_MessageToVariableBinding[message] = variable;
|
||||
_Variables.Add(variable);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Name ?? base.ToString();
|
||||
}
|
||||
}
|
||||
}
|
9
Skills/FactorGraphs/FactorGraph.cs
Normal file
9
Skills/FactorGraphs/FactorGraph.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public class FactorGraph<TSelf, TValue, TVariable>
|
||||
where TSelf : FactorGraph<TSelf, TValue, TVariable>
|
||||
where TVariable : Variable<TValue>
|
||||
{
|
||||
public VariableFactory<TValue> VariableFactory { get; protected set; }
|
||||
}
|
||||
}
|
108
Skills/FactorGraphs/FactorGraphLayer.cs
Normal file
108
Skills/FactorGraphs/FactorGraphLayer.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public abstract class FactorGraphLayerBase<TValue>
|
||||
{
|
||||
public abstract IEnumerable<Factor<TValue>> UntypedFactors { get; }
|
||||
public abstract void BuildLayer();
|
||||
|
||||
public virtual Schedule<TValue> CreatePriorSchedule()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual Schedule<TValue> CreatePosteriorSchedule()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// HACK
|
||||
|
||||
public abstract void SetRawInputVariablesGroups(object value);
|
||||
public abstract object GetRawOutputVariablesGroups();
|
||||
}
|
||||
|
||||
public abstract class FactorGraphLayer<TParentGraph, TValue, TBaseVariable, TInputVariable, TFactor, TOutputVariable>
|
||||
: FactorGraphLayerBase<TValue>
|
||||
where TParentGraph : FactorGraph<TParentGraph, TValue, TBaseVariable>
|
||||
where TBaseVariable : Variable<TValue>
|
||||
where TInputVariable : TBaseVariable
|
||||
where TFactor : Factor<TValue>
|
||||
where TOutputVariable : TBaseVariable
|
||||
{
|
||||
private readonly List<TFactor> _LocalFactors = new List<TFactor>();
|
||||
private readonly List<IList<TOutputVariable>> _OutputVariablesGroups = new List<IList<TOutputVariable>>();
|
||||
private IList<IList<TInputVariable>> _InputVariablesGroups = new List<IList<TInputVariable>>();
|
||||
|
||||
protected FactorGraphLayer(TParentGraph parentGraph)
|
||||
{
|
||||
ParentFactorGraph = parentGraph;
|
||||
}
|
||||
|
||||
protected IList<IList<TInputVariable>> InputVariablesGroups
|
||||
{
|
||||
get { return _InputVariablesGroups; }
|
||||
}
|
||||
|
||||
// HACK
|
||||
|
||||
public TParentGraph ParentFactorGraph { get; private set; }
|
||||
|
||||
public IList<IList<TOutputVariable>> OutputVariablesGroups
|
||||
{
|
||||
get { return _OutputVariablesGroups; }
|
||||
}
|
||||
|
||||
public IList<TFactor> LocalFactors
|
||||
{
|
||||
get { return _LocalFactors; }
|
||||
}
|
||||
|
||||
public override IEnumerable<Factor<TValue>> UntypedFactors
|
||||
{
|
||||
get { return _LocalFactors.Cast<Factor<TValue>>(); }
|
||||
}
|
||||
|
||||
public override void SetRawInputVariablesGroups(object value)
|
||||
{
|
||||
var newList = value as IList<IList<TInputVariable>>;
|
||||
if (newList == null)
|
||||
{
|
||||
// TODO: message
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
_InputVariablesGroups = newList;
|
||||
}
|
||||
|
||||
public override object GetRawOutputVariablesGroups()
|
||||
{
|
||||
return _OutputVariablesGroups;
|
||||
}
|
||||
|
||||
protected Schedule<TValue> ScheduleSequence<TSchedule>(
|
||||
IEnumerable<TSchedule> itemsToSequence,
|
||||
string nameFormat,
|
||||
params object[] args)
|
||||
where TSchedule : Schedule<TValue>
|
||||
|
||||
{
|
||||
string formattedName = String.Format(nameFormat, args);
|
||||
return new ScheduleSequence<TValue, TSchedule>(formattedName, itemsToSequence);
|
||||
}
|
||||
|
||||
protected void AddLayerFactor(TFactor factor)
|
||||
{
|
||||
_LocalFactors.Add(factor);
|
||||
}
|
||||
|
||||
// Helper utility
|
||||
protected double Square(double x)
|
||||
{
|
||||
return x*x;
|
||||
}
|
||||
}
|
||||
}
|
47
Skills/FactorGraphs/FactorList.cs
Normal file
47
Skills/FactorGraphs/FactorList.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for computing the factor graph's normalization constant.
|
||||
/// </summary>
|
||||
public class FactorList<TValue>
|
||||
{
|
||||
private readonly List<Factor<TValue>> _List = new List<Factor<TValue>>();
|
||||
|
||||
public double LogNormalization
|
||||
{
|
||||
get
|
||||
{
|
||||
_List.ForEach(f => f.ResetMarginals());
|
||||
|
||||
double sumLogZ = 0.0;
|
||||
|
||||
for (int i = 0; i < _List.Count; i++)
|
||||
{
|
||||
Factor<TValue> f = _List[i];
|
||||
for (int j = 0; j < f.NumberOfMessages; j++)
|
||||
{
|
||||
sumLogZ += f.SendMessage(j);
|
||||
}
|
||||
}
|
||||
|
||||
double sumLogS = _List.Aggregate(0.0, (acc, fac) => acc + fac.LogNormalization);
|
||||
|
||||
return sumLogZ + sumLogS;
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { return _List.Count; }
|
||||
}
|
||||
|
||||
public Factor<TValue> AddFactor(Factor<TValue> factor)
|
||||
{
|
||||
_List.Add(factor);
|
||||
return factor;
|
||||
}
|
||||
}
|
||||
}
|
30
Skills/FactorGraphs/Message.cs
Normal file
30
Skills/FactorGraphs/Message.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public class Message<T>
|
||||
{
|
||||
private readonly string _NameFormat;
|
||||
private readonly object[] _NameFormatArgs;
|
||||
|
||||
public Message()
|
||||
: this(default(T), null, null)
|
||||
{
|
||||
}
|
||||
|
||||
public Message(T value, string nameFormat, params object[] args)
|
||||
|
||||
{
|
||||
_NameFormat = nameFormat;
|
||||
_NameFormatArgs = args;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public T Value { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return (_NameFormat == null) ? base.ToString() : String.Format(_NameFormat, _NameFormatArgs);
|
||||
}
|
||||
}
|
||||
}
|
105
Skills/FactorGraphs/Schedule.cs
Normal file
105
Skills/FactorGraphs/Schedule.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public abstract class Schedule<T>
|
||||
{
|
||||
private readonly string _Name;
|
||||
|
||||
protected Schedule(string name)
|
||||
{
|
||||
_Name = name;
|
||||
}
|
||||
|
||||
public abstract double Visit(int depth, int maxDepth);
|
||||
|
||||
public double Visit()
|
||||
{
|
||||
return Visit(-1, 0);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Name;
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleStep<T> : Schedule<T>
|
||||
{
|
||||
private readonly Factor<T> _Factor;
|
||||
private readonly int _Index;
|
||||
|
||||
public ScheduleStep(string name, Factor<T> factor, int index)
|
||||
: base(name)
|
||||
{
|
||||
_Factor = factor;
|
||||
_Index = index;
|
||||
}
|
||||
|
||||
public override double Visit(int depth, int maxDepth)
|
||||
{
|
||||
double delta = _Factor.UpdateMessage(_Index);
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove
|
||||
public class ScheduleSequence<TValue> : ScheduleSequence<TValue, Schedule<TValue>>
|
||||
{
|
||||
public ScheduleSequence(string name, IEnumerable<Schedule<TValue>> schedules)
|
||||
: base(name, schedules)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleSequence<TValue, TSchedule> : Schedule<TValue>
|
||||
where TSchedule : Schedule<TValue>
|
||||
{
|
||||
private readonly IEnumerable<TSchedule> _Schedules;
|
||||
|
||||
public ScheduleSequence(string name, IEnumerable<TSchedule> schedules)
|
||||
: base(name)
|
||||
{
|
||||
_Schedules = schedules;
|
||||
}
|
||||
|
||||
public override double Visit(int depth, int maxDepth)
|
||||
{
|
||||
double maxDelta = 0;
|
||||
|
||||
foreach (TSchedule currentSchedule in _Schedules)
|
||||
{
|
||||
maxDelta = Math.Max(currentSchedule.Visit(depth + 1, maxDepth), maxDelta);
|
||||
}
|
||||
|
||||
return maxDelta;
|
||||
}
|
||||
}
|
||||
|
||||
public class ScheduleLoop<T> : Schedule<T>
|
||||
{
|
||||
private readonly double _MaxDelta;
|
||||
private readonly Schedule<T> _ScheduleToLoop;
|
||||
|
||||
public ScheduleLoop(string name, Schedule<T> scheduleToLoop, double maxDelta)
|
||||
: base(name)
|
||||
{
|
||||
_ScheduleToLoop = scheduleToLoop;
|
||||
_MaxDelta = maxDelta;
|
||||
}
|
||||
|
||||
public override double Visit(int depth, int maxDepth)
|
||||
{
|
||||
int totalIterations = 1;
|
||||
double delta = _ScheduleToLoop.Visit(depth + 1, maxDepth);
|
||||
while (delta > _MaxDelta)
|
||||
{
|
||||
delta = _ScheduleToLoop.Visit(depth + 1, maxDepth);
|
||||
totalIterations++;
|
||||
}
|
||||
|
||||
return delta;
|
||||
}
|
||||
}
|
||||
}
|
58
Skills/FactorGraphs/Variable.cs
Normal file
58
Skills/FactorGraphs/Variable.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public class Variable<TValue>
|
||||
{
|
||||
private readonly string _Name;
|
||||
private readonly VariableFactory<TValue> _ParentFactory;
|
||||
private readonly TValue _Prior;
|
||||
private int _ParentIndex;
|
||||
|
||||
public Variable(string name, VariableFactory<TValue> parentFactory, int parentIndex, TValue prior)
|
||||
{
|
||||
_Name = "Variable[" + name + "]";
|
||||
_ParentFactory = parentFactory;
|
||||
_ParentIndex = parentIndex;
|
||||
_Prior = prior;
|
||||
ResetToPrior();
|
||||
}
|
||||
|
||||
public virtual TValue Value { get; set; }
|
||||
|
||||
public void ResetToPrior()
|
||||
{
|
||||
Value = _Prior;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _Name;
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultVariable<TValue> : Variable<TValue>
|
||||
{
|
||||
public DefaultVariable()
|
||||
: base("Default", null, 0, default(TValue))
|
||||
{
|
||||
}
|
||||
|
||||
public override TValue Value
|
||||
{
|
||||
get { return default(TValue); }
|
||||
set { throw new NotSupportedException(); }
|
||||
}
|
||||
}
|
||||
|
||||
public class KeyedVariable<TKey, TValue> : Variable<TValue>
|
||||
{
|
||||
public KeyedVariable(TKey key, string name, VariableFactory<TValue> parentFactory, int parentIndex, TValue prior)
|
||||
: base(name, parentFactory, parentIndex, prior)
|
||||
{
|
||||
Key = key;
|
||||
}
|
||||
|
||||
public TKey Key { get; private set; }
|
||||
}
|
||||
}
|
42
Skills/FactorGraphs/VariableFactory.cs
Normal file
42
Skills/FactorGraphs/VariableFactory.cs
Normal file
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Moserware.Skills.FactorGraphs
|
||||
{
|
||||
public class VariableFactory<TValue>
|
||||
{
|
||||
// using a Func<TValue> to encourage fresh copies in case it's overwritten
|
||||
private readonly List<Variable<TValue>> _CreatedVariables = new List<Variable<TValue>>();
|
||||
private readonly Func<TValue> _VariablePriorInitializer;
|
||||
|
||||
public VariableFactory(Func<TValue> variablePriorInitializer)
|
||||
{
|
||||
_VariablePriorInitializer = variablePriorInitializer;
|
||||
}
|
||||
|
||||
public Variable<TValue> CreateBasicVariable(string nameFormat, params object[] args)
|
||||
{
|
||||
var newVar = new Variable<TValue>(
|
||||
String.Format(nameFormat, args),
|
||||
this,
|
||||
_CreatedVariables.Count,
|
||||
_VariablePriorInitializer());
|
||||
|
||||
_CreatedVariables.Add(newVar);
|
||||
return newVar;
|
||||
}
|
||||
|
||||
public KeyedVariable<TKey, TValue> CreateKeyedVariable<TKey>(TKey key, string nameFormat, params object[] args)
|
||||
{
|
||||
var newVar = new KeyedVariable<TKey, TValue>(
|
||||
key,
|
||||
String.Format(nameFormat, args),
|
||||
this,
|
||||
_CreatedVariables.Count,
|
||||
_VariablePriorInitializer());
|
||||
|
||||
_CreatedVariables.Add(newVar);
|
||||
return newVar;
|
||||
}
|
||||
}
|
||||
}
|
49
Skills/GameInfo.cs
Normal file
49
Skills/GameInfo.cs
Normal file
@ -0,0 +1,49 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Parameters about the game for calculating the TrueSkill.
|
||||
/// </summary>
|
||||
public class GameInfo
|
||||
{
|
||||
private const double DefaultBeta = DefaultInitialMean/6.0;
|
||||
private const double DefaultDrawProbability = 0.10;
|
||||
private const double DefaultDynamicsFactor = DefaultInitialMean/300.0;
|
||||
private const double DefaultInitialMean = 25.0;
|
||||
private const double DefaultInitialStandardDeviation = DefaultInitialMean/3.0;
|
||||
|
||||
public GameInfo(double initialMean, double initialStandardDeviation, double beta, double dynamicFactor,
|
||||
double drawProbability)
|
||||
{
|
||||
InitialMean = initialMean;
|
||||
InitialStandardDeviation = initialStandardDeviation;
|
||||
Beta = beta;
|
||||
DynamicsFactor = dynamicFactor;
|
||||
DrawProbability = drawProbability;
|
||||
}
|
||||
|
||||
public double InitialMean { get; set; }
|
||||
public double InitialStandardDeviation { get; set; }
|
||||
public double Beta { get; set; }
|
||||
|
||||
public double DynamicsFactor { get; set; }
|
||||
public double DrawProbability { get; set; }
|
||||
|
||||
public Rating DefaultRating
|
||||
{
|
||||
get { return new Rating(InitialMean, InitialStandardDeviation); }
|
||||
}
|
||||
|
||||
public static GameInfo DefaultGameInfo
|
||||
{
|
||||
get
|
||||
{
|
||||
// We return a fresh copy since we have public setters that can mutate state
|
||||
return new GameInfo(DefaultInitialMean,
|
||||
DefaultInitialStandardDeviation,
|
||||
DefaultBeta,
|
||||
DefaultDynamicsFactor,
|
||||
DefaultDrawProbability);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
Skills/Guard.cs
Normal file
36
Skills/Guard.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies argument contracts.
|
||||
/// </summary>
|
||||
/// <remarks>These are used until .NET 4.0 ships with Contracts. For more information,
|
||||
/// see http://www.moserware.com/2008/01/borrowing-ideas-from-3-interesting.html</remarks>
|
||||
internal static class Guard
|
||||
{
|
||||
public static void ArgumentNotNull(object value, string parameterName)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
throw new ArgumentNullException(parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ArgumentIsValidIndex(int index, int count, string parameterName)
|
||||
{
|
||||
if ((index < 0) || (index >= count))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ArgumentInRangeInclusive(double value, double min, double max, string parameterName)
|
||||
{
|
||||
if ((value < min) || (value > max))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(parameterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
Skills/ISupportPartialPlay.cs
Normal file
13
Skills/ISupportPartialPlay.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates support for allowing partial play (where a player only plays a part of the time).
|
||||
/// </summary>
|
||||
public interface ISupportPartialPlay
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates the percent of the time the player should be weighted where 0.0 indicates the player didn't play and 1.0 indicates the player played 100% of the time.
|
||||
/// </summary>
|
||||
double PartialPlayPercentage { get; }
|
||||
}
|
||||
}
|
10
Skills/ISupportPartialUpdate.cs
Normal file
10
Skills/ISupportPartialUpdate.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
public interface ISupportPartialUpdate
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicated how much of a skill update a player should receive where 0.0 represents no update and 1.0 represents 100% of the update.
|
||||
/// </summary>
|
||||
double PartialUpdatePercentage { get; }
|
||||
}
|
||||
}
|
37
Skills/License.txt
Normal file
37
Skills/License.txt
Normal file
@ -0,0 +1,37 @@
|
||||
The core ideas used in this Moserware.Skills project were described in
|
||||
"TrueSkill (TM): A Bayesian Skill Rating System" available at
|
||||
http://research.microsoft.com/apps/pubs/default.aspx?id=67956
|
||||
|
||||
The authors of the above paper have asked for a link to that article
|
||||
as attribution in derived works.
|
||||
|
||||
Some concepts were based on sample F# code that was written by Ralf Herbrich
|
||||
Copyright (c) 2007, 2008 Microsoft Research Ltd, available at
|
||||
http://blogs.technet.com/apg/archive/2008/06/16/trueskill-in-f.aspx
|
||||
|
||||
All the C# code in this Moserware.Skills project is
|
||||
Copyright (c) 2010 Jeff Moser <jeff@moserware.com>
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are
|
||||
permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
of conditions and the following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY JEFF MOSER ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JEFF MOSER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those of the
|
||||
authors and should not be interpreted as representing official policies, either expressed
|
||||
or implied, of Jeff Moser.
|
240
Skills/Numerics/GaussianDistribution.cs
Normal file
240
Skills/Numerics/GaussianDistribution.cs
Normal file
@ -0,0 +1,240 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Numerics
|
||||
{
|
||||
public class GaussianDistribution
|
||||
{
|
||||
// Intentionally, we're not going to derive related things, but set them all at once
|
||||
// to get around some NaN issues
|
||||
|
||||
private GaussianDistribution()
|
||||
{
|
||||
}
|
||||
|
||||
public GaussianDistribution(double mean, double standardDeviation)
|
||||
{
|
||||
Mean = mean;
|
||||
StandardDeviation = standardDeviation;
|
||||
Variance = Square(StandardDeviation);
|
||||
Precision = 1.0/Variance;
|
||||
PrecisionMean = Precision*Mean;
|
||||
}
|
||||
|
||||
public double Mean { get; private set; }
|
||||
public double StandardDeviation { get; private set; }
|
||||
|
||||
// Precision and PrecisionMean are used because they make multiplying and dividing simpler
|
||||
// (the the accompanying math paper for more details)
|
||||
|
||||
public double Precision { get; private set; }
|
||||
|
||||
public double PrecisionMean { get; private set; }
|
||||
|
||||
private double Variance { get; set; }
|
||||
|
||||
public double NormalizationConstant
|
||||
{
|
||||
get
|
||||
{
|
||||
// Great derivation of this is at http://www.astro.psu.edu/~mce/A451_2/A451/downloads/notes0.pdf
|
||||
return 1.0/(Math.Sqrt(2*Math.PI)*StandardDeviation);
|
||||
}
|
||||
}
|
||||
|
||||
public GaussianDistribution Clone()
|
||||
{
|
||||
var result = new GaussianDistribution();
|
||||
result.Mean = Mean;
|
||||
result.StandardDeviation = StandardDeviation;
|
||||
result.Variance = Variance;
|
||||
result.Precision = Precision;
|
||||
result.PrecisionMean = PrecisionMean;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static GaussianDistribution FromPrecisionMean(double precisionMean, double precision)
|
||||
{
|
||||
var gaussianDistribution = new GaussianDistribution();
|
||||
gaussianDistribution.Precision = precision;
|
||||
gaussianDistribution.PrecisionMean = precisionMean;
|
||||
gaussianDistribution.Variance = 1.0/precision;
|
||||
gaussianDistribution.StandardDeviation = Math.Sqrt(gaussianDistribution.Variance);
|
||||
gaussianDistribution.Mean = gaussianDistribution.PrecisionMean/gaussianDistribution.Precision;
|
||||
return gaussianDistribution;
|
||||
}
|
||||
|
||||
// Although we could use equations from // For details, see http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf
|
||||
// for multiplication, the precision mean ones are easier to write :)
|
||||
public static GaussianDistribution operator *(GaussianDistribution left, GaussianDistribution right)
|
||||
{
|
||||
return FromPrecisionMean(left.PrecisionMean + right.PrecisionMean, left.Precision + right.Precision);
|
||||
}
|
||||
|
||||
/// Computes the absolute difference between two Gaussians
|
||||
public static double AbsoluteDifference(GaussianDistribution left, GaussianDistribution right)
|
||||
{
|
||||
return Math.Max(
|
||||
Math.Abs(left.PrecisionMean - right.PrecisionMean),
|
||||
Math.Sqrt(Math.Abs(left.Precision - right.Precision)));
|
||||
}
|
||||
|
||||
/// Computes the absolute difference between two Gaussians
|
||||
public static double operator -(GaussianDistribution left, GaussianDistribution right)
|
||||
{
|
||||
return AbsoluteDifference(left, right);
|
||||
}
|
||||
|
||||
public static double LogProductNormalization(GaussianDistribution left, GaussianDistribution right)
|
||||
{
|
||||
if ((left.Precision == 0) || (right.Precision == 0))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double varianceSum = left.Variance + right.Variance;
|
||||
double meanDifference = left.Mean - right.Mean;
|
||||
|
||||
double logSqrt2Pi = Math.Log(Math.Sqrt(2*Math.PI));
|
||||
return -logSqrt2Pi - (Math.Log(varianceSum)/2.0) - (Square(meanDifference)/(2.0*varianceSum));
|
||||
}
|
||||
|
||||
|
||||
public static GaussianDistribution operator /(GaussianDistribution numerator, GaussianDistribution denominator)
|
||||
{
|
||||
return FromPrecisionMean(numerator.PrecisionMean - denominator.PrecisionMean,
|
||||
numerator.Precision - denominator.Precision);
|
||||
}
|
||||
|
||||
public static double LogRatioNormalization(GaussianDistribution numerator, GaussianDistribution denominator)
|
||||
{
|
||||
if ((numerator.Precision == 0) || (denominator.Precision == 0))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double varianceDifference = denominator.Variance - numerator.Variance;
|
||||
double meanDifference = numerator.Mean - denominator.Mean;
|
||||
|
||||
double logSqrt2Pi = Math.Log(Math.Sqrt(2*Math.PI));
|
||||
|
||||
return Math.Log(denominator.Variance) + logSqrt2Pi - Math.Log(varianceDifference)/2.0 +
|
||||
Square(meanDifference)/(2*varianceDifference);
|
||||
}
|
||||
|
||||
private static double Square(double x)
|
||||
{
|
||||
return x*x;
|
||||
}
|
||||
|
||||
public static double At(double x)
|
||||
{
|
||||
return At(x, 0, 1);
|
||||
}
|
||||
|
||||
public static double At(double x, double mean, double standardDeviation)
|
||||
{
|
||||
// See http://mathworld.wolfram.com/NormalDistribution.html
|
||||
// 1 -(x-mean)^2 / (2*stdDev^2)
|
||||
// P(x) = ------------------- * e
|
||||
// stdDev * sqrt(2*pi)
|
||||
|
||||
double multiplier = 1.0/(standardDeviation*Math.Sqrt(2*Math.PI));
|
||||
double expPart = Math.Exp((-1.0*Math.Pow(x - mean, 2.0))/(2*(standardDeviation*standardDeviation)));
|
||||
double result = multiplier*expPart;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double CumulativeTo(double x, double mean, double standardDeviation)
|
||||
{
|
||||
double invsqrt2 = -0.707106781186547524400844362104;
|
||||
double result = ErrorFunctionCumulativeTo(invsqrt2*x);
|
||||
return 0.5*result;
|
||||
}
|
||||
|
||||
public static double CumulativeTo(double x)
|
||||
{
|
||||
return CumulativeTo(x, 0, 1);
|
||||
}
|
||||
|
||||
private static double ErrorFunctionCumulativeTo(double x)
|
||||
{
|
||||
// Derived from page 265 of Numerical Recipes 3rd Edition
|
||||
double z = Math.Abs(x);
|
||||
|
||||
double t = 2.0/(2.0 + z);
|
||||
double ty = 4*t - 2;
|
||||
|
||||
double[] coefficients = {
|
||||
-1.3026537197817094, 6.4196979235649026e-1,
|
||||
1.9476473204185836e-2, -9.561514786808631e-3, -9.46595344482036e-4,
|
||||
3.66839497852761e-4, 4.2523324806907e-5, -2.0278578112534e-5,
|
||||
-1.624290004647e-6, 1.303655835580e-6, 1.5626441722e-8, -8.5238095915e-8,
|
||||
6.529054439e-9, 5.059343495e-9, -9.91364156e-10, -2.27365122e-10,
|
||||
9.6467911e-11, 2.394038e-12, -6.886027e-12, 8.94487e-13, 3.13092e-13,
|
||||
-1.12708e-13, 3.81e-16, 7.106e-15, -1.523e-15, -9.4e-17, 1.21e-16, -2.8e-17
|
||||
};
|
||||
|
||||
int ncof = coefficients.Length;
|
||||
double d = 0.0;
|
||||
double dd = 0.0;
|
||||
|
||||
|
||||
for (int j = ncof - 1; j > 0; j--)
|
||||
{
|
||||
double tmp = d;
|
||||
d = ty*d - dd + coefficients[j];
|
||||
dd = tmp;
|
||||
}
|
||||
|
||||
double ans = t*Math.Exp(-z*z + 0.5*(coefficients[0] + ty*d) - dd);
|
||||
return x >= 0.0 ? ans : (2.0 - ans);
|
||||
}
|
||||
|
||||
|
||||
private static double InverseErrorFunctionCumulativeTo(double p)
|
||||
{
|
||||
// From page 265 of numerical recipes
|
||||
|
||||
if (p >= 2.0)
|
||||
{
|
||||
return -100;
|
||||
}
|
||||
if (p <= 0.0)
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
double pp = (p < 1.0) ? p : 2 - p;
|
||||
double t = Math.Sqrt(-2*Math.Log(pp/2.0)); // Initial guess
|
||||
double x = -0.70711*((2.30753 + t*0.27061)/(1.0 + t*(0.99229 + t*0.04481)) - t);
|
||||
|
||||
for (int j = 0; j < 2; j++)
|
||||
{
|
||||
double err = ErrorFunctionCumulativeTo(x) - pp;
|
||||
x += err/(1.12837916709551257*Math.Exp(-(x*x)) - x*err); // Halley
|
||||
}
|
||||
|
||||
return p < 1.0 ? x : -x;
|
||||
}
|
||||
|
||||
public static double InverseCumulativeTo(double x, double mean, double standardDeviation)
|
||||
{
|
||||
// From numerical recipes, page 320
|
||||
return mean - Math.Sqrt(2)*standardDeviation*InverseErrorFunctionCumulativeTo(2*x);
|
||||
}
|
||||
|
||||
public static double InverseCumulativeTo(double x)
|
||||
{
|
||||
return InverseCumulativeTo(x, 0, 1);
|
||||
}
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
// Debug help
|
||||
return String.Format("μ={0:0.0000}, σ={1:0.0000}",
|
||||
Mean,
|
||||
StandardDeviation);
|
||||
}
|
||||
}
|
||||
}
|
520
Skills/Numerics/Matrix.cs
Normal file
520
Skills/Numerics/Matrix.cs
Normal file
@ -0,0 +1,520 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Numerics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an MxN matrix with double precision values.
|
||||
/// </summary>
|
||||
internal class Matrix
|
||||
{
|
||||
protected double[][] _MatrixRowValues;
|
||||
// Note: some properties like Determinant, Inverse, etc are properties instead
|
||||
// of methods to make the syntax look nicer even though this sort of goes against
|
||||
// Framework Design Guidelines that properties should be "cheap" since it could take
|
||||
// a long time to compute these properties if the matrices are "big."
|
||||
|
||||
protected Matrix()
|
||||
{
|
||||
}
|
||||
|
||||
public Matrix(int rows, int columns, params double[] allRowValues)
|
||||
{
|
||||
Rows = rows;
|
||||
Columns = columns;
|
||||
|
||||
_MatrixRowValues = new double[rows][];
|
||||
|
||||
int currentIndex = 0;
|
||||
for (int currentRow = 0; currentRow < Rows; currentRow++)
|
||||
{
|
||||
_MatrixRowValues[currentRow] = new double[Columns];
|
||||
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
if ((allRowValues != null) && (currentIndex < allRowValues.Length))
|
||||
{
|
||||
_MatrixRowValues[currentRow][currentColumn] = allRowValues[currentIndex++];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Matrix(double[][] rowValues)
|
||||
{
|
||||
if (!rowValues.All(row => row.Length == rowValues[0].Length))
|
||||
{
|
||||
throw new ArgumentException("All rows must be the same length!");
|
||||
}
|
||||
|
||||
Rows = rowValues.Length;
|
||||
Columns = rowValues[0].Length;
|
||||
_MatrixRowValues = rowValues;
|
||||
}
|
||||
|
||||
protected Matrix(int rows, int columns, double[][] matrixRowValues)
|
||||
{
|
||||
Rows = rows;
|
||||
Columns = columns;
|
||||
_MatrixRowValues = matrixRowValues;
|
||||
}
|
||||
|
||||
public Matrix(int rows, int columns, IEnumerable<IEnumerable<double>> columnValues)
|
||||
: this(rows, columns)
|
||||
{
|
||||
int columnIndex = 0;
|
||||
|
||||
foreach (var currentColumn in columnValues)
|
||||
{
|
||||
int rowIndex = 0;
|
||||
foreach (double currentColumnValue in currentColumn)
|
||||
{
|
||||
_MatrixRowValues[rowIndex++][columnIndex] = currentColumnValue;
|
||||
}
|
||||
columnIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
public int Rows { get; protected set; }
|
||||
public int Columns { get; protected set; }
|
||||
|
||||
public double this[int row, int column]
|
||||
{
|
||||
get { return _MatrixRowValues[row][column]; }
|
||||
}
|
||||
|
||||
public Matrix Transpose
|
||||
{
|
||||
get
|
||||
{
|
||||
// Just flip everything
|
||||
var transposeMatrix = new double[Columns][];
|
||||
for (int currentRowTransposeMatrix = 0;
|
||||
currentRowTransposeMatrix < Columns;
|
||||
currentRowTransposeMatrix++)
|
||||
{
|
||||
var transposeMatrixCurrentRowColumnValues = new double[Rows];
|
||||
transposeMatrix[currentRowTransposeMatrix] = transposeMatrixCurrentRowColumnValues;
|
||||
|
||||
for (int currentColumnTransposeMatrix = 0;
|
||||
currentColumnTransposeMatrix < Rows;
|
||||
currentColumnTransposeMatrix++)
|
||||
{
|
||||
transposeMatrixCurrentRowColumnValues[currentColumnTransposeMatrix] =
|
||||
_MatrixRowValues[currentColumnTransposeMatrix][currentRowTransposeMatrix];
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(Columns, Rows, transposeMatrix);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsSquare
|
||||
{
|
||||
get { return (Rows == Columns) && Rows > 0; }
|
||||
}
|
||||
|
||||
public double Determinant
|
||||
{
|
||||
get
|
||||
{
|
||||
// Basic argument checking
|
||||
if (!IsSquare)
|
||||
{
|
||||
throw new NotSupportedException("Matrix must be square!");
|
||||
}
|
||||
|
||||
if (Rows == 1)
|
||||
{
|
||||
// Really happy path :)
|
||||
return _MatrixRowValues[0][0];
|
||||
}
|
||||
|
||||
if (Rows == 2)
|
||||
{
|
||||
// Happy path!
|
||||
// Given:
|
||||
// | a b |
|
||||
// | c d |
|
||||
// The determinant is ad - bc
|
||||
double a = _MatrixRowValues[0][0];
|
||||
double b = _MatrixRowValues[0][1];
|
||||
double c = _MatrixRowValues[1][0];
|
||||
double d = _MatrixRowValues[1][1];
|
||||
return a*d - b*c;
|
||||
}
|
||||
|
||||
// I use the Laplace expansion here since it's straightforward to implement.
|
||||
// It's O(n^2) and my implementation is especially poor performing, but the
|
||||
// core idea is there. Perhaps I should replace it with a better algorithm
|
||||
// later.
|
||||
// See http://en.wikipedia.org/wiki/Laplace_expansion for details
|
||||
|
||||
double result = 0.0;
|
||||
|
||||
// I expand along the first row
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
double firstRowColValue = _MatrixRowValues[0][currentColumn];
|
||||
double cofactor = GetCofactor(0, currentColumn);
|
||||
double itemToAdd = firstRowColValue*cofactor;
|
||||
result += itemToAdd;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public Matrix Adjugate
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsSquare)
|
||||
{
|
||||
throw new ArgumentException("Matrix must be square!");
|
||||
}
|
||||
|
||||
// See http://en.wikipedia.org/wiki/Adjugate_matrix
|
||||
if (Rows == 2)
|
||||
{
|
||||
// Happy path!
|
||||
// Adjugate of:
|
||||
// | a b |
|
||||
// | c d |
|
||||
// is
|
||||
// | d -b |
|
||||
// | -c a |
|
||||
|
||||
double a = _MatrixRowValues[0][0];
|
||||
double b = _MatrixRowValues[0][1];
|
||||
double c = _MatrixRowValues[1][0];
|
||||
double d = _MatrixRowValues[1][1];
|
||||
|
||||
return new SquareMatrix(d, -b,
|
||||
-c, a);
|
||||
}
|
||||
|
||||
// The idea is that it's the transpose of the cofactors
|
||||
var result = new double[Columns][];
|
||||
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
result[currentColumn] = new double[Rows];
|
||||
|
||||
for (int currentRow = 0; currentRow < Rows; currentRow++)
|
||||
{
|
||||
result[currentColumn][currentRow] = GetCofactor(currentRow, currentColumn);
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(result);
|
||||
}
|
||||
}
|
||||
|
||||
public Matrix Inverse
|
||||
{
|
||||
get
|
||||
{
|
||||
if ((Rows == 1) && (Columns == 1))
|
||||
{
|
||||
return new SquareMatrix(1.0/_MatrixRowValues[0][0]);
|
||||
}
|
||||
|
||||
// Take the simple approach:
|
||||
// http://en.wikipedia.org/wiki/Cramer%27s_rule#Finding_inverse_matrix
|
||||
return (1.0/Determinant)*Adjugate;
|
||||
}
|
||||
}
|
||||
|
||||
public static Matrix operator *(double scalarValue, Matrix matrix)
|
||||
{
|
||||
int rows = matrix.Rows;
|
||||
int columns = matrix.Columns;
|
||||
var newValues = new double[rows][];
|
||||
|
||||
for (int currentRow = 0; currentRow < rows; currentRow++)
|
||||
{
|
||||
var newRowColumnValues = new double[columns];
|
||||
newValues[currentRow] = newRowColumnValues;
|
||||
|
||||
for (int currentColumn = 0; currentColumn < columns; currentColumn++)
|
||||
{
|
||||
newRowColumnValues[currentColumn] = scalarValue*matrix._MatrixRowValues[currentRow][currentColumn];
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(rows, columns, newValues);
|
||||
}
|
||||
|
||||
public static Matrix operator +(Matrix left, Matrix right)
|
||||
{
|
||||
if ((left.Rows != right.Rows) || (left.Columns != right.Columns))
|
||||
{
|
||||
throw new ArgumentException("Matrices must be of the same size");
|
||||
}
|
||||
|
||||
// simple addition of each item
|
||||
|
||||
var resultMatrix = new double[left.Rows][];
|
||||
|
||||
for (int currentRow = 0; currentRow < left.Rows; currentRow++)
|
||||
{
|
||||
var rowColumnValues = new double[right.Columns];
|
||||
resultMatrix[currentRow] = rowColumnValues;
|
||||
for (int currentColumn = 0; currentColumn < right.Columns; currentColumn++)
|
||||
{
|
||||
rowColumnValues[currentColumn] = left._MatrixRowValues[currentRow][currentColumn]
|
||||
+
|
||||
right._MatrixRowValues[currentRow][currentColumn];
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(left.Rows, right.Columns, resultMatrix);
|
||||
}
|
||||
|
||||
public static Matrix operator *(Matrix left, Matrix right)
|
||||
{
|
||||
// Just your standard matrix multiplication.
|
||||
// See http://en.wikipedia.org/wiki/Matrix_multiplication for details
|
||||
|
||||
if (left.Columns != right.Rows)
|
||||
{
|
||||
throw new ArgumentException("The width of the left matrix must match the height of the right matrix",
|
||||
"right");
|
||||
}
|
||||
|
||||
int resultRows = left.Rows;
|
||||
int resultColumns = right.Columns;
|
||||
|
||||
var resultMatrix = new double[resultRows][];
|
||||
|
||||
for (int currentRow = 0; currentRow < resultRows; currentRow++)
|
||||
{
|
||||
resultMatrix[currentRow] = new double[resultColumns];
|
||||
|
||||
for (int currentColumn = 0; currentColumn < resultColumns; currentColumn++)
|
||||
{
|
||||
double productValue = 0;
|
||||
|
||||
for (int vectorIndex = 0; vectorIndex < left.Columns; vectorIndex++)
|
||||
{
|
||||
double leftValue = left._MatrixRowValues[currentRow][vectorIndex];
|
||||
double rightValue = right._MatrixRowValues[vectorIndex][currentColumn];
|
||||
double vectorIndexProduct = leftValue*rightValue;
|
||||
productValue += vectorIndexProduct;
|
||||
}
|
||||
|
||||
resultMatrix[currentRow][currentColumn] = productValue;
|
||||
}
|
||||
}
|
||||
|
||||
return new Matrix(resultRows, resultColumns, resultMatrix);
|
||||
}
|
||||
|
||||
private Matrix GetMinorMatrix(int rowToRemove, int columnToRemove)
|
||||
{
|
||||
// See http://en.wikipedia.org/wiki/Minor_(linear_algebra)
|
||||
|
||||
// I'm going to use a horribly naïve algorithm... because I can :)
|
||||
var result = new double[Rows - 1][];
|
||||
int resultRow = 0;
|
||||
|
||||
for (int currentRow = 0; currentRow < Rows; currentRow++)
|
||||
{
|
||||
if (currentRow == rowToRemove)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result[resultRow] = new double[Columns - 1];
|
||||
|
||||
int resultColumn = 0;
|
||||
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
if (currentColumn == columnToRemove)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result[resultRow][resultColumn] = _MatrixRowValues[currentRow][currentColumn];
|
||||
resultColumn++;
|
||||
}
|
||||
|
||||
resultRow++;
|
||||
}
|
||||
|
||||
return new Matrix(Rows - 1, Columns - 1, result);
|
||||
}
|
||||
|
||||
private double GetCofactor(int rowToRemove, int columnToRemove)
|
||||
{
|
||||
// See http://en.wikipedia.org/wiki/Cofactor_(linear_algebra) for details
|
||||
// REVIEW: should things be reversed since I'm 0 indexed?
|
||||
int sum = rowToRemove + columnToRemove;
|
||||
bool isEven = (sum%2 == 0);
|
||||
|
||||
if (isEven)
|
||||
{
|
||||
return GetMinorMatrix(rowToRemove, columnToRemove).Determinant;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1.0*GetMinorMatrix(rowToRemove, columnToRemove).Determinant;
|
||||
}
|
||||
}
|
||||
|
||||
// Equality stuff
|
||||
// See http://msdn.microsoft.com/en-us/library/ms173147.aspx
|
||||
|
||||
public static bool operator ==(Matrix a, Matrix b)
|
||||
{
|
||||
// If both are null, or both are same instance, return true.
|
||||
if (ReferenceEquals(a, b))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If one is null, but not both, return false.
|
||||
if (((object) a == null) || ((object) b == null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((a.Rows != b.Rows) || (a.Columns != b.Columns))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const double errorTolerance = 0.0000000000001;
|
||||
|
||||
for (int currentRow = 0; currentRow < a.Rows; currentRow++)
|
||||
{
|
||||
for (int currentColumn = 0; currentColumn < a.Columns; currentColumn++)
|
||||
{
|
||||
double delta =
|
||||
Math.Abs(a._MatrixRowValues[currentRow][currentColumn] -
|
||||
b._MatrixRowValues[currentRow][currentColumn]);
|
||||
|
||||
if (delta > errorTolerance)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool operator !=(Matrix a, Matrix b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
double result = Rows;
|
||||
result += 2*Columns;
|
||||
|
||||
unchecked
|
||||
{
|
||||
for (int currentRow = 0; currentRow < Rows; currentRow++)
|
||||
{
|
||||
bool eventRow = (currentRow%2) == 0;
|
||||
double multiplier = eventRow ? 1.0 : 2.0;
|
||||
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
result += multiplier*_MatrixRowValues[currentRow][currentColumn];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ok, now convert that double to an int
|
||||
byte[] resultBytes = BitConverter.GetBytes(result);
|
||||
|
||||
var finalBytes = new byte[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
finalBytes[i] = (byte) (resultBytes[i] ^ resultBytes[i + 4]);
|
||||
}
|
||||
|
||||
int hashCode = BitConverter.ToInt32(finalBytes, 0);
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
var other = obj as Matrix;
|
||||
if (other == null)
|
||||
{
|
||||
return base.Equals(obj);
|
||||
}
|
||||
|
||||
return this == other;
|
||||
}
|
||||
}
|
||||
|
||||
internal class DiagonalMatrix : Matrix
|
||||
{
|
||||
public DiagonalMatrix(IList<double> diagonalValues)
|
||||
: base(diagonalValues.Count, diagonalValues.Count)
|
||||
{
|
||||
for (int i = 0; i < diagonalValues.Count; i++)
|
||||
{
|
||||
_MatrixRowValues[i][i] = diagonalValues[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Vector : Matrix
|
||||
{
|
||||
public Vector(IList<double> vectorValues)
|
||||
: base(vectorValues.Count, 1, new IEnumerable<double>[] {vectorValues})
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal class SquareMatrix : Matrix
|
||||
{
|
||||
public SquareMatrix(params double[] allValues)
|
||||
{
|
||||
Rows = (int) Math.Sqrt(allValues.Length);
|
||||
Columns = Rows;
|
||||
|
||||
int allValuesIndex = 0;
|
||||
|
||||
_MatrixRowValues = new double[Rows][];
|
||||
for (int currentRow = 0; currentRow < Rows; currentRow++)
|
||||
{
|
||||
var currentRowValues = new double[Columns];
|
||||
_MatrixRowValues[currentRow] = currentRowValues;
|
||||
|
||||
for (int currentColumn = 0; currentColumn < Columns; currentColumn++)
|
||||
{
|
||||
currentRowValues[currentColumn] = allValues[allValuesIndex++];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class IdentityMatrix : DiagonalMatrix
|
||||
{
|
||||
public IdentityMatrix(int rows)
|
||||
: base(CreateDiagonal(rows))
|
||||
{
|
||||
}
|
||||
|
||||
private static double[] CreateDiagonal(int rows)
|
||||
{
|
||||
var result = new double[rows];
|
||||
for (int i = 0; i < rows; i++)
|
||||
{
|
||||
result[i] = 1.0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
49
Skills/Numerics/Range.cs
Normal file
49
Skills/Numerics/Range.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
|
||||
namespace Moserware.Skills.Numerics
|
||||
{
|
||||
// The whole purpose of this class is to make the code for the SkillCalculator(s)
|
||||
// look a little cleaner
|
||||
|
||||
public abstract class Range<T> where T : Range<T>, new()
|
||||
{
|
||||
private static readonly T _Instance = new T();
|
||||
|
||||
protected Range(int min, int max)
|
||||
{
|
||||
if (min > max)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
|
||||
public int Min { get; private set; }
|
||||
public int Max { get; private set; }
|
||||
protected abstract T Create(int min, int max);
|
||||
|
||||
// REVIEW: It's probably bad form to have access statics via a derived class, but the syntax looks better :-)
|
||||
|
||||
public static T Inclusive(int min, int max)
|
||||
{
|
||||
return _Instance.Create(min, max);
|
||||
}
|
||||
|
||||
public static T Exactly(int value)
|
||||
{
|
||||
return _Instance.Create(value, value);
|
||||
}
|
||||
|
||||
public static T AtLeast(int minimumValue)
|
||||
{
|
||||
return _Instance.Create(minimumValue, int.MaxValue);
|
||||
}
|
||||
|
||||
public bool IsInRange(int value)
|
||||
{
|
||||
return (Min <= value) && (value <= Max);
|
||||
}
|
||||
}
|
||||
}
|
15
Skills/PairwiseComparison.cs
Normal file
15
Skills/PairwiseComparison.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a comparison between two players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The actual values for the enum were chosen so that the also correspond to the multiplier for updates to means.
|
||||
/// </remarks>
|
||||
public enum PairwiseComparison
|
||||
{
|
||||
Win = 1,
|
||||
Draw = 0,
|
||||
Lose = -1
|
||||
}
|
||||
}
|
26
Skills/PartialPlay.cs
Normal file
26
Skills/PartialPlay.cs
Normal file
@ -0,0 +1,26 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
internal static class PartialPlay
|
||||
{
|
||||
public static double GetPartialPlayPercentage(object player)
|
||||
{
|
||||
// If the player doesn't support the interface, assume 1.0 == 100%
|
||||
var partialPlay = player as ISupportPartialPlay;
|
||||
if (partialPlay == null)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
double partialPlayPercentage = partialPlay.PartialPlayPercentage;
|
||||
|
||||
// HACK to get around bug near 0
|
||||
const double smallestPercentage = 0.0001;
|
||||
if (partialPlayPercentage < smallestPercentage)
|
||||
{
|
||||
partialPlayPercentage = smallestPercentage;
|
||||
}
|
||||
|
||||
return partialPlayPercentage;
|
||||
}
|
||||
}
|
||||
}
|
129
Skills/Player.cs
Normal file
129
Skills/Player.cs
Normal file
@ -0,0 +1,129 @@
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a player who has a <see cref="Rating"/>.
|
||||
/// </summary>
|
||||
public class Player<T> : ISupportPartialPlay, ISupportPartialUpdate
|
||||
{
|
||||
private const double DefaultPartialPlayPercentage = 1.0; // = 100% play time
|
||||
private const double DefaultPartialUpdatePercentage = 1.0; // = receive 100% update
|
||||
private readonly T _Id;
|
||||
private readonly double _PartialPlayPercentage;
|
||||
|
||||
private readonly double _PartialUpdatePercentage;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
public Player(T id)
|
||||
: this(id, DefaultPartialPlayPercentage, DefaultPartialUpdatePercentage)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
/// <param name="partialPlayPercentage">The weight percentage to give this player when calculating a new rank.</param>
|
||||
public Player(T id, double partialPlayPercentage)
|
||||
: this(id, partialPlayPercentage, DefaultPartialUpdatePercentage)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
/// <param name="partialPlayPercentage">The weight percentage to give this player when calculating a new rank.</param>
|
||||
/// <param name="partialUpdatePercentage">/// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.</param>
|
||||
public Player(T id, double partialPlayPercentage, double partialUpdatePercentage)
|
||||
{
|
||||
// If they don't want to give a player an id, that's ok...
|
||||
Guard.ArgumentInRangeInclusive(partialPlayPercentage, 0, 1.0, "partialPlayPercentage");
|
||||
Guard.ArgumentInRangeInclusive(partialUpdatePercentage, 0, 1.0, "partialUpdatePercentage");
|
||||
_Id = id;
|
||||
_PartialPlayPercentage = partialPlayPercentage;
|
||||
_PartialUpdatePercentage = partialUpdatePercentage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The identifier for the player, such as a name.
|
||||
/// </summary>
|
||||
public T Id
|
||||
{
|
||||
get { return _Id; }
|
||||
}
|
||||
|
||||
#region ISupportPartialPlay Members
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the percent of the time the player should be weighted where 0.0 indicates the player didn't play and 1.0 indicates the player played 100% of the time.
|
||||
/// </summary>
|
||||
public double PartialPlayPercentage
|
||||
{
|
||||
get { return _PartialPlayPercentage; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISupportPartialUpdate Members
|
||||
|
||||
/// <summary>
|
||||
/// Indicated how much of a skill update a player should receive where 0.0 represents no update and 1.0 represents 100% of the update.
|
||||
/// </summary>
|
||||
public double PartialUpdatePercentage
|
||||
{
|
||||
get { return _PartialUpdatePercentage; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Id != null)
|
||||
{
|
||||
return Id.ToString();
|
||||
}
|
||||
|
||||
return base.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a player who has a <see cref="Rating"/>.
|
||||
/// </summary>
|
||||
public class Player : Player<object>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
public Player(object id)
|
||||
: base(id)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
/// <param name="partialPlayPercentage">The weight percentage to give this player when calculating a new rank.</param>
|
||||
/// <param name="partialUpdatePercentage">Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.</param>
|
||||
public Player(object id, double partialPlayPercentage)
|
||||
: base(id, partialPlayPercentage)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a player.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier for the player, such as a name.</param>
|
||||
/// <param name="partialPlayPercentage">The weight percentage to give this player when calculating a new rank.</param>
|
||||
/// <param name="partialUpdatePercentage">Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.</param>
|
||||
public Player(object id, double partialPlayPercentage, double partialUpdatePercentage)
|
||||
: base(id, partialPlayPercentage, partialUpdatePercentage)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
22
Skills/PlayersRange.cs
Normal file
22
Skills/PlayersRange.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Moserware.Skills.Numerics;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
public class PlayersRange : Range<PlayersRange>
|
||||
{
|
||||
public PlayersRange()
|
||||
: base(int.MinValue, int.MinValue)
|
||||
{
|
||||
}
|
||||
|
||||
private PlayersRange(int min, int max)
|
||||
: base(min, max)
|
||||
{
|
||||
}
|
||||
|
||||
protected override PlayersRange Create(int min, int max)
|
||||
{
|
||||
return new PlayersRange(min, max);
|
||||
}
|
||||
}
|
||||
}
|
45
Skills/Properties/AssemblyInfo.cs
Normal file
45
Skills/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
|
||||
[assembly: AssemblyTitle("Moserware.Skills")]
|
||||
[assembly: AssemblyDescription("Implementation of the TrueSkill algorithm.")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jeff Moser")]
|
||||
[assembly: AssemblyProduct("TrueSkill Calculator")]
|
||||
[assembly: AssemblyCopyright("Copyright © Jeff Moser 2010")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
|
||||
[assembly: Guid("4326f9ed-f234-42ed-bee0-84f7757ab28f")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
|
||||
#if DEBUG
|
||||
|
||||
[assembly: InternalsVisibleTo("UnitTests")]
|
||||
#endif
|
58
Skills/README.txt
Normal file
58
Skills/README.txt
Normal file
@ -0,0 +1,58 @@
|
||||
Hi there!
|
||||
|
||||
Thanks for downloading this code and opening up this file. The goal of this
|
||||
project is to provide an annotated reference implementation of Microsoft's
|
||||
TrueSkill algorithm.
|
||||
|
||||
I describe the philosophy and the buildup of the math involved in my blog post
|
||||
"Computing Your Skill" available at moserware.com.
|
||||
|
||||
In addition, there is a math paper that goes along with the blog post that explains
|
||||
most of the more technical concepts.
|
||||
|
||||
This project isn't intended to win performance tests, it's meant to be read
|
||||
and understood. If you see ways to improve its clarity, please submit a patch.
|
||||
|
||||
If you just want to use the TrueSkill algorithm, simply use the TrueSkillCalculator
|
||||
class and enjoy. If you need examples, please look in the UnitTests\TrueSkill folder.
|
||||
|
||||
If you want to understand the inner workings of the algorithm and implement it
|
||||
yourself, look in the Skills\TrueSkill folder. There are three separate
|
||||
implementations of the algorithm in increasing levels of difficulty:
|
||||
|
||||
1. TwoPlayerTrueSkillCalculator.cs is the easiest to follow and implement. It uses
|
||||
the simple equations directly from the TrueSkill website.
|
||||
2. TwoTeamTrueSkillCalculator.cs is slightly more complicated than the two player
|
||||
version and supports two teams that have at least one player each. It extends
|
||||
the equations on the website and incorporates some things implied in the paper.
|
||||
3. FactorGraphTrueSkillCalculator.cs is a wholly different animal than the first two
|
||||
and it is at least an order of magnitude more complex. It implements the complete
|
||||
TrueSkill algorithm and builds up a "factor graph" composed of several layers.
|
||||
Each layer is composed of "factors", "variables", and "messages" between the two.
|
||||
|
||||
Work happens on the factor graph according to a "schedule" which can either be
|
||||
a single step (e.g. sending a message from a factor to a variable) or a sequence of
|
||||
steps (e.g. everything that happens in a "layer") or a loop where the schedule runs
|
||||
until values start to stabilize (e.g. the bottom layer is approximated and runs until
|
||||
it converges)
|
||||
|
||||
TrueSkill is more general than the popular Elo algorithm. As a comparison, I implemented
|
||||
the Elo algorithm using the both the bell curve (Gaussian) and curve that the FIDE chess
|
||||
league uses (logistic curve). I specifically implemented them in a way to show how the
|
||||
only difference among these Elo implementations is the curve. I also implemented the
|
||||
"duelling" Elo calculator as implied in the paper.
|
||||
|
||||
Everything else was implemented to support these classes. Note that a "player" can be an
|
||||
arbitrary class. However, if that player class supports the "ISupportPartialPlay" or
|
||||
"ISupportPartialUpdate" interfaces, you can add these extra parameters. The only calculator
|
||||
that uses this info is the factor graph implementation. See those files for more details.
|
||||
|
||||
I use this code personally to rank around 45 people, so it's important that it's accurate.
|
||||
Please let me know if you find errors. Bug fix patches are strongly encouraged! Also, feel
|
||||
free to fork the project for different language implementations.
|
||||
|
||||
I'd love to hear from you via comments on the "Computing Your Skill" blog post.
|
||||
|
||||
Have fun and enjoy!
|
||||
|
||||
Jeff Moser <jeff@moserware.com>
|
76
Skills/RankSorter.cs
Normal file
76
Skills/RankSorter.cs
Normal file
@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to sort ranks in non-decreasing order.
|
||||
/// </summary>
|
||||
internal static class RankSorter
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs an in-place sort of the <paramref name="items"/> in according to the <paramref name="ranks"/> in non-decreasing order.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The types of items to sort.</typeparam>
|
||||
/// <param name="items">The items to sort according to the order specified by <paramref name="ranks"/>.</param>
|
||||
/// <param name="ranks">The ranks for each item where 1 is first place.</param>
|
||||
public static void Sort<T>(ref IEnumerable<T> teams, ref int[] teamRanks)
|
||||
{
|
||||
Guard.ArgumentNotNull(teams, "teams");
|
||||
Guard.ArgumentNotNull(teamRanks, "teamRanks");
|
||||
|
||||
int lastObserverdRank = 0;
|
||||
bool needToSort = false;
|
||||
|
||||
foreach (int currentRank in teamRanks)
|
||||
{
|
||||
// We're expecting ranks to go up (e.g. 1, 2, 2, 3, ...)
|
||||
// If it goes down, then we've got to sort it.
|
||||
if (currentRank < lastObserverdRank)
|
||||
{
|
||||
needToSort = true;
|
||||
break;
|
||||
}
|
||||
|
||||
lastObserverdRank = currentRank;
|
||||
}
|
||||
|
||||
if (!needToSort)
|
||||
{
|
||||
// Don't bother doing more work, it's already in a good order
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the existing items as an indexable list.
|
||||
List<T> itemsInList = teams.ToList();
|
||||
|
||||
// item -> rank
|
||||
var itemToRank = new Dictionary<T, int>();
|
||||
|
||||
for (int i = 0; i < itemsInList.Count; i++)
|
||||
{
|
||||
T currentItem = itemsInList[i];
|
||||
int currentItemRank = teamRanks[i];
|
||||
itemToRank[currentItem] = currentItemRank;
|
||||
}
|
||||
|
||||
// Now we need a place for our results...
|
||||
var sortedItems = new T[teamRanks.Length];
|
||||
var sortedRanks = new int[teamRanks.Length];
|
||||
|
||||
// where are we in the result?
|
||||
int currentIndex = 0;
|
||||
|
||||
// Let LINQ-to-Objects to the actual sorting
|
||||
foreach (var sortedKeyValuePair in itemToRank.OrderBy(pair => pair.Value))
|
||||
{
|
||||
sortedItems[currentIndex] = sortedKeyValuePair.Key;
|
||||
sortedRanks[currentIndex++] = sortedKeyValuePair.Value;
|
||||
}
|
||||
|
||||
// And we're done
|
||||
teams = sortedItems;
|
||||
teamRanks = sortedRanks;
|
||||
}
|
||||
}
|
||||
}
|
94
Skills/Rating.cs
Normal file
94
Skills/Rating.cs
Normal file
@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Container for a player's rating.
|
||||
/// </summary>
|
||||
public class Rating
|
||||
{
|
||||
private const int ConservativeStandardDeviationMultiplier = 3;
|
||||
private readonly double _ConservativeStandardDeviationMultiplier;
|
||||
private readonly double _Mean;
|
||||
private readonly double _StandardDeviation;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a rating.
|
||||
/// </summary>
|
||||
/// <param name="mean">The statistical mean value of the rating (also known as μ).</param>
|
||||
/// <param name="standardDeviation">The standard deviation of the rating (also known as σ).</param>
|
||||
public Rating(double mean, double standardDeviation)
|
||||
: this(mean, standardDeviation, ConservativeStandardDeviationMultiplier)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a rating.
|
||||
/// </summary>
|
||||
/// <param name="mean">The statistical mean value of the rating (also known as μ).</param>
|
||||
/// <param name="standardDeviation">The standard deviation (the spread) of the rating (also known as σ).</param>
|
||||
/// <param name="conservativeStandardDeviationMultiplier">The number of <paramref name="standardDeviation"/>s to subtract from the <paramref name="mean"/> to achieve a conservative rating.</param>
|
||||
public Rating(double mean, double standardDeviation, double conservativeStandardDeviationMultiplier)
|
||||
{
|
||||
_Mean = mean;
|
||||
_StandardDeviation = standardDeviation;
|
||||
_ConservativeStandardDeviationMultiplier = conservativeStandardDeviationMultiplier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The statistical mean value of the rating (also known as μ).
|
||||
/// </summary>
|
||||
public double Mean
|
||||
{
|
||||
get { return _Mean; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The standard deviation (the spread) of the rating. This is also known as σ.
|
||||
/// </summary>
|
||||
public double StandardDeviation
|
||||
{
|
||||
get { return _StandardDeviation; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A conservative estimate of skill based on the mean and standard deviation.
|
||||
/// </summary>
|
||||
public double ConservativeRating
|
||||
{
|
||||
get { return _Mean - ConservativeStandardDeviationMultiplier*_StandardDeviation; }
|
||||
}
|
||||
|
||||
public static Rating GetPartialUpdate(Rating prior, Rating fullPosterior, double updatePercentage)
|
||||
{
|
||||
var priorGaussian = new GaussianDistribution(prior.Mean, prior.StandardDeviation);
|
||||
var posteriorGaussian = new GaussianDistribution(fullPosterior.Mean, fullPosterior.StandardDeviation);
|
||||
|
||||
// From a clarification email from Ralf Herbrich:
|
||||
// "the idea is to compute a linear interpolation between the prior and posterior skills of each player
|
||||
// ... in the canonical space of parameters"
|
||||
|
||||
double precisionDifference = posteriorGaussian.Precision - priorGaussian.Precision;
|
||||
double partialPrecisionDifference = updatePercentage*precisionDifference;
|
||||
|
||||
double precisionMeanDifference = posteriorGaussian.PrecisionMean - priorGaussian.PrecisionMean;
|
||||
double partialPrecisionMeanDifference = updatePercentage*precisionMeanDifference;
|
||||
|
||||
GaussianDistribution partialPosteriorGaussion = GaussianDistribution.FromPrecisionMean(
|
||||
priorGaussian.PrecisionMean + partialPrecisionMeanDifference,
|
||||
priorGaussian.Precision + partialPrecisionDifference);
|
||||
|
||||
return new Rating(partialPosteriorGaussion.Mean, partialPosteriorGaussion.StandardDeviation,
|
||||
prior._ConservativeStandardDeviationMultiplier);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
// As a debug helper, display a localized rating:
|
||||
return String.Format(
|
||||
"μ={0:0.0000}, σ={1:0.0000}",
|
||||
Mean, StandardDeviation);
|
||||
}
|
||||
}
|
||||
}
|
93
Skills/SkillCalculator.cs
Normal file
93
Skills/SkillCalculator.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for all skill calculator implementations.
|
||||
/// </summary>
|
||||
public abstract class SkillCalculator
|
||||
{
|
||||
[Flags]
|
||||
public enum SupportedOptions
|
||||
{
|
||||
None = 0x00,
|
||||
PartialPlay = 0x01,
|
||||
PartialUpdate = 0x02,
|
||||
}
|
||||
|
||||
private readonly SupportedOptions _SupportedOptions;
|
||||
private readonly PlayersRange _PlayersPerTeamAllowed;
|
||||
private readonly TeamsRange _TotalTeamsAllowed;
|
||||
|
||||
protected SkillCalculator(SupportedOptions supportedOptions, TeamsRange totalTeamsAllowed, PlayersRange playerPerTeamAllowed)
|
||||
{
|
||||
_SupportedOptions = supportedOptions;
|
||||
_TotalTeamsAllowed = totalTeamsAllowed;
|
||||
_PlayersPerTeamAllowed = playerPerTeamAllowed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates new ratings based on the prior ratings and team ranks.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlayer">The underlying type of the player.</typeparam>
|
||||
/// <param name="gameInfo">Parameters for the game.</param>
|
||||
/// <param name="teams">A mapping of team players and their ratings.</param>
|
||||
/// <param name="teamRanks">The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2)</param>
|
||||
/// <returns>All the players and their new ratings.</returns>
|
||||
public abstract IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable
|
||||
<IDictionary<TPlayer, Rating>>
|
||||
teams,
|
||||
params int[] teamRanks);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the match quality as the likelihood of all teams drawing.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlayer">The underlying type of the player.</typeparam>
|
||||
/// <param name="gameInfo">Parameters for the game.</param>
|
||||
/// <param name="teams">A mapping of team players and their ratings.</param>
|
||||
/// <returns>The quality of the match between the teams as a percentage (0% = bad, 100% = well matched).</returns>
|
||||
public abstract double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams);
|
||||
|
||||
public bool IsSupported(SupportedOptions option)
|
||||
{
|
||||
return (_SupportedOptions & option) == option;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to square the <paramref name="value"/>.
|
||||
/// </summary>
|
||||
/// <returns><param name="value"/> * <param name="value"/></returns>
|
||||
protected static double Square(double value)
|
||||
{
|
||||
return value*value;
|
||||
}
|
||||
|
||||
protected void ValidateTeamCountAndPlayersCountPerTeam<TPlayer>(IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams, _TotalTeamsAllowed, _PlayersPerTeamAllowed);
|
||||
}
|
||||
|
||||
private static void ValidateTeamCountAndPlayersCountPerTeam<TPlayer>(
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams, TeamsRange totalTeams, PlayersRange playersPerTeam)
|
||||
{
|
||||
Guard.ArgumentNotNull(teams, "teams");
|
||||
int countOfTeams = 0;
|
||||
foreach (var currentTeam in teams)
|
||||
{
|
||||
if (!playersPerTeam.IsInRange(currentTeam.Count))
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
countOfTeams++;
|
||||
}
|
||||
|
||||
if (!totalTeams.IsInRange(countOfTeams))
|
||||
{
|
||||
throw new ArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
Skills/Skills.csproj
Normal file
116
Skills/Skills.csproj
Normal file
@ -0,0 +1,116 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>9.0.30729</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Moserware.Skills</RootNamespace>
|
||||
<AssemblyName>Moserware.Skills</AssemblyName>
|
||||
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<StartupObject>
|
||||
</StartupObject>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.Linq">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Data.DataSetExtensions">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Elo\DuellingEloCalculator.cs" />
|
||||
<Compile Include="Elo\GaussianKFactor.cs" />
|
||||
<Compile Include="Elo\TwoPlayerEloCalculator.cs" />
|
||||
<Compile Include="Elo\EloRating.cs" />
|
||||
<Compile Include="Elo\FideKFactor.cs" />
|
||||
<Compile Include="Elo\GaussianEloCalculator.cs" />
|
||||
<Compile Include="Elo\KFactor.cs" />
|
||||
<Compile Include="Numerics\Range.cs" />
|
||||
<Compile Include="PlayersRange.cs" />
|
||||
<Compile Include="TeamsRange.cs" />
|
||||
<Compile Include="TrueSkill\DrawMargin.cs" />
|
||||
<Compile Include="FactorGraphs\FactorGraph.cs" />
|
||||
<Compile Include="FactorGraphs\FactorGraphLayer.cs" />
|
||||
<Compile Include="FactorGraphs\FactorList.cs" />
|
||||
<Compile Include="FactorGraphs\Message.cs" />
|
||||
<Compile Include="FactorGraphs\Schedule.cs" />
|
||||
<Compile Include="FactorGraphs\VariableFactory.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianFactor.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianGreaterThanFactor.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianLikelihoodFactor.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianPriorFactor.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianWeightedSumFactor.cs" />
|
||||
<Compile Include="TrueSkill\Factors\GaussianWithinFactor.cs" />
|
||||
<Compile Include="Elo\FideEloCalculator.cs" />
|
||||
<Compile Include="FactorGraphs\Factor.cs" />
|
||||
<Compile Include="FactorGraphs\Variable.cs" />
|
||||
<Compile Include="TrueSkill\FactorGraphTrueSkillCalculator.cs" />
|
||||
<Compile Include="GameInfo.cs" />
|
||||
<Compile Include="Numerics\GaussianDistribution.cs" />
|
||||
<Compile Include="Guard.cs" />
|
||||
<Compile Include="PartialPlay.cs" />
|
||||
<Compile Include="SkillCalculator.cs" />
|
||||
<Compile Include="ISupportPartialPlay.cs" />
|
||||
<Compile Include="ISupportPartialUpdate.cs" />
|
||||
<Compile Include="Numerics\Matrix.cs" />
|
||||
<Compile Include="TrueSkillCalculator.cs" />
|
||||
<Compile Include="TrueSkill\Layers\IteratedTeamDifferencesInnerLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\PlayerPerformancesToTeamPerformancesLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\PlayerPriorValuesToSkillsLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\PlayerSkillsToPerformancesLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\TeamDifferencesComparisonLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\TeamPerformancesToTeamPerformanceDifferencesLayer.cs" />
|
||||
<Compile Include="TrueSkill\Layers\TrueSkillFactorGraphLayer.cs" />
|
||||
<Compile Include="TrueSkill\TrueSkillFactorGraph.cs" />
|
||||
<Compile Include="TrueSkill\TruncatedGaussianCorrectionFunctions.cs" />
|
||||
<Compile Include="PairwiseComparison.cs" />
|
||||
<Compile Include="Player.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Rating.cs" />
|
||||
<Compile Include="Team.cs" />
|
||||
<Compile Include="RankSorter.cs" />
|
||||
<Compile Include="TrueSkill\TwoPlayerTrueSkillCalculator.cs" />
|
||||
<Compile Include="TrueSkill\TwoTeamTrueSkillCalculator.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="License.txt" />
|
||||
<Content Include="README.txt" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
BIN
Skills/Skills.suo
Normal file
BIN
Skills/Skills.suo
Normal file
Binary file not shown.
90
Skills/Team.cs
Normal file
90
Skills/Team.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for working with a single team.
|
||||
/// </summary>
|
||||
public class Team<TPlayer>
|
||||
{
|
||||
private readonly Dictionary<TPlayer, Rating> _PlayerRatings = new Dictionary<TPlayer, Rating>();
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new team.
|
||||
/// </summary>
|
||||
public Team()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="Team"/> and populates it with the specified <paramref name="player"/>.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to add.</param>
|
||||
/// <param name="rating">The rating of the <paramref name="player"/>.</param>
|
||||
public Team(TPlayer player, Rating rating)
|
||||
{
|
||||
AddPlayer(player, rating);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <paramref name="player"/> to the team.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to add.</param>
|
||||
/// <param name="rating">The rating of the <paramref name="player"/>.</param>
|
||||
/// <returns>The instance of the team (for chaining convenience).</returns>
|
||||
public Team<TPlayer> AddPlayer(TPlayer player, Rating rating)
|
||||
{
|
||||
_PlayerRatings[player] = rating;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="Team"/> as a simple dictionary.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="Team"/> as a simple dictionary.</returns>
|
||||
public IDictionary<TPlayer, Rating> AsDictionary()
|
||||
{
|
||||
return _PlayerRatings;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with a single team.
|
||||
/// </summary>
|
||||
public class Team : Team<Player>
|
||||
{
|
||||
/// <summary>
|
||||
/// Constructs a new team.
|
||||
/// </summary>
|
||||
public Team()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a <see cref="Team"/> and populates it with the specified <paramref name="player"/>.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to add.</param>
|
||||
/// <param name="rating">The rating of the <paramref name="player"/>.</param>
|
||||
public Team(Player player, Rating rating)
|
||||
: base(player, rating)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with multiple teams.
|
||||
/// </summary>
|
||||
public static class Teams
|
||||
{
|
||||
/// <summary>
|
||||
/// Concatenates multiple teams into a list of teams.
|
||||
/// </summary>
|
||||
/// <param name="teams">The teams to concatenate together.</param>
|
||||
/// <returns>A sequence of teams.</returns>
|
||||
public static IEnumerable<IDictionary<TPlayer, Rating>> Concat<TPlayer>(params Team<TPlayer>[] teams)
|
||||
{
|
||||
return teams.Select(t => t.AsDictionary());
|
||||
}
|
||||
}
|
||||
}
|
22
Skills/TeamsRange.cs
Normal file
22
Skills/TeamsRange.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Moserware.Skills.Numerics;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
public class TeamsRange : Range<TeamsRange>
|
||||
{
|
||||
public TeamsRange()
|
||||
: base(int.MinValue, int.MinValue)
|
||||
{
|
||||
}
|
||||
|
||||
private TeamsRange(int min, int max)
|
||||
: base(min, max)
|
||||
{
|
||||
}
|
||||
|
||||
protected override TeamsRange Create(int min, int max)
|
||||
{
|
||||
return new TeamsRange(min, max);
|
||||
}
|
||||
}
|
||||
}
|
23
Skills/TrueSkill/DrawMargin.cs
Normal file
23
Skills/TrueSkill/DrawMargin.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
internal static class DrawMargin
|
||||
{
|
||||
public static double GetDrawMarginFromDrawProbability(double drawProbability, double beta)
|
||||
{
|
||||
// Derived from TrueSkill technical report (MSR-TR-2006-80), page 6
|
||||
|
||||
// draw probability = 2 * CDF(margin/(sqrt(n1+n2)*beta)) -1
|
||||
|
||||
// implies
|
||||
//
|
||||
// margin = inversecdf((draw probability + 1)/2) * sqrt(n1+n2) * beta
|
||||
// n1 and n2 are the number of players on each team
|
||||
double margin = GaussianDistribution.InverseCumulativeTo(.5*(drawProbability + 1), 0, 1)*Math.Sqrt(1 + 1)*
|
||||
beta;
|
||||
return margin;
|
||||
}
|
||||
}
|
||||
}
|
156
Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs
Normal file
156
Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates TrueSkill using a full factor graph.
|
||||
/// </summary>
|
||||
internal class FactorGraphTrueSkillCalculator : SkillCalculator
|
||||
{
|
||||
public FactorGraphTrueSkillCalculator()
|
||||
: base(SupportedOptions.PartialPlay | SupportedOptions.PartialUpdate, TeamsRange.AtLeast(2), PlayersRange.AtLeast(1))
|
||||
{
|
||||
}
|
||||
|
||||
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams,
|
||||
params int[] teamRanks)
|
||||
{
|
||||
Guard.ArgumentNotNull(gameInfo, "gameInfo");
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
|
||||
RankSorter.Sort(ref teams, ref teamRanks);
|
||||
|
||||
var factorGraph = new TrueSkillFactorGraph<TPlayer>(gameInfo, teams, teamRanks);
|
||||
factorGraph.BuildGraph();
|
||||
factorGraph.RunSchedule();
|
||||
|
||||
double probabilityOfOutcome = factorGraph.GetProbabilityOfRanking();
|
||||
|
||||
return factorGraph.GetUpdatedRatings();
|
||||
}
|
||||
|
||||
|
||||
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
// We need to create the A matrix which is the player team assigments.
|
||||
List<IDictionary<TPlayer, Rating>> teamAssignmentsList = teams.ToList();
|
||||
Matrix skillsMatrix = GetPlayerCovarianceMatrix(teamAssignmentsList);
|
||||
Vector meanVector = GetPlayerMeansVector(teamAssignmentsList);
|
||||
Matrix meanVectorTranspose = meanVector.Transpose;
|
||||
|
||||
Matrix playerTeamAssignmentsMatrix = CreatePlayerTeamAssignmentMatrix(teamAssignmentsList, meanVector.Rows);
|
||||
Matrix playerTeamAssignmentsMatrixTranspose = playerTeamAssignmentsMatrix.Transpose;
|
||||
|
||||
double betaSquared = Square(gameInfo.Beta);
|
||||
|
||||
Matrix start = meanVectorTranspose * playerTeamAssignmentsMatrix;
|
||||
Matrix aTa = (betaSquared * playerTeamAssignmentsMatrixTranspose) * playerTeamAssignmentsMatrix;
|
||||
Matrix aTSA = playerTeamAssignmentsMatrixTranspose * skillsMatrix * playerTeamAssignmentsMatrix;
|
||||
Matrix middle = aTa + aTSA;
|
||||
|
||||
Matrix middleInverse = middle.Inverse;
|
||||
|
||||
Matrix end = playerTeamAssignmentsMatrixTranspose * meanVector;
|
||||
|
||||
Matrix expPartMatrix = -0.5 * (start * middleInverse * end);
|
||||
double expPart = expPartMatrix.Determinant;
|
||||
|
||||
double sqrtPartNumerator = aTa.Determinant;
|
||||
double sqrtPartDenominator = middle.Determinant;
|
||||
double sqrtPart = sqrtPartNumerator / sqrtPartDenominator;
|
||||
|
||||
double result = Math.Exp(expPart) * Math.Sqrt(sqrtPart);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Vector GetPlayerMeansVector<TPlayer>(
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList)
|
||||
{
|
||||
// A simple vector of all the player means.
|
||||
return new Vector(GetPlayerRatingValues(teamAssignmentsList, rating => rating.Mean));
|
||||
}
|
||||
|
||||
private static Matrix GetPlayerCovarianceMatrix<TPlayer>(
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList)
|
||||
{
|
||||
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
|
||||
// players.
|
||||
return
|
||||
new DiagonalMatrix(GetPlayerRatingValues(teamAssignmentsList, rating => Square(rating.StandardDeviation)));
|
||||
}
|
||||
|
||||
// Helper function that gets a list of values for all player ratings
|
||||
private static IList<double> GetPlayerRatingValues<TPlayer>(
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teamAssignmentsList, Func<Rating, double> playerRatingFunction)
|
||||
{
|
||||
var playerRatingValues = new List<double>();
|
||||
|
||||
foreach (var currentTeam in teamAssignmentsList)
|
||||
{
|
||||
foreach (Rating currentRating in currentTeam.Values)
|
||||
{
|
||||
playerRatingValues.Add(playerRatingFunction(currentRating));
|
||||
}
|
||||
}
|
||||
|
||||
return playerRatingValues;
|
||||
}
|
||||
|
||||
private static Matrix CreatePlayerTeamAssignmentMatrix<TPlayer>(
|
||||
IList<IDictionary<TPlayer, Rating>> teamAssignmentsList, int totalPlayers)
|
||||
{
|
||||
// The team assignment matrix is often referred to as the "A" matrix. It's a matrix whose rows represent the players
|
||||
// and the columns represent teams. At Matrix[row, column] represents that player[row] is on team[col]
|
||||
// Positive values represent an assignment and a negative value means that we subtract the value of the next
|
||||
// team since we're dealing with pairs. This means that this matrix always has teams - 1 columns.
|
||||
// The only other tricky thing is that values represent the play percentage.
|
||||
|
||||
// For example, consider a 3 team game where team1 is just player1, team 2 is player 2 and player 3, and
|
||||
// team3 is just player 4. Furthermore, player 2 and player 3 on team 2 played 25% and 75% of the time
|
||||
// (e.g. partial play), the A matrix would be:
|
||||
|
||||
// A = this 4x2 matrix:
|
||||
// | 1.00 0.00 |
|
||||
// | -0.25 0.25 |
|
||||
// | -0.75 0.75 |
|
||||
// | 0.00 -1.00 |
|
||||
|
||||
var playerAssignments = new List<IEnumerable<double>>();
|
||||
int totalPreviousPlayers = 0;
|
||||
|
||||
for (int i = 0; i < teamAssignmentsList.Count - 1; i++)
|
||||
{
|
||||
IDictionary<TPlayer, Rating> currentTeam = teamAssignmentsList[i];
|
||||
|
||||
// Need to add in 0's for all the previous players, since they're not
|
||||
// on this team
|
||||
var currentRowValues = new List<double>(new double[totalPreviousPlayers]);
|
||||
playerAssignments.Add(currentRowValues);
|
||||
|
||||
foreach (var currentRating in currentTeam)
|
||||
{
|
||||
currentRowValues.Add(PartialPlay.GetPartialPlayPercentage(currentRating.Key));
|
||||
// indicates the player is on the team
|
||||
totalPreviousPlayers++;
|
||||
}
|
||||
|
||||
IDictionary<TPlayer, Rating> nextTeam = teamAssignmentsList[i + 1];
|
||||
foreach (var nextTeamPlayerPair in nextTeam)
|
||||
{
|
||||
// Add a -1 * playing time to represent the difference
|
||||
currentRowValues.Add(-1 * PartialPlay.GetPartialPlayPercentage(nextTeamPlayerPair.Key));
|
||||
}
|
||||
}
|
||||
|
||||
var playerTeamAssignmentsMatrix = new Matrix(totalPlayers, teamAssignmentsList.Count - 1, playerAssignments);
|
||||
|
||||
return playerTeamAssignmentsMatrix;
|
||||
}
|
||||
}
|
||||
}
|
33
Skills/TrueSkill/Factors/GaussianFactor.cs
Normal file
33
Skills/TrueSkill/Factors/GaussianFactor.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
public abstract class GaussianFactor : Factor<GaussianDistribution>
|
||||
{
|
||||
protected GaussianFactor(string name)
|
||||
: base(name)
|
||||
{
|
||||
}
|
||||
|
||||
/// Sends the factor-graph message with and returns the log-normalization constant
|
||||
protected override double SendMessage(Message<GaussianDistribution> message,
|
||||
Variable<GaussianDistribution> variable)
|
||||
{
|
||||
GaussianDistribution marginal = variable.Value;
|
||||
GaussianDistribution messageValue = message.Value;
|
||||
double logZ = GaussianDistribution.LogProductNormalization(marginal, messageValue);
|
||||
variable.Value = marginal*messageValue;
|
||||
return logZ;
|
||||
}
|
||||
|
||||
public override Message<GaussianDistribution> CreateVariableToMessageBinding(
|
||||
Variable<GaussianDistribution> variable)
|
||||
{
|
||||
return CreateVariableToMessageBinding(variable,
|
||||
new Message<GaussianDistribution>(
|
||||
GaussianDistribution.FromPrecisionMean(0, 0),
|
||||
"message from {0} to {1}", this, variable));
|
||||
}
|
||||
}
|
||||
}
|
75
Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs
Normal file
75
Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs
Normal file
@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor representing a team difference that has exceeded the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
public class GaussianGreaterThanFactor : GaussianFactor
|
||||
{
|
||||
private readonly double _Epsilon;
|
||||
|
||||
public GaussianGreaterThanFactor(double epsilon, Variable<GaussianDistribution> variable)
|
||||
: base(String.Format("{0} > {1:0.000}", variable, epsilon))
|
||||
{
|
||||
_Epsilon = epsilon;
|
||||
CreateVariableToMessageBinding(variable);
|
||||
}
|
||||
|
||||
public override double LogNormalization
|
||||
{
|
||||
get
|
||||
{
|
||||
GaussianDistribution marginal = Variables[0].Value;
|
||||
GaussianDistribution message = Messages[0].Value;
|
||||
GaussianDistribution messageFromVariable = marginal/message;
|
||||
return -GaussianDistribution.LogProductNormalization(messageFromVariable, message)
|
||||
+
|
||||
Math.Log(
|
||||
GaussianDistribution.CumulativeTo((messageFromVariable.Mean - _Epsilon)/
|
||||
messageFromVariable.StandardDeviation));
|
||||
}
|
||||
}
|
||||
|
||||
protected override double UpdateMessage(Message<GaussianDistribution> message,
|
||||
Variable<GaussianDistribution> variable)
|
||||
{
|
||||
GaussianDistribution oldMarginal = variable.Value.Clone();
|
||||
GaussianDistribution oldMessage = message.Value.Clone();
|
||||
GaussianDistribution messageFromVar = oldMarginal/oldMessage;
|
||||
|
||||
double c = messageFromVar.Precision;
|
||||
double d = messageFromVar.PrecisionMean;
|
||||
|
||||
double sqrtC = Math.Sqrt(c);
|
||||
|
||||
double dOnSqrtC = d/sqrtC;
|
||||
|
||||
double epsilsonTimesSqrtC = _Epsilon*sqrtC;
|
||||
d = messageFromVar.PrecisionMean;
|
||||
|
||||
double denom = 1.0 - TruncatedGaussianCorrectionFunctions.WExceedsMargin(dOnSqrtC, epsilsonTimesSqrtC);
|
||||
|
||||
double newPrecision = c/denom;
|
||||
double newPrecisionMean = (d +
|
||||
sqrtC*
|
||||
TruncatedGaussianCorrectionFunctions.VExceedsMargin(dOnSqrtC, epsilsonTimesSqrtC))/
|
||||
denom;
|
||||
|
||||
GaussianDistribution newMarginal = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
|
||||
|
||||
GaussianDistribution newMessage = oldMessage*newMarginal/oldMarginal;
|
||||
|
||||
/// Update the message and marginal
|
||||
message.Value = newMessage;
|
||||
|
||||
variable.Value = newMarginal;
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return newMarginal - oldMarginal;
|
||||
}
|
||||
}
|
||||
}
|
72
Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs
Normal file
72
Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects two variables and adds uncertainty.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
public class GaussianLikelihoodFactor : GaussianFactor
|
||||
{
|
||||
private readonly double _Precision;
|
||||
|
||||
public GaussianLikelihoodFactor(double betaSquared, Variable<GaussianDistribution> variable1,
|
||||
Variable<GaussianDistribution> variable2)
|
||||
: base(String.Format("Likelihood of {0} going to {1}", variable2, variable1))
|
||||
{
|
||||
_Precision = 1.0/betaSquared;
|
||||
CreateVariableToMessageBinding(variable1);
|
||||
CreateVariableToMessageBinding(variable2);
|
||||
}
|
||||
|
||||
public override double LogNormalization
|
||||
{
|
||||
get { return GaussianDistribution.LogRatioNormalization(Variables[0].Value, Messages[0].Value); }
|
||||
}
|
||||
|
||||
private double UpdateHelper(Message<GaussianDistribution> message1, Message<GaussianDistribution> message2,
|
||||
Variable<GaussianDistribution> variable1, Variable<GaussianDistribution> variable2)
|
||||
{
|
||||
GaussianDistribution message1Value = message1.Value.Clone();
|
||||
GaussianDistribution message2Value = message2.Value.Clone();
|
||||
|
||||
GaussianDistribution marginal1 = variable1.Value.Clone();
|
||||
GaussianDistribution marginal2 = variable2.Value.Clone();
|
||||
|
||||
double a = _Precision/(_Precision + marginal2.Precision - message2Value.Precision);
|
||||
|
||||
GaussianDistribution newMessage = GaussianDistribution.FromPrecisionMean(
|
||||
a*(marginal2.PrecisionMean - message2Value.PrecisionMean),
|
||||
a*(marginal2.Precision - message2Value.Precision));
|
||||
|
||||
GaussianDistribution oldMarginalWithoutMessage = marginal1/message1Value;
|
||||
|
||||
GaussianDistribution newMarginal = oldMarginalWithoutMessage*newMessage;
|
||||
|
||||
/// Update the message and marginal
|
||||
|
||||
message1.Value = newMessage;
|
||||
variable1.Value = newMarginal;
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return newMarginal - marginal1;
|
||||
}
|
||||
|
||||
public override double UpdateMessage(int messageIndex)
|
||||
{
|
||||
switch (messageIndex)
|
||||
{
|
||||
case 0:
|
||||
return UpdateHelper(Messages[0], Messages[1],
|
||||
Variables[0], Variables[1]);
|
||||
case 1:
|
||||
return UpdateHelper(Messages[1], Messages[0],
|
||||
Variables[1], Variables[0]);
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
39
Skills/TrueSkill/Factors/GaussianPriorFactor.cs
Normal file
39
Skills/TrueSkill/Factors/GaussianPriorFactor.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
/// <summary>
|
||||
/// Supplies the factor graph with prior information.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
public class GaussianPriorFactor : GaussianFactor
|
||||
{
|
||||
private readonly GaussianDistribution _NewMessage;
|
||||
|
||||
public GaussianPriorFactor(double mean, double variance, Variable<GaussianDistribution> variable)
|
||||
: base(String.Format("Prior value going to {0}", variable))
|
||||
{
|
||||
_NewMessage = new GaussianDistribution(mean, Math.Sqrt(variance));
|
||||
CreateVariableToMessageBinding(variable,
|
||||
new Message<GaussianDistribution>(
|
||||
GaussianDistribution.FromPrecisionMean(0, 0), "message from {0} to {1}",
|
||||
this, variable));
|
||||
}
|
||||
|
||||
protected override double UpdateMessage(Message<GaussianDistribution> message,
|
||||
Variable<GaussianDistribution> variable)
|
||||
{
|
||||
GaussianDistribution oldMarginal = variable.Value.Clone();
|
||||
Message<GaussianDistribution> oldMessage = message;
|
||||
GaussianDistribution newMarginal =
|
||||
GaussianDistribution.FromPrecisionMean(
|
||||
oldMarginal.PrecisionMean + _NewMessage.PrecisionMean - oldMessage.Value.PrecisionMean,
|
||||
oldMarginal.Precision + _NewMessage.Precision - oldMessage.Value.Precision);
|
||||
variable.Value = newMarginal;
|
||||
message.Value = _NewMessage;
|
||||
return oldMarginal - newMarginal;
|
||||
}
|
||||
}
|
||||
}
|
252
Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs
Normal file
252
Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs
Normal file
@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor that sums together multiple Gaussians.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
public class GaussianWeightedSumFactor : GaussianFactor
|
||||
{
|
||||
private readonly List<int[]> _VariableIndexOrdersForWeights = new List<int[]>();
|
||||
|
||||
// This following is used for convenience, for example, the first entry is [0, 1, 2]
|
||||
// corresponding to v[0] = a1*v[1] + a2*v[2]
|
||||
private readonly double[][] _Weights;
|
||||
private readonly double[][] _WeightsSquared;
|
||||
|
||||
public GaussianWeightedSumFactor(Variable<GaussianDistribution> sumVariable,
|
||||
Variable<GaussianDistribution>[] variablesToSum)
|
||||
: this(sumVariable,
|
||||
variablesToSum,
|
||||
variablesToSum.Select(v => 1.0).ToArray()) // By default, set the weight to 1.0
|
||||
{
|
||||
}
|
||||
|
||||
public GaussianWeightedSumFactor(Variable<GaussianDistribution> sumVariable,
|
||||
Variable<GaussianDistribution>[] variablesToSum, double[] variableWeights)
|
||||
: base(CreateName(sumVariable, variablesToSum, variableWeights))
|
||||
{
|
||||
_Weights = new double[variableWeights.Length + 1][];
|
||||
_WeightsSquared = new double[_Weights.Length][];
|
||||
|
||||
// The first weights are a straightforward copy
|
||||
// v_0 = a_1*v_1 + a_2*v_2 + ... + a_n * v_n
|
||||
_Weights[0] = new double[variableWeights.Length];
|
||||
Array.Copy(variableWeights, _Weights[0], variableWeights.Length);
|
||||
_WeightsSquared[0] = _Weights[0].Select(w => w*w).ToArray();
|
||||
|
||||
// 0..n-1
|
||||
_VariableIndexOrdersForWeights.Add(Enumerable.Range(0, 1 + variablesToSum.Length).ToArray());
|
||||
|
||||
|
||||
// The rest move the variables around and divide out the constant.
|
||||
// For example:
|
||||
// v_1 = (-a_2 / a_1) * v_2 + (-a3/a1) * v_3 + ... + (1.0 / a_1) * v_0
|
||||
// By convention, we'll put the v_0 term at the end
|
||||
|
||||
for (int weightsIndex = 1; weightsIndex < _Weights.Length; weightsIndex++)
|
||||
{
|
||||
var currentWeights = new double[variableWeights.Length];
|
||||
_Weights[weightsIndex] = currentWeights;
|
||||
|
||||
var variableIndices = new int[variableWeights.Length + 1];
|
||||
variableIndices[0] = weightsIndex;
|
||||
|
||||
var currentWeightsSquared = new double[variableWeights.Length];
|
||||
_WeightsSquared[weightsIndex] = currentWeightsSquared;
|
||||
|
||||
// keep a single variable to keep track of where we are in the array.
|
||||
// This is helpful since we skip over one of the spots
|
||||
int currentDestinationWeightIndex = 0;
|
||||
|
||||
for (int currentWeightSourceIndex = 0;
|
||||
currentWeightSourceIndex < variableWeights.Length;
|
||||
currentWeightSourceIndex++)
|
||||
{
|
||||
// TODO: get this test to be right
|
||||
if (currentWeightSourceIndex == (weightsIndex - 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
double currentWeight = (-variableWeights[currentWeightSourceIndex]/variableWeights[weightsIndex - 1]);
|
||||
|
||||
if (variableWeights[weightsIndex - 1] == 0)
|
||||
{
|
||||
// HACK: Getting around division by zero
|
||||
currentWeight = 0;
|
||||
}
|
||||
|
||||
currentWeights[currentDestinationWeightIndex] = currentWeight;
|
||||
currentWeightsSquared[currentDestinationWeightIndex] = currentWeight*currentWeight;
|
||||
|
||||
variableIndices[currentDestinationWeightIndex + 1] = currentWeightSourceIndex + 1;
|
||||
currentDestinationWeightIndex++;
|
||||
}
|
||||
|
||||
// And the final one
|
||||
double finalWeight = 1.0/variableWeights[weightsIndex - 1];
|
||||
|
||||
if (variableWeights[weightsIndex - 1] == 0)
|
||||
{
|
||||
// HACK: Getting around division by zero
|
||||
finalWeight = 0;
|
||||
}
|
||||
currentWeights[currentDestinationWeightIndex] = finalWeight;
|
||||
currentWeightsSquared[currentDestinationWeightIndex] = finalWeight*finalWeight;
|
||||
variableIndices[variableIndices.Length - 1] = 0;
|
||||
_VariableIndexOrdersForWeights.Add(variableIndices);
|
||||
}
|
||||
|
||||
CreateVariableToMessageBinding(sumVariable);
|
||||
|
||||
foreach (var currentVariable in variablesToSum)
|
||||
{
|
||||
CreateVariableToMessageBinding(currentVariable);
|
||||
}
|
||||
}
|
||||
|
||||
public override double LogNormalization
|
||||
{
|
||||
get
|
||||
{
|
||||
ReadOnlyCollection<Variable<GaussianDistribution>> vars = Variables;
|
||||
ReadOnlyCollection<Message<GaussianDistribution>> messages = Messages;
|
||||
|
||||
double result = 0.0;
|
||||
|
||||
// We start at 1 since offset 0 has the sum
|
||||
for (int i = 1; i < vars.Count; i++)
|
||||
{
|
||||
result += GaussianDistribution.LogRatioNormalization(vars[i].Value, messages[i].Value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private double UpdateHelper(double[] weights, double[] weightsSquared,
|
||||
IList<Message<GaussianDistribution>> messages,
|
||||
IList<Variable<GaussianDistribution>> variables)
|
||||
{
|
||||
// Potentially look at http://mathworld.wolfram.com/NormalSumDistribution.html for clues as
|
||||
// to what it's doing
|
||||
|
||||
GaussianDistribution message0 = messages[0].Value.Clone();
|
||||
GaussianDistribution marginal0 = variables[0].Value.Clone();
|
||||
|
||||
// The math works out so that 1/newPrecision = sum of a_i^2 /marginalsWithoutMessages[i]
|
||||
double inverseOfNewPrecisionSum = 0.0;
|
||||
double anotherInverseOfNewPrecisionSum = 0.0;
|
||||
double weightedMeanSum = 0.0;
|
||||
double anotherWeightedMeanSum = 0.0;
|
||||
|
||||
for (int i = 0; i < weightsSquared.Length; i++)
|
||||
{
|
||||
// These flow directly from the paper
|
||||
|
||||
inverseOfNewPrecisionSum += weightsSquared[i]/
|
||||
(variables[i + 1].Value.Precision - messages[i + 1].Value.Precision);
|
||||
|
||||
GaussianDistribution diff = (variables[i + 1].Value/messages[i + 1].Value);
|
||||
anotherInverseOfNewPrecisionSum += weightsSquared[i]/diff.Precision;
|
||||
|
||||
weightedMeanSum += weights[i]
|
||||
*
|
||||
(variables[i + 1].Value.PrecisionMean - messages[i + 1].Value.PrecisionMean)
|
||||
/
|
||||
(variables[i + 1].Value.Precision - messages[i + 1].Value.Precision);
|
||||
|
||||
anotherWeightedMeanSum += weights[i]*diff.PrecisionMean/diff.Precision;
|
||||
}
|
||||
|
||||
double newPrecision = 1.0/inverseOfNewPrecisionSum;
|
||||
double anotherNewPrecision = 1.0/anotherInverseOfNewPrecisionSum;
|
||||
|
||||
double newPrecisionMean = newPrecision*weightedMeanSum;
|
||||
double anotherNewPrecisionMean = anotherNewPrecision*anotherWeightedMeanSum;
|
||||
|
||||
GaussianDistribution newMessage = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
|
||||
GaussianDistribution oldMarginalWithoutMessage = marginal0/message0;
|
||||
|
||||
GaussianDistribution newMarginal = oldMarginalWithoutMessage*newMessage;
|
||||
|
||||
/// Update the message and marginal
|
||||
|
||||
messages[0].Value = newMessage;
|
||||
variables[0].Value = newMarginal;
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return newMarginal - marginal0;
|
||||
}
|
||||
|
||||
public override double UpdateMessage(int messageIndex)
|
||||
{
|
||||
ReadOnlyCollection<Message<GaussianDistribution>> allMessages = Messages;
|
||||
ReadOnlyCollection<Variable<GaussianDistribution>> allVariables = Variables;
|
||||
|
||||
Guard.ArgumentIsValidIndex(messageIndex, allMessages.Count, "messageIndex");
|
||||
|
||||
var updatedMessages = new List<Message<GaussianDistribution>>();
|
||||
var updatedVariables = new List<Variable<GaussianDistribution>>();
|
||||
|
||||
int[] indicesToUse = _VariableIndexOrdersForWeights[messageIndex];
|
||||
|
||||
// The tricky part here is that we have to put the messages and variables in the same
|
||||
// order as the weights. Thankfully, the weights and messages share the same index numbers,
|
||||
// so we just need to make sure they're consistent
|
||||
for (int i = 0; i < allMessages.Count; i++)
|
||||
{
|
||||
updatedMessages.Add(allMessages[indicesToUse[i]]);
|
||||
updatedVariables.Add(allVariables[indicesToUse[i]]);
|
||||
}
|
||||
|
||||
return UpdateHelper(_Weights[messageIndex], _WeightsSquared[messageIndex], updatedMessages, updatedVariables);
|
||||
}
|
||||
|
||||
private static string CreateName(Variable<GaussianDistribution> sumVariable,
|
||||
IList<Variable<GaussianDistribution>> variablesToSum, double[] weights)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(sumVariable.ToString());
|
||||
sb.Append(" = ");
|
||||
for (int i = 0; i < variablesToSum.Count; i++)
|
||||
{
|
||||
bool isFirst = (i == 0);
|
||||
|
||||
if (isFirst && (weights[i] < 0))
|
||||
{
|
||||
sb.Append("-");
|
||||
}
|
||||
|
||||
sb.Append(Math.Abs(weights[i]).ToString("0.00"));
|
||||
sb.Append("*[");
|
||||
sb.Append(variablesToSum[i]);
|
||||
sb.Append("]");
|
||||
|
||||
bool isLast = (i == variablesToSum.Count - 1);
|
||||
|
||||
if (!isLast)
|
||||
{
|
||||
if (weights[i + 1] >= 0)
|
||||
{
|
||||
sb.Append(" + ");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
73
Skills/TrueSkill/Factors/GaussianWithinFactor.cs
Normal file
73
Skills/TrueSkill/Factors/GaussianWithinFactor.cs
Normal file
@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Factors
|
||||
{
|
||||
/// <summary>
|
||||
/// Factor representing a team difference that has not exceeded the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>See the accompanying math paper for more details.</remarks>
|
||||
public class GaussianWithinFactor : GaussianFactor
|
||||
{
|
||||
private readonly double _Epsilon;
|
||||
|
||||
public GaussianWithinFactor(double epsilon, Variable<GaussianDistribution> variable)
|
||||
: base(String.Format("{0} <= {1:0.000}", variable, epsilon))
|
||||
{
|
||||
_Epsilon = epsilon;
|
||||
CreateVariableToMessageBinding(variable);
|
||||
}
|
||||
|
||||
public override double LogNormalization
|
||||
{
|
||||
get
|
||||
{
|
||||
GaussianDistribution marginal = Variables[0].Value;
|
||||
GaussianDistribution message = Messages[0].Value;
|
||||
GaussianDistribution messageFromVariable = marginal/message;
|
||||
double mean = messageFromVariable.Mean;
|
||||
double std = messageFromVariable.StandardDeviation;
|
||||
double z = GaussianDistribution.CumulativeTo((_Epsilon - mean)/std)
|
||||
-
|
||||
GaussianDistribution.CumulativeTo((-_Epsilon - mean)/std);
|
||||
|
||||
return -GaussianDistribution.LogProductNormalization(messageFromVariable, message) + Math.Log(z);
|
||||
}
|
||||
}
|
||||
|
||||
protected override double UpdateMessage(Message<GaussianDistribution> message,
|
||||
Variable<GaussianDistribution> variable)
|
||||
{
|
||||
GaussianDistribution oldMarginal = variable.Value.Clone();
|
||||
GaussianDistribution oldMessage = message.Value.Clone();
|
||||
GaussianDistribution messageFromVariable = oldMarginal/oldMessage;
|
||||
|
||||
double c = messageFromVariable.Precision;
|
||||
double d = messageFromVariable.PrecisionMean;
|
||||
|
||||
double sqrtC = Math.Sqrt(c);
|
||||
double dOnSqrtC = d/sqrtC;
|
||||
|
||||
double epsilonTimesSqrtC = _Epsilon*sqrtC;
|
||||
d = messageFromVariable.PrecisionMean;
|
||||
|
||||
double denominator = 1.0 - TruncatedGaussianCorrectionFunctions.WWithinMargin(dOnSqrtC, epsilonTimesSqrtC);
|
||||
double newPrecision = c/denominator;
|
||||
double newPrecisionMean = (d +
|
||||
sqrtC*
|
||||
TruncatedGaussianCorrectionFunctions.VWithinMargin(dOnSqrtC, epsilonTimesSqrtC))/
|
||||
denominator;
|
||||
|
||||
GaussianDistribution newMarginal = GaussianDistribution.FromPrecisionMean(newPrecisionMean, newPrecision);
|
||||
GaussianDistribution newMessage = oldMessage*newMarginal/oldMarginal;
|
||||
|
||||
/// Update the message and marginal
|
||||
message.Value = newMessage;
|
||||
variable.Value = newMarginal;
|
||||
|
||||
/// Return the difference in the new marginal
|
||||
return newMarginal - oldMarginal;
|
||||
}
|
||||
}
|
||||
}
|
192
Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs
Normal file
192
Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs
Normal file
@ -0,0 +1,192 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
// The whole purpose of this is to do a loop on the bottom
|
||||
internal class IteratedTeamDifferencesInnerLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, Variable<GaussianDistribution>, GaussianWeightedSumFactor, Variable<GaussianDistribution>>
|
||||
{
|
||||
private readonly TeamDifferencesComparisonLayer<TPlayer> _TeamDifferencesComparisonLayer;
|
||||
|
||||
private readonly TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer;
|
||||
|
||||
public IteratedTeamDifferencesInnerLayer(TrueSkillFactorGraph<TPlayer> parentGraph,
|
||||
TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>
|
||||
teamPerformancesToPerformanceDifferences,
|
||||
TeamDifferencesComparisonLayer<TPlayer> teamDifferencesComparisonLayer)
|
||||
: base(parentGraph)
|
||||
{
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer = teamPerformancesToPerformanceDifferences;
|
||||
_TeamDifferencesComparisonLayer = teamDifferencesComparisonLayer;
|
||||
}
|
||||
|
||||
public override IEnumerable<Factor<GaussianDistribution>> UntypedFactors
|
||||
{
|
||||
get
|
||||
{
|
||||
return
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.UntypedFactors.Concat(
|
||||
_TeamDifferencesComparisonLayer.UntypedFactors);
|
||||
}
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.SetRawInputVariablesGroups(InputVariablesGroups);
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.BuildLayer();
|
||||
|
||||
_TeamDifferencesComparisonLayer.SetRawInputVariablesGroups(
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.GetRawOutputVariablesGroups());
|
||||
_TeamDifferencesComparisonLayer.BuildLayer();
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePriorSchedule()
|
||||
{
|
||||
Schedule<GaussianDistribution> loop = null;
|
||||
|
||||
switch (InputVariablesGroups.Count)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
throw new InvalidOperationException();
|
||||
case 2:
|
||||
loop = CreateTwoTeamInnerPriorLoopSchedule();
|
||||
break;
|
||||
default:
|
||||
loop = CreateMultipleTeamInnerPriorLoopSchedule();
|
||||
break;
|
||||
}
|
||||
|
||||
// When dealing with differences, there are always (n-1) differences, so add in the 1
|
||||
int totalTeamDifferences = _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors.Count;
|
||||
int totalTeams = totalTeamDifferences + 1;
|
||||
|
||||
var innerSchedule = new ScheduleSequence<GaussianDistribution>(
|
||||
"inner schedule",
|
||||
new[]
|
||||
{
|
||||
loop,
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
"teamPerformanceToPerformanceDifferenceFactors[0] @ 1",
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0], 1),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = {0} - 1] @ 2",
|
||||
totalTeamDifferences),
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[totalTeamDifferences - 1], 2)
|
||||
}
|
||||
);
|
||||
|
||||
return innerSchedule;
|
||||
}
|
||||
|
||||
private Schedule<GaussianDistribution> CreateTwoTeamInnerPriorLoopSchedule()
|
||||
{
|
||||
return ScheduleSequence(
|
||||
new[]
|
||||
{
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
"send team perf to perf differences",
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0],
|
||||
0),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
"send to greater than or within factor",
|
||||
_TeamDifferencesComparisonLayer.LocalFactors[0],
|
||||
0)
|
||||
},
|
||||
"loop of just two teams inner sequence");
|
||||
}
|
||||
|
||||
private Schedule<GaussianDistribution> CreateMultipleTeamInnerPriorLoopSchedule()
|
||||
{
|
||||
int totalTeamDifferences = _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors.Count;
|
||||
|
||||
var forwardScheduleList = new List<Schedule<GaussianDistribution>>();
|
||||
|
||||
for (int i = 0; i < totalTeamDifferences - 1; i++)
|
||||
{
|
||||
Schedule<GaussianDistribution> currentForwardSchedulePiece =
|
||||
ScheduleSequence(
|
||||
new Schedule<GaussianDistribution>[]
|
||||
{
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("team perf to perf diff {0}",
|
||||
i),
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[i], 0),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("greater than or within result factor {0}",
|
||||
i),
|
||||
_TeamDifferencesComparisonLayer.LocalFactors[i],
|
||||
0),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("team perf to perf diff factors [{0}], 2",
|
||||
i),
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[i], 2)
|
||||
}, "current forward schedule piece {0}", i);
|
||||
|
||||
forwardScheduleList.Add(currentForwardSchedulePiece);
|
||||
}
|
||||
|
||||
var forwardSchedule =
|
||||
new ScheduleSequence<GaussianDistribution>(
|
||||
"forward schedule",
|
||||
forwardScheduleList);
|
||||
|
||||
var backwardScheduleList = new List<Schedule<GaussianDistribution>>();
|
||||
|
||||
for (int i = 0; i < totalTeamDifferences - 1; i++)
|
||||
{
|
||||
var currentBackwardSchedulePiece = new ScheduleSequence<GaussianDistribution>(
|
||||
"current backward schedule piece",
|
||||
new Schedule<GaussianDistribution>[]
|
||||
{
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 0",
|
||||
i),
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[
|
||||
totalTeamDifferences - 1 - i], 0),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - {0}] @ 0",
|
||||
i),
|
||||
_TeamDifferencesComparisonLayer.LocalFactors[totalTeamDifferences - 1 - i], 0),
|
||||
new ScheduleStep<GaussianDistribution>(
|
||||
String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 1",
|
||||
i),
|
||||
_TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[
|
||||
totalTeamDifferences - 1 - i], 1)
|
||||
}
|
||||
);
|
||||
backwardScheduleList.Add(currentBackwardSchedulePiece);
|
||||
}
|
||||
|
||||
var backwardSchedule =
|
||||
new ScheduleSequence<GaussianDistribution>(
|
||||
"backward schedule",
|
||||
backwardScheduleList);
|
||||
|
||||
var forwardBackwardScheduleToLoop =
|
||||
new ScheduleSequence<GaussianDistribution>(
|
||||
"forward Backward Schedule To Loop",
|
||||
new Schedule<GaussianDistribution>[]
|
||||
{
|
||||
forwardSchedule, backwardSchedule
|
||||
});
|
||||
|
||||
const double initialMaxDelta = 0.0001;
|
||||
|
||||
var loop = new ScheduleLoop<GaussianDistribution>(
|
||||
String.Format("loop with max delta of {0}",
|
||||
initialMaxDelta),
|
||||
forwardBackwardScheduleToLoop,
|
||||
initialMaxDelta);
|
||||
|
||||
return loop;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
internal class PlayerPerformancesToTeamPerformancesLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, KeyedVariable<TPlayer, GaussianDistribution>, GaussianWeightedSumFactor,
|
||||
Variable<GaussianDistribution>>
|
||||
{
|
||||
public PlayerPerformancesToTeamPerformancesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
|
||||
: base(parentGraph)
|
||||
{
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
foreach (var currentTeam in InputVariablesGroups)
|
||||
{
|
||||
Variable<GaussianDistribution> teamPerformance = CreateOutputVariable(currentTeam);
|
||||
AddLayerFactor(CreatePlayerToTeamSumFactor(currentTeam, teamPerformance));
|
||||
|
||||
// REVIEW: Does it make sense to have groups of one?
|
||||
OutputVariablesGroups.Add(new[] {teamPerformance});
|
||||
}
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePriorSchedule()
|
||||
{
|
||||
return ScheduleSequence(
|
||||
from weightedSumFactor in LocalFactors
|
||||
select new ScheduleStep<GaussianDistribution>("Perf to Team Perf Step", weightedSumFactor, 0),
|
||||
"all player perf to team perf schedule");
|
||||
}
|
||||
|
||||
protected GaussianWeightedSumFactor CreatePlayerToTeamSumFactor(
|
||||
IList<KeyedVariable<TPlayer, GaussianDistribution>> teamMembers, Variable<GaussianDistribution> sumVariable)
|
||||
{
|
||||
return new GaussianWeightedSumFactor(sumVariable, teamMembers.ToArray(),
|
||||
teamMembers.Select(v => PartialPlay.GetPartialPlayPercentage(v.Key)).
|
||||
ToArray());
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePosteriorSchedule()
|
||||
{
|
||||
return ScheduleSequence(from currentFactor in LocalFactors
|
||||
from currentIteration in
|
||||
Enumerable.Range(1, currentFactor.NumberOfMessages - 1)
|
||||
select new ScheduleStep<GaussianDistribution>(
|
||||
"team sum perf @" + currentIteration,
|
||||
currentFactor,
|
||||
currentIteration),
|
||||
"all of the team's sum iterations");
|
||||
}
|
||||
|
||||
private Variable<GaussianDistribution> CreateOutputVariable(
|
||||
IList<KeyedVariable<TPlayer, GaussianDistribution>> team)
|
||||
{
|
||||
string teamMemberNames = String.Join(", ", team.Select(teamMember => teamMember.Key.ToString()).ToArray());
|
||||
|
||||
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team[{0}]'s performance", teamMemberNames);
|
||||
}
|
||||
}
|
||||
}
|
63
Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs
Normal file
63
Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
// We intentionally have no Posterior schedule since the only purpose here is to
|
||||
internal class PlayerPriorValuesToSkillsLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, DefaultVariable<GaussianDistribution>, GaussianPriorFactor,
|
||||
KeyedVariable<TPlayer, GaussianDistribution>>
|
||||
{
|
||||
private readonly IEnumerable<IDictionary<TPlayer, Rating>> _Teams;
|
||||
|
||||
public PlayerPriorValuesToSkillsLayer(TrueSkillFactorGraph<TPlayer> parentGraph,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
: base(parentGraph)
|
||||
{
|
||||
_Teams = teams;
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
foreach (var currentTeam in _Teams)
|
||||
{
|
||||
var currentTeamSkills = new List<KeyedVariable<TPlayer, GaussianDistribution>>();
|
||||
|
||||
foreach (var currentTeamPlayer in currentTeam)
|
||||
{
|
||||
KeyedVariable<TPlayer, GaussianDistribution> playerSkill =
|
||||
CreateSkillOutputVariable(currentTeamPlayer.Key);
|
||||
AddLayerFactor(CreatePriorFactor(currentTeamPlayer.Key, currentTeamPlayer.Value, playerSkill));
|
||||
currentTeamSkills.Add(playerSkill);
|
||||
}
|
||||
|
||||
OutputVariablesGroups.Add(currentTeamSkills);
|
||||
}
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePriorSchedule()
|
||||
{
|
||||
return ScheduleSequence(
|
||||
from prior in LocalFactors
|
||||
select new ScheduleStep<GaussianDistribution>("Prior to Skill Step", prior, 0),
|
||||
"All priors");
|
||||
}
|
||||
|
||||
private GaussianPriorFactor CreatePriorFactor(TPlayer player, Rating priorRating,
|
||||
Variable<GaussianDistribution> skillsVariable)
|
||||
{
|
||||
return new GaussianPriorFactor(priorRating.Mean,
|
||||
Square(priorRating.StandardDeviation) +
|
||||
Square(ParentFactorGraph.GameInfo.DynamicsFactor), skillsVariable);
|
||||
}
|
||||
|
||||
private KeyedVariable<TPlayer, GaussianDistribution> CreateSkillOutputVariable(TPlayer key)
|
||||
{
|
||||
return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s skill", key);
|
||||
}
|
||||
}
|
||||
}
|
64
Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs
Normal file
64
Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
internal class PlayerSkillsToPerformancesLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, KeyedVariable<TPlayer, GaussianDistribution>, GaussianLikelihoodFactor,
|
||||
KeyedVariable<TPlayer, GaussianDistribution>>
|
||||
{
|
||||
public PlayerSkillsToPerformancesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
|
||||
: base(parentGraph)
|
||||
{
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
foreach (var currentTeam in InputVariablesGroups)
|
||||
{
|
||||
var currentTeamPlayerPerformances = new List<KeyedVariable<TPlayer, GaussianDistribution>>();
|
||||
|
||||
foreach (var playerSkillVariable in currentTeam)
|
||||
{
|
||||
KeyedVariable<TPlayer, GaussianDistribution> playerPerformance =
|
||||
CreateOutputVariable(playerSkillVariable.Key);
|
||||
AddLayerFactor(CreateLikelihood(playerSkillVariable, playerPerformance));
|
||||
currentTeamPlayerPerformances.Add(playerPerformance);
|
||||
}
|
||||
|
||||
OutputVariablesGroups.Add(currentTeamPlayerPerformances);
|
||||
}
|
||||
}
|
||||
|
||||
private GaussianLikelihoodFactor CreateLikelihood(KeyedVariable<TPlayer, GaussianDistribution> playerSkill,
|
||||
KeyedVariable<TPlayer, GaussianDistribution> playerPerformance)
|
||||
{
|
||||
return new GaussianLikelihoodFactor(Square(ParentFactorGraph.GameInfo.Beta), playerPerformance, playerSkill);
|
||||
}
|
||||
|
||||
private KeyedVariable<TPlayer, GaussianDistribution> CreateOutputVariable(TPlayer key)
|
||||
{
|
||||
return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s performance", key);
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePriorSchedule()
|
||||
{
|
||||
return ScheduleSequence(
|
||||
from likelihood in LocalFactors
|
||||
select new ScheduleStep<GaussianDistribution>("Skill to Perf step", likelihood, 0),
|
||||
"All skill to performance sending");
|
||||
}
|
||||
|
||||
public override Schedule<GaussianDistribution> CreatePosteriorSchedule()
|
||||
{
|
||||
return ScheduleSequence(
|
||||
from likelihood in LocalFactors
|
||||
select new ScheduleStep<GaussianDistribution>("name", likelihood, 1),
|
||||
"All skill to performance sending");
|
||||
}
|
||||
}
|
||||
}
|
38
Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs
Normal file
38
Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs
Normal file
@ -0,0 +1,38 @@
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
internal class TeamDifferencesComparisonLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, Variable<GaussianDistribution>, GaussianFactor, DefaultVariable<GaussianDistribution>>
|
||||
{
|
||||
private readonly double _Epsilon;
|
||||
private readonly int[] _TeamRanks;
|
||||
|
||||
public TeamDifferencesComparisonLayer(TrueSkillFactorGraph<TPlayer> parentGraph, int[] teamRanks)
|
||||
: base(parentGraph)
|
||||
{
|
||||
_TeamRanks = teamRanks;
|
||||
GameInfo gameInfo = ParentFactorGraph.GameInfo;
|
||||
_Epsilon = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
for (int i = 0; i < InputVariablesGroups.Count; i++)
|
||||
{
|
||||
bool isDraw = (_TeamRanks[i] == _TeamRanks[i + 1]);
|
||||
Variable<GaussianDistribution> teamDifference = InputVariablesGroups[i][0];
|
||||
|
||||
GaussianFactor factor =
|
||||
isDraw
|
||||
? (GaussianFactor) new GaussianWithinFactor(_Epsilon, teamDifference)
|
||||
: new GaussianGreaterThanFactor(_Epsilon, teamDifference);
|
||||
|
||||
AddLayerFactor(factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Factors;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
internal class TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer> :
|
||||
TrueSkillFactorGraphLayer
|
||||
<TPlayer, Variable<GaussianDistribution>, GaussianWeightedSumFactor, Variable<GaussianDistribution>>
|
||||
{
|
||||
public TeamPerformancesToTeamPerformanceDifferencesLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
|
||||
: base(parentGraph)
|
||||
{
|
||||
}
|
||||
|
||||
public override void BuildLayer()
|
||||
{
|
||||
for (int i = 0; i < InputVariablesGroups.Count - 1; i++)
|
||||
{
|
||||
Variable<GaussianDistribution> strongerTeam = InputVariablesGroups[i][0];
|
||||
Variable<GaussianDistribution> weakerTeam = InputVariablesGroups[i + 1][0];
|
||||
|
||||
Variable<GaussianDistribution> currentDifference = CreateOutputVariable();
|
||||
AddLayerFactor(CreateTeamPerformanceToDifferenceFactor(strongerTeam, weakerTeam, currentDifference));
|
||||
|
||||
// REVIEW: Does it make sense to have groups of one?
|
||||
OutputVariablesGroups.Add(new[] {currentDifference});
|
||||
}
|
||||
}
|
||||
|
||||
private GaussianWeightedSumFactor CreateTeamPerformanceToDifferenceFactor(
|
||||
Variable<GaussianDistribution> strongerTeam, Variable<GaussianDistribution> weakerTeam,
|
||||
Variable<GaussianDistribution> output)
|
||||
{
|
||||
return new GaussianWeightedSumFactor(output, new[] {strongerTeam, weakerTeam}, new[] {1.0, -1.0});
|
||||
}
|
||||
|
||||
private Variable<GaussianDistribution> CreateOutputVariable()
|
||||
{
|
||||
return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team performance difference");
|
||||
}
|
||||
}
|
||||
}
|
20
Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs
Normal file
20
Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill.Layers
|
||||
{
|
||||
internal abstract class TrueSkillFactorGraphLayer<TPlayer, TInputVariable, TFactor, TOutputVariable>
|
||||
:
|
||||
FactorGraphLayer
|
||||
<TrueSkillFactorGraph<TPlayer>, GaussianDistribution, Variable<GaussianDistribution>, TInputVariable,
|
||||
TFactor, TOutputVariable>
|
||||
where TInputVariable : Variable<GaussianDistribution>
|
||||
where TFactor : Factor<GaussianDistribution>
|
||||
where TOutputVariable : Variable<GaussianDistribution>
|
||||
{
|
||||
public TrueSkillFactorGraphLayer(TrueSkillFactorGraph<TPlayer> parentGraph)
|
||||
: base(parentGraph)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
119
Skills/TrueSkill/TrueSkillFactorGraph.cs
Normal file
119
Skills/TrueSkill/TrueSkillFactorGraph.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Numerics;
|
||||
using Moserware.Skills.FactorGraphs;
|
||||
using Moserware.Skills.TrueSkill.Layers;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
public class TrueSkillFactorGraph<TPlayer> :
|
||||
FactorGraph<TrueSkillFactorGraph<TPlayer>, GaussianDistribution, Variable<GaussianDistribution>>
|
||||
{
|
||||
private readonly List<FactorGraphLayerBase<GaussianDistribution>> _Layers;
|
||||
private readonly PlayerPriorValuesToSkillsLayer<TPlayer> _PriorLayer;
|
||||
|
||||
public TrueSkillFactorGraph(GameInfo gameInfo, IEnumerable<IDictionary<TPlayer, Rating>> teams, int[] teamRanks)
|
||||
{
|
||||
_PriorLayer = new PlayerPriorValuesToSkillsLayer<TPlayer>(this, teams);
|
||||
GameInfo = gameInfo;
|
||||
VariableFactory =
|
||||
new VariableFactory<GaussianDistribution>(() => GaussianDistribution.FromPrecisionMean(0, 0));
|
||||
|
||||
_Layers = new List<FactorGraphLayerBase<GaussianDistribution>>
|
||||
{
|
||||
_PriorLayer,
|
||||
new PlayerSkillsToPerformancesLayer<TPlayer>(this),
|
||||
new PlayerPerformancesToTeamPerformancesLayer<TPlayer>(this),
|
||||
new IteratedTeamDifferencesInnerLayer<TPlayer>(
|
||||
this,
|
||||
new TeamPerformancesToTeamPerformanceDifferencesLayer<TPlayer>(this),
|
||||
new TeamDifferencesComparisonLayer<TPlayer>(this, teamRanks))
|
||||
};
|
||||
}
|
||||
|
||||
public GameInfo GameInfo { get; private set; }
|
||||
|
||||
public void BuildGraph()
|
||||
{
|
||||
object lastOutput = null;
|
||||
|
||||
foreach (var currentLayer in _Layers)
|
||||
{
|
||||
if (lastOutput != null)
|
||||
{
|
||||
currentLayer.SetRawInputVariablesGroups(lastOutput);
|
||||
}
|
||||
|
||||
currentLayer.BuildLayer();
|
||||
|
||||
lastOutput = currentLayer.GetRawOutputVariablesGroups();
|
||||
}
|
||||
}
|
||||
|
||||
public void RunSchedule()
|
||||
{
|
||||
Schedule<GaussianDistribution> fullSchedule = CreateFullSchedule();
|
||||
double fullScheduleDelta = fullSchedule.Visit();
|
||||
}
|
||||
|
||||
public double GetProbabilityOfRanking()
|
||||
{
|
||||
var factorList = new FactorList<GaussianDistribution>();
|
||||
|
||||
foreach (var currentLayer in _Layers)
|
||||
{
|
||||
foreach (var currentFactor in currentLayer.UntypedFactors)
|
||||
{
|
||||
factorList.AddFactor(currentFactor);
|
||||
}
|
||||
}
|
||||
|
||||
double logZ = factorList.LogNormalization;
|
||||
return Math.Exp(logZ);
|
||||
}
|
||||
|
||||
private Schedule<GaussianDistribution> CreateFullSchedule()
|
||||
{
|
||||
var fullSchedule = new List<Schedule<GaussianDistribution>>();
|
||||
|
||||
foreach (var currentLayer in _Layers)
|
||||
{
|
||||
Schedule<GaussianDistribution> currentPriorSchedule = currentLayer.CreatePriorSchedule();
|
||||
if (currentPriorSchedule != null)
|
||||
{
|
||||
fullSchedule.Add(currentPriorSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
// Casting to IEnumerable to get the LINQ Reverse()
|
||||
IEnumerable<FactorGraphLayerBase<GaussianDistribution>> allLayers = _Layers;
|
||||
|
||||
foreach (var currentLayer in allLayers.Reverse())
|
||||
{
|
||||
Schedule<GaussianDistribution> currentPosteriorSchedule = currentLayer.CreatePosteriorSchedule();
|
||||
if (currentPosteriorSchedule != null)
|
||||
{
|
||||
fullSchedule.Add(currentPosteriorSchedule);
|
||||
}
|
||||
}
|
||||
|
||||
return new ScheduleSequence<GaussianDistribution>("Full schedule", fullSchedule);
|
||||
}
|
||||
|
||||
public IDictionary<TPlayer, Rating> GetUpdatedRatings()
|
||||
{
|
||||
var result = new Dictionary<TPlayer, Rating>();
|
||||
foreach (var currentTeam in _PriorLayer.OutputVariablesGroups)
|
||||
{
|
||||
foreach (var currentPlayer in currentTeam)
|
||||
{
|
||||
result[currentPlayer.Key] = new Rating(currentPlayer.Value.Mean,
|
||||
currentPlayer.Value.StandardDeviation);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
188
Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs
Normal file
188
Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs
Normal file
@ -0,0 +1,188 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
internal static class TruncatedGaussianCorrectionFunctions
|
||||
{
|
||||
// These functions from the bottom of page 4 of the TrueSkill paper.
|
||||
|
||||
/// <summary>
|
||||
/// The "V" function where the team performance difference is greater than the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>In the reference F# implementation, this is referred to as "the additive
|
||||
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
|
||||
/// <param name="teamPerformanceDifference"></param>
|
||||
/// <param name="drawMargin">In the paper, it's referred to as just "ε".</param>
|
||||
/// <returns></returns>
|
||||
public static double VExceedsMargin(double teamPerformanceDifference, double drawMargin, double c)
|
||||
{
|
||||
return VExceedsMargin(teamPerformanceDifference/c, drawMargin/c);
|
||||
//return GaussianDistribution.At((teamPerformanceDifference - drawMargin) / c) / GaussianDistribution.CumulativeTo((teamPerformanceDifference - drawMargin) / c);
|
||||
}
|
||||
|
||||
public static double VExceedsMargin(double teamPerformanceDifference, double drawMargin)
|
||||
{
|
||||
double denominator = GaussianDistribution.CumulativeTo(teamPerformanceDifference - drawMargin);
|
||||
|
||||
if (denominator < 2.222758749e-162)
|
||||
{
|
||||
return -teamPerformanceDifference + drawMargin;
|
||||
}
|
||||
|
||||
return GaussianDistribution.At(teamPerformanceDifference - drawMargin)/denominator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The "W" function where the team performance difference is greater than the draw margin.
|
||||
/// </summary>
|
||||
/// <remarks>In the reference F# implementation, this is referred to as "the multiplicative
|
||||
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
|
||||
/// <param name="teamPerformanceDifference"></param>
|
||||
/// <param name="drawMargin"></param>
|
||||
/// <param name="c"></param>
|
||||
/// <returns></returns>
|
||||
public static double WExceedsMargin(double teamPerformanceDifference, double drawMargin, double c)
|
||||
{
|
||||
return WExceedsMargin(teamPerformanceDifference/c, drawMargin/c);
|
||||
//var vWin = VExceedsMargin(teamPerformanceDifference, drawMargin, c);
|
||||
//return vWin * (vWin + (teamPerformanceDifference - drawMargin) / c);
|
||||
}
|
||||
|
||||
public static double WExceedsMargin(double teamPerformanceDifference, double drawMargin)
|
||||
{
|
||||
double denominator = GaussianDistribution.CumulativeTo(teamPerformanceDifference - drawMargin);
|
||||
|
||||
if (denominator < 2.222758749e-162)
|
||||
{
|
||||
if (teamPerformanceDifference < 0.0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double vWin = VExceedsMargin(teamPerformanceDifference, drawMargin);
|
||||
return vWin*(vWin + teamPerformanceDifference - drawMargin);
|
||||
}
|
||||
|
||||
// the additive correction of a double-sided truncated Gaussian with unit variance
|
||||
public static double VWithinMargin(double teamPerformanceDifference, double drawMargin, double c)
|
||||
{
|
||||
return VWithinMargin(teamPerformanceDifference/c, drawMargin/c);
|
||||
//var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
//return (GaussianDistribution.At((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c) - GaussianDistribution.At((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c))
|
||||
// /
|
||||
// (GaussianDistribution.CumulativeTo((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c) - GaussianDistribution.CumulativeTo((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c));
|
||||
}
|
||||
|
||||
// My original:
|
||||
//public static double VWithinMargin(double teamPerformanceDifference, double drawMargin)
|
||||
//{
|
||||
// var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
// return (GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue))
|
||||
// /
|
||||
// (GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue));
|
||||
//}
|
||||
|
||||
// from F#:
|
||||
public static double VWithinMargin(double teamPerformanceDifference, double drawMargin)
|
||||
{
|
||||
double teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
double denominator =
|
||||
GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) -
|
||||
GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue);
|
||||
if (denominator < 2.222758749e-162)
|
||||
{
|
||||
if (teamPerformanceDifference < 0.0)
|
||||
{
|
||||
return -teamPerformanceDifference - drawMargin;
|
||||
}
|
||||
|
||||
return -teamPerformanceDifference + drawMargin;
|
||||
}
|
||||
|
||||
double numerator = GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue) -
|
||||
GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue);
|
||||
|
||||
if (teamPerformanceDifference < 0.0)
|
||||
{
|
||||
return -numerator/denominator;
|
||||
}
|
||||
|
||||
return numerator/denominator;
|
||||
}
|
||||
|
||||
// the multiplicative correction of a double-sided truncated Gaussian with unit variance
|
||||
public static double WWithinMargin(double teamPerformanceDifference, double drawMargin, double c)
|
||||
{
|
||||
return WWithinMargin(teamPerformanceDifference/c, drawMargin/c);
|
||||
//var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
//var vDraw = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin, c);
|
||||
|
||||
//return (vDraw * vDraw)
|
||||
// +
|
||||
// (
|
||||
// (
|
||||
// (
|
||||
// ((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// *
|
||||
// GaussianDistribution.At((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// )
|
||||
// +
|
||||
// (
|
||||
// ((drawMargin + teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// *
|
||||
// GaussianDistribution.At((drawMargin + teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// )
|
||||
// )
|
||||
// /
|
||||
// (
|
||||
// GaussianDistribution.CumulativeTo((drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// -
|
||||
// GaussianDistribution.CumulativeTo((-drawMargin - teamPerformanceDifferenceAbsoluteValue) / c)
|
||||
// )
|
||||
// );
|
||||
}
|
||||
|
||||
// My original:
|
||||
//public static double WWithinMargin(double teamPerformanceDifference, double drawMargin)
|
||||
//{
|
||||
// var teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
// var vDraw = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin);
|
||||
// return (vDraw * vDraw)
|
||||
// +
|
||||
// (
|
||||
// ((drawMargin - teamPerformanceDifferenceAbsoluteValue) * GaussianDistribution.At(drawMargin - teamPerformanceDifferenceAbsoluteValue) + (drawMargin + teamPerformanceDifferenceAbsoluteValue) * GaussianDistribution.At(drawMargin + teamPerformanceDifferenceAbsoluteValue))
|
||||
// /
|
||||
// (GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue) - GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue))
|
||||
// );
|
||||
//}
|
||||
|
||||
// From F#:
|
||||
public static double WWithinMargin(double teamPerformanceDifference, double drawMargin)
|
||||
{
|
||||
double teamPerformanceDifferenceAbsoluteValue = Math.Abs(teamPerformanceDifference);
|
||||
double denominator = GaussianDistribution.CumulativeTo(drawMargin - teamPerformanceDifferenceAbsoluteValue)
|
||||
-
|
||||
GaussianDistribution.CumulativeTo(-drawMargin - teamPerformanceDifferenceAbsoluteValue);
|
||||
|
||||
if (denominator < 2.222758749e-162)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
double vt = VWithinMargin(teamPerformanceDifferenceAbsoluteValue, drawMargin);
|
||||
|
||||
return vt*vt +
|
||||
(
|
||||
(drawMargin - teamPerformanceDifferenceAbsoluteValue)
|
||||
*
|
||||
GaussianDistribution.At(
|
||||
drawMargin - teamPerformanceDifferenceAbsoluteValue)
|
||||
- (-drawMargin - teamPerformanceDifferenceAbsoluteValue)
|
||||
*
|
||||
GaussianDistribution.At(-drawMargin - teamPerformanceDifferenceAbsoluteValue))/denominator;
|
||||
}
|
||||
}
|
||||
}
|
150
Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs
Normal file
150
Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Skills.Numerics;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the new ratings for only two players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When you only have two players, a lot of the math simplifies. The main purpose of this class
|
||||
/// is to show the bare minimum of what a TrueSkill implementation should have.
|
||||
/// </remarks>
|
||||
public class TwoPlayerTrueSkillCalculator : SkillCalculator
|
||||
{
|
||||
public TwoPlayerTrueSkillCalculator()
|
||||
: base(SupportedOptions.None, Range<TeamsRange>.Exactly(2), Range<PlayersRange>.Exactly(1))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable
|
||||
<IDictionary<TPlayer, Rating>>
|
||||
teams, params int[] teamRanks)
|
||||
{
|
||||
// Basic argument checking
|
||||
Guard.ArgumentNotNull(gameInfo, "gameInfo");
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
|
||||
// Make sure things are in order
|
||||
RankSorter.Sort(ref teams, ref teamRanks);
|
||||
|
||||
// Get the teams as a list to make it easier to index
|
||||
List<IDictionary<TPlayer, Rating>> teamList = teams.ToList();
|
||||
|
||||
// Since we verified that each team has one player, we know the player is the first one
|
||||
IDictionary<TPlayer, Rating> winningTeam = teamList[0];
|
||||
TPlayer winner = winningTeam.Keys.First();
|
||||
Rating winnerPreviousRating = winningTeam[winner];
|
||||
|
||||
IDictionary<TPlayer, Rating> losingTeam = teamList[1];
|
||||
TPlayer loser = losingTeam.Keys.First();
|
||||
Rating loserPreviousRating = losingTeam[loser];
|
||||
|
||||
bool wasDraw = (teamRanks[0] == teamRanks[1]);
|
||||
|
||||
var results = new Dictionary<TPlayer, Rating>();
|
||||
results[winner] = CalculateNewRating(gameInfo, winnerPreviousRating, loserPreviousRating,
|
||||
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Win);
|
||||
results[loser] = CalculateNewRating(gameInfo, loserPreviousRating, winnerPreviousRating,
|
||||
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Lose);
|
||||
|
||||
// And we're done!
|
||||
return results;
|
||||
}
|
||||
|
||||
private static Rating CalculateNewRating(GameInfo gameInfo, Rating selfRating, Rating opponentRating,
|
||||
PairwiseComparison comparison)
|
||||
{
|
||||
double drawMargin = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
|
||||
|
||||
double c =
|
||||
Math.Sqrt(
|
||||
Square(selfRating.StandardDeviation)
|
||||
+
|
||||
Square(opponentRating.StandardDeviation)
|
||||
+
|
||||
2*Square(gameInfo.Beta));
|
||||
|
||||
double winningMean = selfRating.Mean;
|
||||
double losingMean = opponentRating.Mean;
|
||||
|
||||
switch (comparison)
|
||||
{
|
||||
case PairwiseComparison.Win:
|
||||
case PairwiseComparison.Draw:
|
||||
// NOP
|
||||
break;
|
||||
case PairwiseComparison.Lose:
|
||||
winningMean = opponentRating.Mean;
|
||||
losingMean = selfRating.Mean;
|
||||
break;
|
||||
}
|
||||
|
||||
double meanDelta = winningMean - losingMean;
|
||||
|
||||
double v;
|
||||
double w;
|
||||
double rankMultiplier;
|
||||
|
||||
if (comparison != PairwiseComparison.Draw)
|
||||
{
|
||||
// non-draw case
|
||||
v = TruncatedGaussianCorrectionFunctions.VExceedsMargin(meanDelta, drawMargin, c);
|
||||
w = TruncatedGaussianCorrectionFunctions.WExceedsMargin(meanDelta, drawMargin, c);
|
||||
rankMultiplier = (int) comparison;
|
||||
}
|
||||
else
|
||||
{
|
||||
v = TruncatedGaussianCorrectionFunctions.VWithinMargin(meanDelta, drawMargin, c);
|
||||
w = TruncatedGaussianCorrectionFunctions.WWithinMargin(meanDelta, drawMargin, c);
|
||||
rankMultiplier = 1;
|
||||
}
|
||||
|
||||
double meanMultiplier = (Square(selfRating.StandardDeviation) + Square(gameInfo.DynamicsFactor))/c;
|
||||
|
||||
double varianceWithDynamics = Square(selfRating.StandardDeviation) + Square(gameInfo.DynamicsFactor);
|
||||
double stdDevMultiplier = varianceWithDynamics/Square(c);
|
||||
|
||||
double newMean = selfRating.Mean + (rankMultiplier*meanMultiplier*v);
|
||||
double newStdDev = Math.Sqrt(varianceWithDynamics*(1 - w*stdDevMultiplier));
|
||||
|
||||
return new Rating(newMean, newStdDev);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
Guard.ArgumentNotNull(gameInfo, "gameInfo");
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
|
||||
Rating player1Rating = teams.First().Values.First();
|
||||
Rating player2Rating = teams.Last().Values.First();
|
||||
|
||||
// We just use equation 4.1 found on page 8 of the TrueSkill 2006 paper:
|
||||
double betaSquared = Square(gameInfo.Beta);
|
||||
double player1SigmaSquared = Square(player1Rating.StandardDeviation);
|
||||
double player2SigmaSquared = Square(player2Rating.StandardDeviation);
|
||||
|
||||
// This is the square root part of the equation:
|
||||
double sqrtPart =
|
||||
Math.Sqrt(
|
||||
(2*betaSquared)
|
||||
/
|
||||
(2*betaSquared + player1SigmaSquared + player2SigmaSquared));
|
||||
|
||||
// This is the exponent part of the equation:
|
||||
double expPart =
|
||||
Math.Exp(
|
||||
(-1*Square(player1Rating.Mean - player2Rating.Mean))
|
||||
/
|
||||
(2*(2*betaSquared + player1SigmaSquared + player2SigmaSquared)));
|
||||
|
||||
return sqrtPart*expPart;
|
||||
}
|
||||
}
|
||||
}
|
173
Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs
Normal file
173
Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Moserware.Skills.Numerics;
|
||||
|
||||
namespace Moserware.Skills.TrueSkill
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates new ratings for only two teams where each team has 1 or more players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When you only have two teams, the math is still simple: no factor graphs are used yet.
|
||||
/// </remarks>
|
||||
public class TwoTeamTrueSkillCalculator : SkillCalculator
|
||||
{
|
||||
public TwoTeamTrueSkillCalculator()
|
||||
: base(SupportedOptions.None, Range<TeamsRange>.Exactly(2), Range<PlayersRange>.AtLeast(1))
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable
|
||||
<IDictionary<TPlayer, Rating>>
|
||||
teams, params int[] teamRanks)
|
||||
{
|
||||
Guard.ArgumentNotNull(gameInfo, "gameInfo");
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
|
||||
RankSorter.Sort(ref teams, ref teamRanks);
|
||||
|
||||
IDictionary<TPlayer, Rating> team1 = teams.First();
|
||||
IDictionary<TPlayer, Rating> team2 = teams.Last();
|
||||
|
||||
bool wasDraw = (teamRanks[0] == teamRanks[1]);
|
||||
|
||||
var results = new Dictionary<TPlayer, Rating>();
|
||||
|
||||
UpdatePlayerRatings(gameInfo,
|
||||
results,
|
||||
team1,
|
||||
team2,
|
||||
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Win);
|
||||
|
||||
UpdatePlayerRatings(gameInfo,
|
||||
results,
|
||||
team2,
|
||||
team1,
|
||||
wasDraw ? PairwiseComparison.Draw : PairwiseComparison.Lose);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void UpdatePlayerRatings<TPlayer>(GameInfo gameInfo,
|
||||
IDictionary<TPlayer, Rating> newPlayerRatings,
|
||||
IDictionary<TPlayer, Rating> selfTeam,
|
||||
IDictionary<TPlayer, Rating> otherTeam,
|
||||
PairwiseComparison selfToOtherTeamComparison)
|
||||
{
|
||||
double drawMargin = DrawMargin.GetDrawMarginFromDrawProbability(gameInfo.DrawProbability, gameInfo.Beta);
|
||||
double betaSquared = Square(gameInfo.Beta);
|
||||
double tauSquared = Square(gameInfo.DynamicsFactor);
|
||||
|
||||
int totalPlayers = selfTeam.Count() + otherTeam.Count();
|
||||
|
||||
double selfMeanSum = selfTeam.Values.Sum(r => r.Mean);
|
||||
double otherTeamMeanSum = otherTeam.Values.Sum(r => r.Mean);
|
||||
|
||||
double c = Math.Sqrt(selfTeam.Values.Sum(r => Square(r.StandardDeviation))
|
||||
+
|
||||
otherTeam.Values.Sum(r => Square(r.StandardDeviation))
|
||||
+
|
||||
totalPlayers*betaSquared);
|
||||
|
||||
double winningMean = selfMeanSum;
|
||||
double losingMean = otherTeamMeanSum;
|
||||
|
||||
switch (selfToOtherTeamComparison)
|
||||
{
|
||||
case PairwiseComparison.Win:
|
||||
case PairwiseComparison.Draw:
|
||||
// NOP
|
||||
break;
|
||||
case PairwiseComparison.Lose:
|
||||
winningMean = otherTeamMeanSum;
|
||||
losingMean = selfMeanSum;
|
||||
break;
|
||||
}
|
||||
|
||||
double meanDelta = winningMean - losingMean;
|
||||
|
||||
double v;
|
||||
double w;
|
||||
double rankMultiplier;
|
||||
|
||||
if (selfToOtherTeamComparison != PairwiseComparison.Draw)
|
||||
{
|
||||
// non-draw case
|
||||
v = TruncatedGaussianCorrectionFunctions.VExceedsMargin(meanDelta, drawMargin, c);
|
||||
w = TruncatedGaussianCorrectionFunctions.WExceedsMargin(meanDelta, drawMargin, c);
|
||||
rankMultiplier = (int) selfToOtherTeamComparison;
|
||||
}
|
||||
else
|
||||
{
|
||||
// assume draw
|
||||
v = TruncatedGaussianCorrectionFunctions.VWithinMargin(meanDelta, drawMargin, c);
|
||||
w = TruncatedGaussianCorrectionFunctions.WWithinMargin(meanDelta, drawMargin, c);
|
||||
rankMultiplier = 1;
|
||||
}
|
||||
|
||||
foreach (var teamPlayerRatingPair in selfTeam)
|
||||
{
|
||||
Rating previousPlayerRating = teamPlayerRatingPair.Value;
|
||||
|
||||
double meanMultiplier = (Square(previousPlayerRating.StandardDeviation) + tauSquared)/c;
|
||||
double stdDevMultiplier = (Square(previousPlayerRating.StandardDeviation) + tauSquared)/Square(c);
|
||||
|
||||
double playerMeanDelta = (rankMultiplier*meanMultiplier*v);
|
||||
double newMean = previousPlayerRating.Mean + playerMeanDelta;
|
||||
|
||||
double newStdDev =
|
||||
Math.Sqrt((Square(previousPlayerRating.StandardDeviation) + tauSquared)*(1 - w*stdDevMultiplier));
|
||||
|
||||
newPlayerRatings[teamPlayerRatingPair.Key] = new Rating(newMean, newStdDev);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
Guard.ArgumentNotNull(gameInfo, "gameInfo");
|
||||
ValidateTeamCountAndPlayersCountPerTeam(teams);
|
||||
|
||||
// We've verified that there's just two teams
|
||||
ICollection<Rating> team1 = teams.First().Values;
|
||||
int team1Count = team1.Count();
|
||||
|
||||
ICollection<Rating> team2 = teams.Last().Values;
|
||||
int team2Count = team2.Count();
|
||||
|
||||
int totalPlayers = team1Count + team2Count;
|
||||
|
||||
double betaSquared = Square(gameInfo.Beta);
|
||||
|
||||
double team1MeanSum = team1.Sum(r => r.Mean);
|
||||
double team1StdDevSquared = team1.Sum(r => Square(r.StandardDeviation));
|
||||
|
||||
double team2MeanSum = team2.Sum(r => r.Mean);
|
||||
double team2SigmaSquared = team2.Sum(r => Square(r.StandardDeviation));
|
||||
|
||||
// This comes from equation 4.1 in the TrueSkill paper on page 8
|
||||
// The equation was broken up into the part under the square root sign and
|
||||
// the exponential part to make the code easier to read.
|
||||
|
||||
double sqrtPart
|
||||
= Math.Sqrt(
|
||||
(totalPlayers*betaSquared)
|
||||
/
|
||||
(totalPlayers*betaSquared + team1StdDevSquared + team2SigmaSquared)
|
||||
);
|
||||
|
||||
double expPart
|
||||
= Math.Exp(
|
||||
(-1*Square(team1MeanSum - team2MeanSum))
|
||||
/
|
||||
(2*(totalPlayers*betaSquared + team1StdDevSquared + team2SigmaSquared))
|
||||
);
|
||||
|
||||
return expPart*sqrtPart;
|
||||
}
|
||||
}
|
||||
}
|
45
Skills/TrueSkillCalculator.cs
Normal file
45
Skills/TrueSkillCalculator.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using System.Collections.Generic;
|
||||
using Moserware.Skills.TrueSkill;
|
||||
|
||||
namespace Moserware.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates a TrueSkill rating using <see cref="FactorGraphTrueSkillCalculator"/>.
|
||||
/// </summary>
|
||||
public static class TrueSkillCalculator
|
||||
{
|
||||
// Keep a singleton around
|
||||
private static readonly SkillCalculator _Calculator
|
||||
= new FactorGraphTrueSkillCalculator();
|
||||
|
||||
/// <summary>
|
||||
/// Calculates new ratings based on the prior ratings and team ranks.
|
||||
/// </summary>
|
||||
/// <param name="gameInfo">Parameters for the game.</param>
|
||||
/// <param name="teams">A mapping of team players and their ratings.</param>
|
||||
/// <param name="teamRanks">The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2)</param>
|
||||
/// <returns>All the players and their new ratings.</returns>
|
||||
public static IDictionary<TPlayer, Rating> CalculateNewRatings<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable
|
||||
<IDictionary<TPlayer, Rating>> teams,
|
||||
params int[] teamRanks)
|
||||
{
|
||||
// Just punt the work to the full implementation
|
||||
return _Calculator.CalculateNewRatings(gameInfo, teams, teamRanks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the match quality as the likelihood of all teams drawing.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlayer">The underlying type of the player.</typeparam>
|
||||
/// <param name="gameInfo">Parameters for the game.</param>
|
||||
/// <param name="teams">A mapping of team players and their ratings.</param>
|
||||
/// <returns>The match quality as a percentage (between 0.0 and 1.0).</returns>
|
||||
public static double CalculateMatchQuality<TPlayer>(GameInfo gameInfo,
|
||||
IEnumerable<IDictionary<TPlayer, Rating>> teams)
|
||||
{
|
||||
// Just punt the work to the full implementation
|
||||
return _Calculator.CalculateMatchQuality(gameInfo, teams);
|
||||
}
|
||||
}
|
||||
}
|
BIN
Skills/bin/Debug/Moserware.Skills.dll
Normal file
BIN
Skills/bin/Debug/Moserware.Skills.dll
Normal file
Binary file not shown.
BIN
Skills/bin/Debug/Moserware.Skills.pdb
Normal file
BIN
Skills/bin/Debug/Moserware.Skills.pdb
Normal file
Binary file not shown.
51
UnitTests/Elo/DuellingEloTest.cs
Normal file
51
UnitTests/Elo/DuellingEloTest.cs
Normal file
@ -0,0 +1,51 @@
|
||||
using Moserware.Skills;
|
||||
using Moserware.Skills.Elo;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Elo
|
||||
{
|
||||
[TestFixture]
|
||||
public class DuellingEloTest
|
||||
{
|
||||
private const double ErrorTolerance = 0.1;
|
||||
|
||||
[Test]
|
||||
public void TwoOnTwoDuellingTest()
|
||||
{
|
||||
var calculator = new DuellingEloCalculator(new GaussianEloCalculator());
|
||||
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating)
|
||||
.AddPlayer(player2, gameInfo.DefaultRating);
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// TODO: Verify?
|
||||
AssertRating(37, newRatingsWinLose[player1]);
|
||||
AssertRating(37, newRatingsWinLose[player2]);
|
||||
AssertRating(13, newRatingsWinLose[player3]);
|
||||
AssertRating(13, newRatingsWinLose[player4]);
|
||||
|
||||
var quality = calculator.CalculateMatchQuality(gameInfo, teams);
|
||||
Assert.AreEqual(1.0, quality, 0.001);
|
||||
}
|
||||
|
||||
private static void AssertRating(double expected, Rating actual)
|
||||
{
|
||||
Assert.AreEqual(expected, actual.Mean, ErrorTolerance);
|
||||
}
|
||||
}
|
||||
}
|
37
UnitTests/Elo/EloAssert.cs
Normal file
37
UnitTests/Elo/EloAssert.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using Moserware.Skills;
|
||||
using Moserware.Skills.Elo;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Elo
|
||||
{
|
||||
internal static class EloAssert
|
||||
{
|
||||
private const double ErrorTolerance = 0.1;
|
||||
|
||||
public static void AssertChessRating(TwoPlayerEloCalculator calculator,
|
||||
double player1BeforeRating,
|
||||
double player2BeforeRating,
|
||||
PairwiseComparison player1Result,
|
||||
double player1AfterRating,
|
||||
double player2AfterRating)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var teams = Teams.Concat(
|
||||
new Team(player1, new EloRating(player1BeforeRating)),
|
||||
new Team(player2, new EloRating(player2BeforeRating)));
|
||||
|
||||
var chessGameInfo = new GameInfo(1200, 0, 200, 0, 0);
|
||||
|
||||
var result = calculator.CalculateNewRatings(chessGameInfo, teams,
|
||||
(player1Result == PairwiseComparison.Win) ? new[] { 1, 2 } :
|
||||
(player1Result == PairwiseComparison.Lose) ? new[] { 2, 1 } :
|
||||
new[] { 1, 1 });
|
||||
|
||||
|
||||
Assert.AreEqual(player1AfterRating, result[player1].Mean, ErrorTolerance);
|
||||
Assert.AreEqual(player2AfterRating, result[player2].Mean, ErrorTolerance);
|
||||
}
|
||||
}
|
||||
}
|
36
UnitTests/Elo/FideEloCalculatorTest.cs
Normal file
36
UnitTests/Elo/FideEloCalculatorTest.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Moserware.Skills;
|
||||
using Moserware.Skills.Elo;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Elo
|
||||
{
|
||||
[TestFixture]
|
||||
public class FideEloCalculatorTest
|
||||
{
|
||||
[Test]
|
||||
public void FideProvisionalEloCalculatorTests()
|
||||
{
|
||||
// verified against http://ratings.fide.com/calculator_rtd.phtml
|
||||
var calc = new FideEloCalculator(new FideKFactor.Provisional());
|
||||
|
||||
EloAssert.AssertChessRating(calc, 1200, 1500, PairwiseComparison.Win, 1221.25, 1478.75);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1500, PairwiseComparison.Draw, 1208.75, 1491.25);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1500, PairwiseComparison.Lose, 1196.25, 1503.75);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FideNonProvisionalEloCalculatorTests()
|
||||
{
|
||||
// verified against http://ratings.fide.com/calculator_rtd.phtml
|
||||
var calc = new FideEloCalculator();
|
||||
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Win, 1207.5, 1192.5);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Draw, 1200, 1200);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Lose, 1192.5, 1207.5);
|
||||
|
||||
EloAssert.AssertChessRating(calc, 2600, 2500, PairwiseComparison.Win, 2603.6, 2496.4);
|
||||
EloAssert.AssertChessRating(calc, 2600, 2500, PairwiseComparison.Draw, 2598.6, 2501.4);
|
||||
EloAssert.AssertChessRating(calc, 2600, 2500, PairwiseComparison.Lose, 2593.6, 2506.4);
|
||||
}
|
||||
}
|
||||
}
|
26
UnitTests/Elo/GaussianEloCalculatorTest.cs
Normal file
26
UnitTests/Elo/GaussianEloCalculatorTest.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Moserware.Skills;
|
||||
using Moserware.Skills.Elo;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Elo
|
||||
{
|
||||
[TestFixture]
|
||||
public class GaussianEloCalculatorTest
|
||||
{
|
||||
[Test]
|
||||
public void GaussianEloCalculatorTests()
|
||||
{
|
||||
const double defaultKFactor = 24.0;
|
||||
var calc = new GaussianEloCalculator();
|
||||
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Win, 1212, 1188);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Draw, 1200, 1200);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1200, PairwiseComparison.Lose, 1188, 1212);
|
||||
|
||||
// verified using TrueSkill paper equation
|
||||
EloAssert.AssertChessRating(calc, 1200, 1000, PairwiseComparison.Win, 1200 + ((1 - 0.76024993890652326884) * defaultKFactor), 1000 - (1 - 0.76024993890652326884) * defaultKFactor);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1000, PairwiseComparison.Draw, 1200 - (0.76024993890652326884 - 0.5) * defaultKFactor, 1000 + (0.76024993890652326884 - 0.5) * defaultKFactor);
|
||||
EloAssert.AssertChessRating(calc, 1200, 1000, PairwiseComparison.Lose, 1200 - 0.76024993890652326884 * defaultKFactor, 1000 + 0.76024993890652326884 * defaultKFactor);
|
||||
}
|
||||
}
|
||||
}
|
68
UnitTests/Numerics/GaussianDistributionTests.cs
Normal file
68
UnitTests/Numerics/GaussianDistributionTests.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using Moserware.Numerics;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Numerics
|
||||
{
|
||||
[TestFixture]
|
||||
public class GaussianDistributionTests
|
||||
{
|
||||
private const double ErrorTolerance = 0.000001;
|
||||
|
||||
[Test]
|
||||
public void MultiplicationTests()
|
||||
{
|
||||
// I verified this against the formula at http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf
|
||||
var standardNormal = new GaussianDistribution(0, 1);
|
||||
var shiftedGaussian = new GaussianDistribution(2, 3);
|
||||
|
||||
var product = standardNormal * shiftedGaussian;
|
||||
|
||||
Assert.AreEqual(0.2, product.Mean, ErrorTolerance);
|
||||
Assert.AreEqual(3.0 / Math.Sqrt(10), product.StandardDeviation, ErrorTolerance);
|
||||
|
||||
var m4s5 = new GaussianDistribution(4, 5);
|
||||
var m6s7 = new GaussianDistribution(6, 7);
|
||||
|
||||
var product2 = m4s5 * m6s7;
|
||||
Func<double, double> square = x => x*x;
|
||||
|
||||
var expectedMean = (4 * square(7) + 6 * square(5)) / (square(5) + square(7));
|
||||
Assert.AreEqual(expectedMean, product2.Mean, ErrorTolerance);
|
||||
|
||||
var expectedSigma = Math.Sqrt(((square(5) * square(7)) / (square(5) + square(7))));
|
||||
Assert.AreEqual(expectedSigma, product2.StandardDeviation, ErrorTolerance);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DivisionTests()
|
||||
{
|
||||
// Since the multiplication was worked out by hand, we use the same numbers but work backwards
|
||||
var product = new GaussianDistribution(0.2, 3.0 / Math.Sqrt(10));
|
||||
var standardNormal = new GaussianDistribution(0, 1);
|
||||
|
||||
var productDividedByStandardNormal = product / standardNormal;
|
||||
Assert.AreEqual(2.0, productDividedByStandardNormal.Mean, ErrorTolerance);
|
||||
Assert.AreEqual(3.0, productDividedByStandardNormal.StandardDeviation, ErrorTolerance);
|
||||
|
||||
Func<double, double> square = x => x * x;
|
||||
var product2 = new GaussianDistribution((4 * square(7) + 6 * square(5)) / (square(5) + square(7)), Math.Sqrt(((square(5) * square(7)) / (square(5) + square(7)))));
|
||||
var m4s5 = new GaussianDistribution(4,5);
|
||||
var product2DividedByM4S5 = product2 / m4s5;
|
||||
Assert.AreEqual(6.0, product2DividedByM4S5.Mean, ErrorTolerance);
|
||||
Assert.AreEqual(7.0, product2DividedByM4S5.StandardDeviation, ErrorTolerance);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void LogProductNormalizationTests()
|
||||
{
|
||||
var m4s5 = new GaussianDistribution(4, 5);
|
||||
var m6s7 = new GaussianDistribution(6, 7);
|
||||
|
||||
var product2 = m4s5 * m6s7;
|
||||
var normConstant = 1.0 / (Math.Sqrt(2 * Math.PI) * product2.StandardDeviation);
|
||||
var lpn = GaussianDistribution.LogProductNormalization(m4s5, m6s7);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
186
UnitTests/Numerics/MatrixTests.cs
Normal file
186
UnitTests/Numerics/MatrixTests.cs
Normal file
@ -0,0 +1,186 @@
|
||||
using Moserware.Numerics;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.Numerics
|
||||
{
|
||||
[TestFixture]
|
||||
public class MatrixTests
|
||||
{
|
||||
[Test]
|
||||
public void TwoByTwoDeterminantTests()
|
||||
{
|
||||
var a = new SquareMatrix(1, 2,
|
||||
3, 4);
|
||||
Assert.AreEqual(-2, a.Determinant);
|
||||
|
||||
var b = new SquareMatrix(3, 4,
|
||||
5, 6);
|
||||
Assert.AreEqual(-2, b.Determinant);
|
||||
|
||||
var c = new SquareMatrix(1, 1,
|
||||
1, 1);
|
||||
Assert.AreEqual(0, c.Determinant);
|
||||
|
||||
var d = new SquareMatrix(12, 15,
|
||||
17, 21);
|
||||
Assert.AreEqual(12 * 21 - 15 * 17, d.Determinant);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ThreeByThreeDeterminantTests()
|
||||
{
|
||||
var a = new SquareMatrix(1, 2, 3,
|
||||
4, 5, 6,
|
||||
7, 8, 9);
|
||||
Assert.AreEqual(0, a.Determinant);
|
||||
|
||||
var π = new SquareMatrix(3, 1, 4,
|
||||
1, 5, 9,
|
||||
2, 6, 5);
|
||||
|
||||
// Verified against http://www.wolframalpha.com/input/?i=determinant+%7B%7B3%2C1%2C4%7D%2C%7B1%2C5%2C9%7D%2C%7B2%2C6%2C5%7D%7D
|
||||
Assert.AreEqual(-90, π.Determinant);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FourByFourDeterminantTests()
|
||||
{
|
||||
var a = new SquareMatrix( 1, 2, 3, 4,
|
||||
5, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
13, 14, 15, 16);
|
||||
|
||||
Assert.AreEqual(0, a.Determinant);
|
||||
|
||||
var π = new SquareMatrix(3, 1, 4, 1,
|
||||
5, 9, 2, 6,
|
||||
5, 3, 5, 8,
|
||||
9, 7, 9, 3);
|
||||
|
||||
// Verified against http://www.wolframalpha.com/input/?i=determinant+%7B+%7B3%2C1%2C4%2C1%7D%2C+%7B5%2C9%2C2%2C6%7D%2C+%7B5%2C3%2C5%2C8%7D%2C+%7B9%2C7%2C9%2C3%7D%7D
|
||||
Assert.AreEqual(98, π.Determinant);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EightByEightDeterminantTests()
|
||||
{
|
||||
var a = new SquareMatrix( 1, 2, 3, 4, 5, 6, 7, 8,
|
||||
9, 10, 11, 12, 13, 14, 15, 16,
|
||||
17, 18, 19, 20, 21, 22, 23, 24,
|
||||
25, 26, 27, 28, 29, 30, 31, 32,
|
||||
33, 34, 35, 36, 37, 38, 39, 40,
|
||||
41, 42, 32, 44, 45, 46, 47, 48,
|
||||
49, 50, 51, 52, 53, 54, 55, 56,
|
||||
57, 58, 59, 60, 61, 62, 63, 64);
|
||||
|
||||
Assert.AreEqual(0, a.Determinant);
|
||||
|
||||
var π = new SquareMatrix(3, 1, 4, 1, 5, 9, 2, 6,
|
||||
5, 3, 5, 8, 9, 7, 9, 3,
|
||||
2, 3, 8, 4, 6, 2, 6, 4,
|
||||
3, 3, 8, 3, 2, 7, 9, 5,
|
||||
0, 2, 8, 8, 4, 1, 9, 7,
|
||||
1, 6, 9, 3, 9, 9, 3, 7,
|
||||
5, 1, 0, 5, 8, 2, 0, 9,
|
||||
7, 4, 9, 4, 4, 5, 9, 2);
|
||||
|
||||
// Verified against http://www.wolframalpha.com/input/?i=det+%7B%7B3%2C1%2C4%2C1%2C5%2C9%2C2%2C6%7D%2C%7B5%2C3%2C5%2C8%2C9%2C7%2C9%2C3%7D%2C%7B2%2C3%2C8%2C4%2C6%2C2%2C6%2C4%7D%2C%7B3%2C3%2C8%2C3%2C2%2C7%2C9%2C5%7D%2C%7B0%2C2%2C8%2C8%2C4%2C1%2C9%2C7%7D%2C%7B1%2C6%2C9%2C3%2C9%2C9%2C3%2C7%7D%2C%7B5%2C1%2C0%2C5%2C8%2C2%2C0%2C9%7D%2C%7B7%2C4%2C9%2C4%2C4%2C5%2C9%2C2%7D%7D
|
||||
Assert.AreEqual(1378143, π.Determinant);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EqualsTest()
|
||||
{
|
||||
var a = new SquareMatrix(1, 2,
|
||||
3, 4);
|
||||
|
||||
var b = new SquareMatrix(1, 2,
|
||||
3, 4);
|
||||
|
||||
Assert.IsTrue(a == b);
|
||||
Assert.AreEqual(a, b);
|
||||
|
||||
var c = new Matrix(2, 3,
|
||||
1, 2, 3,
|
||||
4, 5, 6);
|
||||
|
||||
var d = new Matrix(2, 3,
|
||||
1, 2, 3,
|
||||
4, 5, 6);
|
||||
|
||||
Assert.IsTrue(c == d);
|
||||
Assert.AreEqual(c, d);
|
||||
|
||||
var e = new Matrix(3, 2,
|
||||
1, 4,
|
||||
2, 5,
|
||||
3, 6);
|
||||
|
||||
var f = e.Transpose;
|
||||
Assert.IsTrue(d == f);
|
||||
Assert.AreEqual(d, f);
|
||||
Assert.AreEqual(d.GetHashCode(), f.GetHashCode());
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AdjugateTests()
|
||||
{
|
||||
// From Wikipedia: http://en.wikipedia.org/wiki/Adjugate_matrix
|
||||
|
||||
var a = new SquareMatrix(1, 2,
|
||||
3, 4);
|
||||
|
||||
var b = new SquareMatrix( 4, -2,
|
||||
-3, 1);
|
||||
|
||||
Assert.AreEqual(b, a.Adjugate);
|
||||
|
||||
|
||||
var c = new SquareMatrix(-3, 2, -5,
|
||||
-1, 0, -2,
|
||||
3, -4, 1);
|
||||
|
||||
var d = new SquareMatrix(-8, 18, -4,
|
||||
-5, 12, -1,
|
||||
4, -6, 2);
|
||||
|
||||
Assert.AreEqual(d, c.Adjugate);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InverseTests()
|
||||
{
|
||||
// see http://www.mathwords.com/i/inverse_of_a_matrix.htm
|
||||
var a = new SquareMatrix(4, 3,
|
||||
3, 2);
|
||||
|
||||
var b = new SquareMatrix(-2, 3,
|
||||
3, -4);
|
||||
|
||||
var aInverse = a.Inverse;
|
||||
Assert.AreEqual(b, aInverse);
|
||||
|
||||
var identity2x2 = new IdentityMatrix(2);
|
||||
|
||||
var aaInverse = a * aInverse;
|
||||
Assert.IsTrue(identity2x2 == aaInverse);
|
||||
|
||||
var c = new SquareMatrix(1, 2, 3,
|
||||
0, 4, 5,
|
||||
1, 0, 6);
|
||||
|
||||
var cInverse = c.Inverse;
|
||||
var d = (1.0 / 22) * new SquareMatrix(24, -12, -2,
|
||||
5, 3, -5,
|
||||
-4, 2, 4);
|
||||
|
||||
|
||||
Assert.IsTrue(d == cInverse);
|
||||
var identity3x3 = new IdentityMatrix(3);
|
||||
|
||||
var ccInverse = c * cInverse;
|
||||
Assert.IsTrue(identity3x3 == ccInverse);
|
||||
}
|
||||
}
|
||||
}
|
35
UnitTests/Properties/AssemblyInfo.cs
Normal file
35
UnitTests/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("UnitTests")]
|
||||
[assembly: AssemblyDescription("Unit tests for Moserware.Skills")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Jeff Moser")]
|
||||
[assembly: AssemblyProduct("UnitTests")]
|
||||
[assembly: AssemblyCopyright("Copyright © Jeff Moser 2010")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("ddd7d430-f9c0-45c8-9576-70418d766e1f")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
23
UnitTests/README.txt
Normal file
23
UnitTests/README.txt
Normal file
@ -0,0 +1,23 @@
|
||||
These tests were written using NUnit 2.5.2 that is available for download
|
||||
at:
|
||||
|
||||
http://sourceforge.net/projects/nunit/files/NUnit%20Version%202/NUnit-2.5.2.9222.msi/download
|
||||
|
||||
If you have a different version or setup, you'll need to update the path under
|
||||
the UnitTests project properties by right clicking on UnitTests and then
|
||||
click "properties" and then click the "debug" tab. The "start external program"
|
||||
points to the NUnit test runner.
|
||||
|
||||
I did it this way so you didn't need more than the express version of
|
||||
Visual Studio to run. If you have a fancy test runner already, feel
|
||||
free to use that.
|
||||
|
||||
Additionally, it should be easy to update the tests to your tool
|
||||
of choice.
|
||||
|
||||
Finally, realize that these tests test *all* of the calculators
|
||||
implementations. For that reason, they create a new instance of
|
||||
a particular calculator. If you're using this code in your application,
|
||||
you can just use the convenience helper class of "TrueSkillCalculator"
|
||||
that has static methods. If you do that, you won't have to worry
|
||||
about creating your own instances.
|
34
UnitTests/RankSorterTest.cs
Normal file
34
UnitTests/RankSorterTest.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using Moserware.Skills;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class RankSorterTest
|
||||
{
|
||||
[Test]
|
||||
public void SortAlreadySortedTest()
|
||||
{
|
||||
IEnumerable<string> people = new[] { "One", "Two", "Three" };
|
||||
int[] ranks = new[] { 1, 2, 3 };
|
||||
|
||||
RankSorter.Sort(ref people, ref ranks);
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "One", "Two", "Three" }, people);
|
||||
CollectionAssert.AreEqual(new[] { 1, 2, 3 }, ranks);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SortUnsortedTest()
|
||||
{
|
||||
IEnumerable<string> people = new[] { "Five", "Two1", "Two2", "One", "Four" };
|
||||
int[] ranks = new[] { 5, 2, 2, 1, 4 };
|
||||
|
||||
RankSorter.Sort(ref people, ref ranks);
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "One", "Two1", "Two2", "Four", "Five" }, people);
|
||||
CollectionAssert.AreEqual(new[] { 1, 2, 2, 4, 5 }, ranks);
|
||||
}
|
||||
}
|
||||
}
|
27
UnitTests/TrueSkill/DrawMarginTest.cs
Normal file
27
UnitTests/TrueSkill/DrawMarginTest.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using Moserware.Skills.TrueSkill;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.TrueSkill
|
||||
{
|
||||
[TestFixture]
|
||||
public class DrawMarginTest
|
||||
{
|
||||
private const double ErrorTolerance = .000001;
|
||||
|
||||
[Test]
|
||||
public void GetDrawMarginFromDrawProbabilityTest()
|
||||
{
|
||||
double beta = 25.0 / 6.0;
|
||||
// The expected values were compared against Ralf Herbrich's implementation in F#
|
||||
AssertDrawMargin(0.10, beta, 0.74046637542690541);
|
||||
AssertDrawMargin(0.25, beta, 1.87760059883033);
|
||||
AssertDrawMargin(0.33, beta, 2.5111010132487492);
|
||||
}
|
||||
|
||||
private static void AssertDrawMargin(double drawProbability, double beta, double expected)
|
||||
{
|
||||
double actual = DrawMargin.GetDrawMarginFromDrawProbability(drawProbability, beta);
|
||||
Assert.AreEqual(expected, actual, ErrorTolerance);
|
||||
}
|
||||
}
|
||||
}
|
22
UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs
Normal file
22
UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using Moserware.Skills.TrueSkill;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.TrueSkill
|
||||
{
|
||||
[TestFixture]
|
||||
public class FactorGraphTrueSkillCalculatorTests
|
||||
{
|
||||
[Test]
|
||||
public void FullFactorGraphCalculatorTests()
|
||||
{
|
||||
var calculator = new FactorGraphTrueSkillCalculator();
|
||||
|
||||
// We can test all classes
|
||||
TrueSkillCalculatorTests.TestAllTwoPlayerScenarios(calculator);
|
||||
TrueSkillCalculatorTests.TestAllTwoTeamScenarios(calculator);
|
||||
TrueSkillCalculatorTests.TestAllMultipleTeamScenarios(calculator);
|
||||
|
||||
TrueSkillCalculatorTests.TestPartialPlayScenarios(calculator);
|
||||
}
|
||||
}
|
||||
}
|
987
UnitTests/TrueSkill/TrueSkillCalculatorTests.cs
Normal file
987
UnitTests/TrueSkill/TrueSkillCalculatorTests.cs
Normal file
@ -0,0 +1,987 @@
|
||||
using Moserware.Skills;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.TrueSkill
|
||||
{
|
||||
public static class TrueSkillCalculatorTests
|
||||
{
|
||||
private const double ErrorTolerance = 0.085;
|
||||
|
||||
// These are the roll-up ones
|
||||
|
||||
public static void TestAllTwoPlayerScenarios(SkillCalculator calculator)
|
||||
{
|
||||
TwoPlayerTestNotDrawn(calculator);
|
||||
TwoPlayerTestDrawn(calculator);
|
||||
OneOnOneMassiveUpsetDrawTest(calculator);
|
||||
|
||||
TwoPlayerChessTestNotDrawn(calculator);
|
||||
}
|
||||
|
||||
public static void TestAllTwoTeamScenarios(SkillCalculator calculator)
|
||||
{
|
||||
OneOnTwoSimpleTest(calculator);
|
||||
OneOnTwoDrawTest(calculator);
|
||||
OneOnTwoSomewhatBalanced(calculator);
|
||||
OneOnThreeDrawTest(calculator);
|
||||
OneOnThreeSimpleTest(calculator);
|
||||
OneOnSevenSimpleTest(calculator);
|
||||
|
||||
TwoOnTwoSimpleTest(calculator);
|
||||
TwoOnTwoUnbalancedDrawTest(calculator);
|
||||
TwoOnTwoDrawTest(calculator);
|
||||
TwoOnTwoUpsetTest(calculator);
|
||||
|
||||
ThreeOnTwoTests(calculator);
|
||||
|
||||
FourOnFourSimpleTest(calculator);
|
||||
}
|
||||
|
||||
public static void TestAllMultipleTeamScenarios(SkillCalculator calculator)
|
||||
{
|
||||
ThreeTeamsOfOneNotDrawn(calculator);
|
||||
ThreeTeamsOfOneDrawn(calculator);
|
||||
FourTeamsOfOneNotDrawn(calculator);
|
||||
FiveTeamsOfOneNotDrawn(calculator);
|
||||
EightTeamsOfOneDrawn(calculator);
|
||||
EightTeamsOfOneUpset(calculator);
|
||||
SixteenTeamsOfOneNotDrawn(calculator);
|
||||
|
||||
TwoOnFourOnTwoWinDraw(calculator);
|
||||
}
|
||||
|
||||
public static void TestPartialPlayScenarios(SkillCalculator calculator)
|
||||
{
|
||||
OneOnTwoBalancedPartialPlay(calculator);
|
||||
}
|
||||
|
||||
//------------------- Actual Tests ---------------------------
|
||||
// If you see more than 3 digits of precision in the decimal point, then the expected values calculated from
|
||||
// F# RalfH's implementation with the same input. It didn't support teams, so team values all came from the
|
||||
// online calculator at http://atom.research.microsoft.com/trueskill/rankcalculator.aspx
|
||||
//
|
||||
// All match quality expected values came from the online calculator
|
||||
|
||||
// In both cases, there may be some discrepancy after the first decimal point. I think this is due to my implementation
|
||||
// using slightly higher precision in GaussianDistribution.
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Two Player Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
private static void TwoPlayerTestNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(29.39583201999924, 7.171475587326186, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(20.60416798000076, 7.171475587326186, player2NewRating);
|
||||
|
||||
AssertMatchQuality(0.447, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void TwoPlayerTestDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(25.0, 6.4575196623173081, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(25.0, 6.4575196623173081, player2NewRating);
|
||||
|
||||
AssertMatchQuality(0.447, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void TwoPlayerChessTestNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
// Inspired by a real bug :-)
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var gameInfo = new GameInfo(1200.0, 1200.0 / 3.0, 200.0, 1200.0 / 300.0, 0.03);
|
||||
|
||||
var team1 = new Team(player1, new Rating(1301.0007, 42.9232));
|
||||
var team2 = new Team(player2, new Rating(1188.7560, 42.5570));
|
||||
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, Teams.Concat(team1, team2), 1, 2);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(1304.7820836053318, 42.843513887848658, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(1185.0383099003536, 42.485604606897752, player2NewRating);
|
||||
}
|
||||
|
||||
private static void OneOnOneMassiveUpsetDrawTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, new Rating(50, 12.5));
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(31.662, 7.137, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(35.010, 7.910, newRatingsWinLose[player2]);
|
||||
|
||||
AssertMatchQuality(0.110, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Two Team Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
private static void TwoOnTwoSimpleTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating)
|
||||
.AddPlayer(player2, gameInfo.DefaultRating);
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(28.108, 7.774, newRatingsWinLose[player1]);
|
||||
AssertRating(28.108, 7.774, newRatingsWinLose[player2]);
|
||||
|
||||
// Losers
|
||||
AssertRating(21.892, 7.774, newRatingsWinLose[player3]);
|
||||
AssertRating(21.892, 7.774, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.447, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void TwoOnTwoDrawTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating)
|
||||
.AddPlayer(player2, gameInfo.DefaultRating);
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(25, 7.455, newRatingsWinLose[player1]);
|
||||
AssertRating(25, 7.455, newRatingsWinLose[player2]);
|
||||
|
||||
// Losers
|
||||
AssertRating(25, 7.455, newRatingsWinLose[player3]);
|
||||
AssertRating(25, 7.455, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.447, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void TwoOnTwoUnbalancedDrawTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, new Rating(15, 8))
|
||||
.AddPlayer(player2, new Rating(20, 6));
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, new Rating(25, 4))
|
||||
.AddPlayer(player4, new Rating(30, 3));
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(21.570, 6.556, newRatingsWinLose[player1]);
|
||||
AssertRating(23.696, 5.418, newRatingsWinLose[player2]);
|
||||
|
||||
// Losers
|
||||
AssertRating(23.357, 3.833, newRatingsWinLose[player3]);
|
||||
AssertRating(29.075, 2.931, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.214, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void TwoOnTwoUpsetTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, new Rating(20, 8))
|
||||
.AddPlayer(player2, new Rating(25, 6));
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, new Rating(35, 7))
|
||||
.AddPlayer(player4, new Rating(40, 5));
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(29.698, 7.008, newRatingsWinLose[player1]);
|
||||
AssertRating(30.455, 5.594, newRatingsWinLose[player2]);
|
||||
|
||||
// Losers
|
||||
AssertRating(27.575, 6.346, newRatingsWinLose[player3]);
|
||||
AssertRating(36.211, 4.768, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.084, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void FourOnFourSimpleTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating)
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player5, gameInfo.DefaultRating)
|
||||
.AddPlayer(player6, gameInfo.DefaultRating)
|
||||
.AddPlayer(player7, gameInfo.DefaultRating)
|
||||
.AddPlayer(player8, gameInfo.DefaultRating);
|
||||
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(27.198, 8.059, newRatingsWinLose[player1]);
|
||||
AssertRating(27.198, 8.059, newRatingsWinLose[player2]);
|
||||
AssertRating(27.198, 8.059, newRatingsWinLose[player3]);
|
||||
AssertRating(27.198, 8.059, newRatingsWinLose[player4]);
|
||||
|
||||
// Losers
|
||||
AssertRating(22.802, 8.059, newRatingsWinLose[player5]);
|
||||
AssertRating(22.802, 8.059, newRatingsWinLose[player6]);
|
||||
AssertRating(22.802, 8.059, newRatingsWinLose[player7]);
|
||||
AssertRating(22.802, 8.059, newRatingsWinLose[player8]);
|
||||
|
||||
AssertMatchQuality(0.447, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnTwoSimpleTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(33.730, 7.317, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(16.270, 7.317, newRatingsWinLose[player2]);
|
||||
AssertRating(16.270, 7.317, newRatingsWinLose[player3]);
|
||||
|
||||
AssertMatchQuality(0.135, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnTwoSomewhatBalanced(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, new Rating(40, 6));
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, new Rating(20, 7))
|
||||
.AddPlayer(player3, new Rating(25, 8));
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(42.744, 5.602, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(16.266, 6.359, newRatingsWinLose[player2]);
|
||||
AssertRating(20.123, 7.028, newRatingsWinLose[player3]);
|
||||
|
||||
AssertMatchQuality(0.478, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnThreeSimpleTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(36.337, 7.527, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(13.663, 7.527, newRatingsWinLose[player2]);
|
||||
AssertRating(13.663, 7.527, newRatingsWinLose[player3]);
|
||||
AssertRating(13.663, 7.527, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.012, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnTwoDrawTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(31.660, 7.138, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(18.340, 7.138, newRatingsWinLose[player2]);
|
||||
AssertRating(18.340, 7.138, newRatingsWinLose[player3]);
|
||||
|
||||
AssertMatchQuality(0.135, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnThreeDrawTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(34.990, 7.455, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(15.010, 7.455, newRatingsWinLose[player2]);
|
||||
AssertRating(15.010, 7.455, newRatingsWinLose[player3]);
|
||||
AssertRating(15.010, 7.455, newRatingsWinLose[player4]);
|
||||
|
||||
AssertMatchQuality(0.012, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void OneOnSevenSimpleTest(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, gameInfo.DefaultRating);
|
||||
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player2, gameInfo.DefaultRating)
|
||||
.AddPlayer(player3, gameInfo.DefaultRating)
|
||||
.AddPlayer(player4, gameInfo.DefaultRating)
|
||||
.AddPlayer(player5, gameInfo.DefaultRating)
|
||||
.AddPlayer(player6, gameInfo.DefaultRating)
|
||||
.AddPlayer(player7, gameInfo.DefaultRating)
|
||||
.AddPlayer(player8, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(40.582, 7.917, newRatingsWinLose[player1]);
|
||||
|
||||
// Losers
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player2]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player3]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player4]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player5]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player6]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player7]);
|
||||
AssertRating(9.418, 7.917, newRatingsWinLose[player8]);
|
||||
|
||||
AssertMatchQuality(0.000, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void ThreeOnTwoTests(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, new Rating(28, 7))
|
||||
.AddPlayer(player2, new Rating(27, 6))
|
||||
.AddPlayer(player3, new Rating(26, 5));
|
||||
|
||||
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player4, new Rating(30, 4))
|
||||
.AddPlayer(player5, new Rating(31, 3));
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatingsWinLoseExpected = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(28.658, 6.770, newRatingsWinLoseExpected[player1]);
|
||||
AssertRating(27.484, 5.856, newRatingsWinLoseExpected[player2]);
|
||||
AssertRating(26.336, 4.917, newRatingsWinLoseExpected[player3]);
|
||||
|
||||
// Losers
|
||||
AssertRating(29.785, 3.958, newRatingsWinLoseExpected[player4]);
|
||||
AssertRating(30.879, 2.983, newRatingsWinLoseExpected[player5]);
|
||||
|
||||
var newRatingsWinLoseUpset = calculator.CalculateNewRatings(gameInfo, Teams.Concat(team1, team2), 2, 1);
|
||||
|
||||
// Winners
|
||||
AssertRating(32.012, 3.877, newRatingsWinLoseUpset[player4]);
|
||||
AssertRating(32.132, 2.949, newRatingsWinLoseUpset[player5]);
|
||||
|
||||
// Losers
|
||||
AssertRating(21.840, 6.314, newRatingsWinLoseUpset[player1]);
|
||||
AssertRating(22.474, 5.575, newRatingsWinLoseUpset[player2]);
|
||||
AssertRating(22.857, 4.757, newRatingsWinLoseUpset[player3]);
|
||||
|
||||
AssertMatchQuality(0.254, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Multiple Teams Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
private static void TwoOnFourOnTwoWinDraw(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team()
|
||||
.AddPlayer(player1, new Rating(40,4))
|
||||
.AddPlayer(player2, new Rating(45,3));
|
||||
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(player3, new Rating(20, 7))
|
||||
.AddPlayer(player4, new Rating(19, 6))
|
||||
.AddPlayer(player5, new Rating(30, 9))
|
||||
.AddPlayer(player6, new Rating(10, 4));
|
||||
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
|
||||
var team3 = new Team()
|
||||
.AddPlayer(player7, new Rating(50,5))
|
||||
.AddPlayer(player8, new Rating(30,2));
|
||||
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3);
|
||||
var newRatingsWinLose = calculator.CalculateNewRatings(gameInfo, teams, 1, 2, 2);
|
||||
|
||||
// Winners
|
||||
AssertRating(40.877, 3.840, newRatingsWinLose[player1]);
|
||||
AssertRating(45.493, 2.934, newRatingsWinLose[player2]);
|
||||
AssertRating(19.609, 6.396, newRatingsWinLose[player3]);
|
||||
AssertRating(18.712, 5.625, newRatingsWinLose[player4]);
|
||||
AssertRating(29.353, 7.673, newRatingsWinLose[player5]);
|
||||
AssertRating(9.872, 3.891, newRatingsWinLose[player6]);
|
||||
AssertRating(48.830, 4.590, newRatingsWinLose[player7]);
|
||||
AssertRating(29.813, 1.976, newRatingsWinLose[player8]);
|
||||
|
||||
AssertMatchQuality(0.367, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void ThreeTeamsOfOneNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2, 3);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(31.675352419172107, 6.6559853776206905, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(25.000000000003912, 6.2078966412243233, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(18.324647580823971, 6.6559853776218318, player3NewRating);
|
||||
|
||||
AssertMatchQuality(0.200, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void ThreeTeamsOfOneDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 1, 1);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(25.000, 5.698, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(25.000, 5.695, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(25.000, 5.698, player3NewRating);
|
||||
|
||||
AssertMatchQuality(0.200, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void FourTeamsOfOneNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
var team4 = new Team(player4, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3, team4);
|
||||
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2, 3, 4);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(33.206680965631264, 6.3481091698077057, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(27.401454693843323, 5.7871629348447584, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(22.598545306188374, 5.7871629348413451, player3NewRating);
|
||||
|
||||
var player4NewRating = newRatings[player4];
|
||||
AssertRating(16.793319034361271, 6.3481091698144967, player4NewRating);
|
||||
|
||||
AssertMatchQuality(0.089, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void FiveTeamsOfOneNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
var team4 = new Team(player4, gameInfo.DefaultRating);
|
||||
var team5 = new Team(player5, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3, team4, team5);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2, 3, 4, 5);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(34.363135705841188, 6.1361528798112692, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(29.058448805636779, 5.5358352402833413, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(25.000000000031758, 5.4200805474429847, player3NewRating);
|
||||
|
||||
var player4NewRating = newRatings[player4];
|
||||
AssertRating(20.941551194426314, 5.5358352402709672, player4NewRating);
|
||||
|
||||
var player5NewRating = newRatings[player5];
|
||||
AssertRating(15.636864294158848, 6.136152879829349, player5NewRating);
|
||||
|
||||
AssertMatchQuality(0.040, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void EightTeamsOfOneDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
var team4 = new Team(player4, gameInfo.DefaultRating);
|
||||
var team5 = new Team(player5, gameInfo.DefaultRating);
|
||||
var team6 = new Team(player6, gameInfo.DefaultRating);
|
||||
var team7 = new Team(player7, gameInfo.DefaultRating);
|
||||
var team8 = new Team(player8, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3, team4, team5, team6, team7, team8);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 1, 1, 1, 1, 1, 1, 1);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(25.000, 4.592, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(25.000, 4.583, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(25.000, 4.576, player3NewRating);
|
||||
|
||||
var player4NewRating = newRatings[player4];
|
||||
AssertRating(25.000, 4.573, player4NewRating);
|
||||
|
||||
var player5NewRating = newRatings[player5];
|
||||
AssertRating(25.000, 4.573, player5NewRating);
|
||||
|
||||
var player6NewRating = newRatings[player6];
|
||||
AssertRating(25.000, 4.576, player6NewRating);
|
||||
|
||||
var player7NewRating = newRatings[player7];
|
||||
AssertRating(25.000, 4.583, player7NewRating);
|
||||
|
||||
var player8NewRating = newRatings[player8];
|
||||
AssertRating(25.000, 4.592, player8NewRating);
|
||||
|
||||
AssertMatchQuality(0.004, calculator.CalculateMatchQuality(gameInfo, teams));
|
||||
}
|
||||
|
||||
private static void EightTeamsOfOneUpset(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, new Rating(10, 8));
|
||||
var team2 = new Team(player2, new Rating(15, 7));
|
||||
var team3 = new Team(player3, new Rating(20, 6));
|
||||
var team4 = new Team(player4, new Rating(25, 5));
|
||||
var team5 = new Team(player5, new Rating(30, 4));
|
||||
var team6 = new Team(player6, new Rating(35, 3));
|
||||
var team7 = new Team(player7, new Rating(40, 2));
|
||||
var team8 = new Team(player8, new Rating(45, 1));
|
||||
|
||||
var teams = Teams.Concat(team1, team2, team3, team4, team5, team6, team7, team8);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2, 3, 4, 5, 6, 7, 8);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(35.135, 4.506, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(32.585, 4.037, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(31.329, 3.756, player3NewRating);
|
||||
|
||||
var player4NewRating = newRatings[player4];
|
||||
AssertRating(30.984, 3.453, player4NewRating);
|
||||
|
||||
var player5NewRating = newRatings[player5];
|
||||
AssertRating(31.751, 3.064, player5NewRating);
|
||||
|
||||
var player6NewRating = newRatings[player6];
|
||||
AssertRating(34.051, 2.541, player6NewRating);
|
||||
|
||||
var player7NewRating = newRatings[player7];
|
||||
AssertRating(38.263, 1.849, player7NewRating);
|
||||
|
||||
var player8NewRating = newRatings[player8];
|
||||
AssertRating(44.118, 0.983, player8NewRating);
|
||||
|
||||
AssertMatchQuality(0.000, calculator.CalculateMatchQuality(gameInfo,teams));
|
||||
}
|
||||
|
||||
private static void SixteenTeamsOfOneNotDrawn(SkillCalculator calculator)
|
||||
{
|
||||
var player1 = new Player(1);
|
||||
var player2 = new Player(2);
|
||||
var player3 = new Player(3);
|
||||
var player4 = new Player(4);
|
||||
var player5 = new Player(5);
|
||||
var player6 = new Player(6);
|
||||
var player7 = new Player(7);
|
||||
var player8 = new Player(8);
|
||||
var player9 = new Player(9);
|
||||
var player10 = new Player(10);
|
||||
var player11 = new Player(11);
|
||||
var player12 = new Player(12);
|
||||
var player13 = new Player(13);
|
||||
var player14 = new Player(14);
|
||||
var player15 = new Player(15);
|
||||
var player16 = new Player(16);
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var team1 = new Team(player1, gameInfo.DefaultRating);
|
||||
var team2 = new Team(player2, gameInfo.DefaultRating);
|
||||
var team3 = new Team(player3, gameInfo.DefaultRating);
|
||||
var team4 = new Team(player4, gameInfo.DefaultRating);
|
||||
var team5 = new Team(player5, gameInfo.DefaultRating);
|
||||
var team6 = new Team(player6, gameInfo.DefaultRating);
|
||||
var team7 = new Team(player7, gameInfo.DefaultRating);
|
||||
var team8 = new Team(player8, gameInfo.DefaultRating);
|
||||
var team9 = new Team(player9, gameInfo.DefaultRating);
|
||||
var team10 = new Team(player10, gameInfo.DefaultRating);
|
||||
var team11 = new Team(player11, gameInfo.DefaultRating);
|
||||
var team12 = new Team(player12, gameInfo.DefaultRating);
|
||||
var team13 = new Team(player13, gameInfo.DefaultRating);
|
||||
var team14 = new Team(player14, gameInfo.DefaultRating);
|
||||
var team15 = new Team(player15, gameInfo.DefaultRating);
|
||||
var team16 = new Team(player16, gameInfo.DefaultRating);
|
||||
|
||||
var newRatings =
|
||||
calculator.CalculateNewRatings(
|
||||
gameInfo,
|
||||
Teams.Concat(
|
||||
team1, team2, team3, team4, team5,
|
||||
team6, team7, team8, team9, team10,
|
||||
team11, team12, team13, team14, team15,
|
||||
team16),
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
|
||||
|
||||
var player1NewRating = newRatings[player1];
|
||||
AssertRating(40.53945776946920, 5.27581643889050, player1NewRating);
|
||||
|
||||
var player2NewRating = newRatings[player2];
|
||||
AssertRating(36.80951229454210, 4.71121217610266, player2NewRating);
|
||||
|
||||
var player3NewRating = newRatings[player3];
|
||||
AssertRating(34.34726355544460, 4.52440328139991, player3NewRating);
|
||||
|
||||
var player4NewRating = newRatings[player4];
|
||||
AssertRating(32.33614722608720, 4.43258628279632, player4NewRating);
|
||||
|
||||
var player5NewRating = newRatings[player5];
|
||||
AssertRating(30.55048814671730, 4.38010805034365, player5NewRating);
|
||||
|
||||
var player6NewRating = newRatings[player6];
|
||||
AssertRating(28.89277312234790, 4.34859291776483, player6NewRating);
|
||||
|
||||
var player7NewRating = newRatings[player7];
|
||||
AssertRating(27.30952161972210, 4.33037679041216, player7NewRating);
|
||||
|
||||
var player8NewRating = newRatings[player8];
|
||||
AssertRating(25.76571046519540, 4.32197078088701, player8NewRating);
|
||||
|
||||
var player9NewRating = newRatings[player9];
|
||||
AssertRating(24.23428953480470, 4.32197078088703, player9NewRating);
|
||||
|
||||
var player10NewRating = newRatings[player10];
|
||||
AssertRating(22.69047838027800, 4.33037679041219, player10NewRating);
|
||||
|
||||
var player11NewRating = newRatings[player11];
|
||||
AssertRating(21.10722687765220, 4.34859291776488, player11NewRating);
|
||||
|
||||
var player12NewRating = newRatings[player12];
|
||||
AssertRating(19.44951185328290, 4.38010805034375, player12NewRating);
|
||||
|
||||
var player13NewRating = newRatings[player13];
|
||||
AssertRating(17.66385277391300, 4.43258628279643, player13NewRating);
|
||||
|
||||
var player14NewRating = newRatings[player14];
|
||||
AssertRating(15.65273644455550, 4.52440328139996, player14NewRating);
|
||||
|
||||
var player15NewRating = newRatings[player15];
|
||||
AssertRating(13.19048770545810, 4.71121217610273, player15NewRating);
|
||||
|
||||
var player16NewRating = newRatings[player16];
|
||||
AssertRating(9.46054223053080, 5.27581643889032, player16NewRating);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Partial Play Tests
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
private static void OneOnTwoBalancedPartialPlay(SkillCalculator calculator)
|
||||
{
|
||||
var gameInfo = GameInfo.DefaultGameInfo;
|
||||
|
||||
var p1 = new Player(1);
|
||||
var team1 = new Team(p1, gameInfo.DefaultRating);
|
||||
|
||||
var p2 = new Player(2, 0.0);
|
||||
var p3 = new Player(3, 1.00);
|
||||
|
||||
var team2 = new Team()
|
||||
.AddPlayer(p2, gameInfo.DefaultRating)
|
||||
.AddPlayer(p3, gameInfo.DefaultRating);
|
||||
|
||||
var teams = Teams.Concat(team1, team2);
|
||||
var newRatings = calculator.CalculateNewRatings(gameInfo, teams, 1, 2);
|
||||
var matchQuality = calculator.CalculateMatchQuality(gameInfo, teams);
|
||||
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
private static void AssertRating(double expectedMean, double expectedStandardDeviation, Rating actual)
|
||||
{
|
||||
Assert.AreEqual(expectedMean, actual.Mean, ErrorTolerance);
|
||||
Assert.AreEqual(expectedStandardDeviation, actual.StandardDeviation, ErrorTolerance);
|
||||
}
|
||||
|
||||
private static void AssertMatchQuality(double expectedMatchQuality, double actualMatchQuality)
|
||||
{
|
||||
Assert.AreEqual(expectedMatchQuality, actualMatchQuality, 0.0005);
|
||||
}
|
||||
}
|
||||
}
|
20
UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs
Normal file
20
UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs
Normal file
@ -0,0 +1,20 @@
|
||||
using Moserware.Skills.TrueSkill;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.TrueSkill
|
||||
{
|
||||
[TestFixture]
|
||||
public class TwoPlayerTrueSkillCalculatorTest
|
||||
{
|
||||
[Test]
|
||||
public void TwoPlayerTrueSkillCalculatorTests()
|
||||
{
|
||||
var calculator = new TwoPlayerTrueSkillCalculator();
|
||||
|
||||
// We only support two players
|
||||
TrueSkillCalculatorTests.TestAllTwoPlayerScenarios(calculator);
|
||||
|
||||
// TODO: Assert failures for larger teams
|
||||
}
|
||||
}
|
||||
}
|
19
UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs
Normal file
19
UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using Moserware.Skills.TrueSkill;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace UnitTests.TrueSkill
|
||||
{
|
||||
[TestFixture]
|
||||
public class TwoTeamTrueSkillCalculatorTest
|
||||
{
|
||||
[Test]
|
||||
public void TwoTeamTrueSkillCalculatorTests()
|
||||
{
|
||||
var calculator = new TwoTeamTrueSkillCalculator();
|
||||
|
||||
// This calculator supports up to two teams with many players each
|
||||
TrueSkillCalculatorTests.TestAllTwoPlayerScenarios(calculator);
|
||||
TrueSkillCalculatorTests.TestAllTwoTeamScenarios(calculator);
|
||||
}
|
||||
}
|
||||
}
|
80
UnitTests/UnitTests.csproj
Normal file
80
UnitTests/UnitTests.csproj
Normal file
@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>9.0.30729</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{6F80946D-AC8B-4063-8588-96841C18BF0A}</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>UnitTests</RootNamespace>
|
||||
<AssemblyName>UnitTests</AssemblyName>
|
||||
<TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="nunit.framework, Version=2.5.2.9222, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.Linq">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Data.DataSetExtensions">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Elo\DuellingEloTest.cs" />
|
||||
<Compile Include="Elo\EloAssert.cs" />
|
||||
<Compile Include="Elo\GaussianEloCalculatorTest.cs" />
|
||||
<Compile Include="TrueSkill\DrawMarginTest.cs" />
|
||||
<Compile Include="TrueSkill\FactorGraphTrueSkillCalculatorTests.cs" />
|
||||
<Compile Include="Elo\FideEloCalculatorTest.cs" />
|
||||
<Compile Include="Numerics\GaussianDistributionTests.cs" />
|
||||
<Compile Include="Numerics\MatrixTests.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="RankSorterTest.cs" />
|
||||
<Compile Include="TrueSkill\TrueSkillCalculatorTests.cs" />
|
||||
<Compile Include="TrueSkill\TwoPlayerTrueSkillCalculatorTest.cs" />
|
||||
<Compile Include="TrueSkill\TwoTeamTrueSkillCalculatorTest.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Skills\Skills.csproj">
|
||||
<Project>{15AD1345-984C-48ED-AF9A-2EAB44E5AA2B}</Project>
|
||||
<Name>Skills</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="README.txt" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
9
UnitTests/UnitTests.csproj.user
Normal file
9
UnitTests/UnitTests.csproj.user
Normal file
@ -0,0 +1,9 @@
|
||||
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<StartAction>Program</StartAction>
|
||||
<StartProgram>C:\Program Files (x86)\NUnit 2.5.2\bin\net-2.0\nunit.exe</StartProgram>
|
||||
<StartWorkingDirectory>
|
||||
</StartWorkingDirectory>
|
||||
<StartArguments>UnitTests.dll</StartArguments>
|
||||
</PropertyGroup>
|
||||
</Project>
|
Reference in New Issue
Block a user