diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73f6849 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +cmake_install.cmake +CMakeCache.txt +CMakeFiles +Makefile +Moserware.Skills.dll +TestResult.xml +UnitTests.dll + +bin +deploy +deploy/* +obj +*.suo +*.cache +*.tmp + +_Resharper.* + +*.resharper +*.resharper.user \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..419d94d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 2.6) +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/modules) +enable_language(CSharp) + +csharp_add_library(Moserware.Skills + Skills/Elo/DuellingEloCalculator.cs + Skills/Elo/EloRating.cs + Skills/Elo/FideEloCalculator.cs + Skills/Elo/FideKFactor.cs + Skills/Elo/GaussianEloCalculator.cs + Skills/Elo/GaussianKFactor.cs + Skills/Elo/KFactor.cs + Skills/Elo/TwoPlayerEloCalculator.cs + Skills/FactorGraphs/Factor.cs + Skills/FactorGraphs/FactorGraph.cs + Skills/FactorGraphs/FactorGraphLayer.cs + Skills/FactorGraphs/FactorList.cs + Skills/FactorGraphs/Message.cs + Skills/FactorGraphs/Schedule.cs + Skills/FactorGraphs/Variable.cs + Skills/FactorGraphs/VariableFactory.cs + Skills/GameInfo.cs + Skills/Guard.cs + Skills/ISupportPartialPlay.cs + Skills/ISupportPartialUpdate.cs + Skills/Numerics/GaussianDistribution.cs + Skills/Numerics/Matrix.cs + Skills/Numerics/Range.cs + Skills/PairwiseComparison.cs + Skills/PartialPlay.cs + Skills/Player.cs + Skills/PlayersRange.cs + Skills/Properties/AssemblyInfo.cs + Skills/RankSorter.cs + Skills/Rating.cs + Skills/SkillCalculator.cs + Skills/Team.cs + Skills/TeamsRange.cs + Skills/TrueSkill/DrawMargin.cs + Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs + Skills/TrueSkill/TrueSkillFactorGraph.cs + Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs + Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs + Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs + Skills/TrueSkill/Factors/GaussianFactor.cs + Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs + Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs + Skills/TrueSkill/Factors/GaussianPriorFactor.cs + Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs + Skills/TrueSkill/Factors/GaussianWithinFactor.cs + Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs + Skills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.cs + Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs + Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs + Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs + Skills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.cs + Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs + Skills/TrueSkillCalculator.cs +) + +# find_program(NUNIT_CONSOLE nunit-console) + +csharp_add_library(UnitTests + UnitTests/Elo/GaussianEloCalculatorTest.cs + UnitTests/Elo/DuellingEloTest.cs + UnitTests/Elo/EloAssert.cs + UnitTests/Elo/FideEloCalculatorTest.cs + UnitTests/Numerics/MatrixTests.cs + UnitTests/Numerics/GaussianDistributionTests.cs + UnitTests/Properties/AssemblyInfo.cs + UnitTests/RankSorterTest.cs + UnitTests/TrueSkill/DrawMarginTest.cs + UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs + UnitTests/TrueSkill/TrueSkillCalculatorTests.cs + UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs + UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs + REFERENCES Moserware.Skills nunit.framework +) +add_dependencies(UnitTests Moserware.Skills) diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..c7a875e --- /dev/null +++ b/README.txt @@ -0,0 +1,4 @@ +The code for the TrueSkill calculator is in the "Skills" folder and its unit +tests are in the "UnitTests" folder. + +For more details, see the "README" file in each of those folders. \ No newline at end of file diff --git a/Skills.sln b/Skills.sln new file mode 100644 index 0000000..7e5e0f9 --- /dev/null +++ b/Skills.sln @@ -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 diff --git a/Skills/Elo/DuellingEloCalculator.cs b/Skills/Elo/DuellingEloCalculator.cs new file mode 100644 index 0000000..d135608 --- /dev/null +++ b/Skills/Elo/DuellingEloCalculator.cs @@ -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 CalculateNewRatings(GameInfo gameInfo, IEnumerable> 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>(); + + 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(gameInfo, deltas, + currentTeamPlayerRatingPair.Key, currentTeamPlayerRatingPair.Value, + otherTeamPlayerRatingPair.Key, otherTeamPlayerRatingPair.Value, + comparison); + + } + } + } + } + + var result = new Dictionary(); + + 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(GameInfo gameInfo, + IDictionary> duels, + TPlayer player1, Rating player1Rating, + TPlayer player2, Rating player2Rating, + PairwiseComparison weakToStrongComparison) + { + + var duelOutcomes = _TwoPlayerEloCalc.CalculateNewRatings(gameInfo, + Teams.Concat( + new Team(player1, player1Rating), + new Team(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(IDictionary> duels, + TPlayer self, Rating selfBeforeRating, Rating selfAfterRating, + TPlayer opponent ) + { + IDictionary selfToOpponentDuelDeltas; + + if(!duels.TryGetValue(self, out selfToOpponentDuelDeltas)) + { + selfToOpponentDuelDeltas = new Dictionary(); + duels[self] = selfToOpponentDuelDeltas; + } + + selfToOpponentDuelDeltas[opponent] = selfAfterRating.Mean - selfBeforeRating.Mean; + } + + public override double CalculateMatchQuality(GameInfo gameInfo, IEnumerable> 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; + } + } +} \ No newline at end of file diff --git a/Skills/Elo/EloRating.cs b/Skills/Elo/EloRating.cs new file mode 100644 index 0000000..e408c43 --- /dev/null +++ b/Skills/Elo/EloRating.cs @@ -0,0 +1,14 @@ + +namespace Moserware.Skills.Elo +{ + /// + /// An Elo rating represented by a single number (mean). + /// + public class EloRating : Rating + { + public EloRating(double rating) + : base(rating, 0) + { + } + } +} diff --git a/Skills/Elo/FideEloCalculator.cs b/Skills/Elo/FideEloCalculator.cs new file mode 100644 index 0000000..cc3623e --- /dev/null +++ b/Skills/Elo/FideEloCalculator.cs @@ -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)) + ); + } + } +} \ No newline at end of file diff --git a/Skills/Elo/FideKFactor.cs b/Skills/Elo/FideKFactor.cs new file mode 100644 index 0000000..f3c16b2 --- /dev/null +++ b/Skills/Elo/FideKFactor.cs @@ -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; + } + + /// + /// Indicates someone who has played less than 30 games. + /// + public class Provisional : FideKFactor + { + public Provisional() + { + } + + public override double GetValueForRating(double rating) + { + return 25; + } + } + } +} diff --git a/Skills/Elo/GaussianEloCalculator.cs b/Skills/Elo/GaussianEloCalculator.cs new file mode 100644 index 0000000..9e9a012 --- /dev/null +++ b/Skills/Elo/GaussianEloCalculator.cs @@ -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)); + } + } +} diff --git a/Skills/Elo/GaussianKFactor.cs b/Skills/Elo/GaussianKFactor.cs new file mode 100644 index 0000000..f2fa62a --- /dev/null +++ b/Skills/Elo/GaussianKFactor.cs @@ -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)) + { + } + } +} diff --git a/Skills/Elo/KFactor.cs b/Skills/Elo/KFactor.cs new file mode 100644 index 0000000..c8be739 --- /dev/null +++ b/Skills/Elo/KFactor.cs @@ -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; + } + } +} diff --git a/Skills/Elo/TwoPlayerEloCalculator.cs b/Skills/Elo/TwoPlayerEloCalculator.cs new file mode 100644 index 0000000..943db47 --- /dev/null +++ b/Skills/Elo/TwoPlayerEloCalculator.cs @@ -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 CalculateNewRatings(GameInfo gameInfo, IEnumerable> teams, params int[] teamRanks) + { + ValidateTeamCountAndPlayersCountPerTeam(teams); + RankSorter.Sort(ref teams, ref teamRanks); + + var result = new Dictionary(); + 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(GameInfo gameInfo, IEnumerable> 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; + } + } +} diff --git a/Skills/FactorGraphs/Factor.cs b/Skills/FactorGraphs/Factor.cs new file mode 100644 index 0000000..6e7caf2 --- /dev/null +++ b/Skills/FactorGraphs/Factor.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Moserware.Skills.FactorGraphs +{ + public abstract class Factor + { + private readonly List> _Messages = new List>(); + + private readonly Dictionary, Variable> _MessageToVariableBinding = + new Dictionary, Variable>(); + + private readonly string _Name; + private readonly List> _Variables = new List>(); + + 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> Variables + { + get { return _Variables.AsReadOnly(); } + } + + protected ReadOnlyCollection> 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 message, Variable 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 message = _Messages[messageIndex]; + Variable variable = _MessageToVariableBinding[message]; + return SendMessage(message, variable); + } + + protected abstract double SendMessage(Message message, Variable variable); + + public abstract Message CreateVariableToMessageBinding(Variable variable); + + protected Message CreateVariableToMessageBinding(Variable variable, Message message) + { + int index = _Messages.Count; + _Messages.Add(message); + _MessageToVariableBinding[message] = variable; + _Variables.Add(variable); + + return message; + } + + public override string ToString() + { + return _Name ?? base.ToString(); + } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/FactorGraph.cs b/Skills/FactorGraphs/FactorGraph.cs new file mode 100644 index 0000000..a7e93c5 --- /dev/null +++ b/Skills/FactorGraphs/FactorGraph.cs @@ -0,0 +1,9 @@ +namespace Moserware.Skills.FactorGraphs +{ + public class FactorGraph + where TSelf : FactorGraph + where TVariable : Variable + { + public VariableFactory VariableFactory { get; protected set; } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/FactorGraphLayer.cs b/Skills/FactorGraphs/FactorGraphLayer.cs new file mode 100644 index 0000000..fd76b5b --- /dev/null +++ b/Skills/FactorGraphs/FactorGraphLayer.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Moserware.Skills.FactorGraphs +{ + public abstract class FactorGraphLayerBase + { + public abstract IEnumerable> UntypedFactors { get; } + public abstract void BuildLayer(); + + public virtual Schedule CreatePriorSchedule() + { + return null; + } + + public virtual Schedule CreatePosteriorSchedule() + { + return null; + } + + // HACK + + public abstract void SetRawInputVariablesGroups(object value); + public abstract object GetRawOutputVariablesGroups(); + } + + public abstract class FactorGraphLayer + : FactorGraphLayerBase + where TParentGraph : FactorGraph + where TBaseVariable : Variable + where TInputVariable : TBaseVariable + where TFactor : Factor + where TOutputVariable : TBaseVariable + { + private readonly List _LocalFactors = new List(); + private readonly List> _OutputVariablesGroups = new List>(); + private IList> _InputVariablesGroups = new List>(); + + protected FactorGraphLayer(TParentGraph parentGraph) + { + ParentFactorGraph = parentGraph; + } + + protected IList> InputVariablesGroups + { + get { return _InputVariablesGroups; } + } + + // HACK + + public TParentGraph ParentFactorGraph { get; private set; } + + public IList> OutputVariablesGroups + { + get { return _OutputVariablesGroups; } + } + + public IList LocalFactors + { + get { return _LocalFactors; } + } + + public override IEnumerable> UntypedFactors + { + get { return _LocalFactors.Cast>(); } + } + + public override void SetRawInputVariablesGroups(object value) + { + var newList = value as IList>; + if (newList == null) + { + // TODO: message + throw new ArgumentException(); + } + + _InputVariablesGroups = newList; + } + + public override object GetRawOutputVariablesGroups() + { + return _OutputVariablesGroups; + } + + protected Schedule ScheduleSequence( + IEnumerable itemsToSequence, + string nameFormat, + params object[] args) + where TSchedule : Schedule + + { + string formattedName = String.Format(nameFormat, args); + return new ScheduleSequence(formattedName, itemsToSequence); + } + + protected void AddLayerFactor(TFactor factor) + { + _LocalFactors.Add(factor); + } + + // Helper utility + protected double Square(double x) + { + return x*x; + } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/FactorList.cs b/Skills/FactorGraphs/FactorList.cs new file mode 100644 index 0000000..07540f7 --- /dev/null +++ b/Skills/FactorGraphs/FactorList.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Moserware.Skills.FactorGraphs +{ + /// + /// Helper class for computing the factor graph's normalization constant. + /// + public class FactorList + { + private readonly List> _List = new List>(); + + public double LogNormalization + { + get + { + _List.ForEach(f => f.ResetMarginals()); + + double sumLogZ = 0.0; + + for (int i = 0; i < _List.Count; i++) + { + Factor 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 AddFactor(Factor factor) + { + _List.Add(factor); + return factor; + } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/Message.cs b/Skills/FactorGraphs/Message.cs new file mode 100644 index 0000000..85f8c90 --- /dev/null +++ b/Skills/FactorGraphs/Message.cs @@ -0,0 +1,30 @@ +using System; + +namespace Moserware.Skills.FactorGraphs +{ + public class Message + { + 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); + } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/Schedule.cs b/Skills/FactorGraphs/Schedule.cs new file mode 100644 index 0000000..7e43f09 --- /dev/null +++ b/Skills/FactorGraphs/Schedule.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; + +namespace Moserware.Skills.FactorGraphs +{ + public abstract class Schedule + { + 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 : Schedule + { + private readonly Factor _Factor; + private readonly int _Index; + + public ScheduleStep(string name, Factor factor, int index) + : base(name) + { + _Factor = factor; + _Index = index; + } + + public override double Visit(int depth, int maxDepth) + { + double delta = _Factor.UpdateMessage(_Index); + return delta; + } + } + + public class ScheduleSequence : ScheduleSequence> + { + public ScheduleSequence(string name, IEnumerable> schedules) + : base(name, schedules) + { + } + } + + public class ScheduleSequence : Schedule + where TSchedule : Schedule + { + private readonly IEnumerable _Schedules; + + public ScheduleSequence(string name, IEnumerable 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 : Schedule + { + private readonly double _MaxDelta; + private readonly Schedule _ScheduleToLoop; + + public ScheduleLoop(string name, Schedule 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; + } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/Variable.cs b/Skills/FactorGraphs/Variable.cs new file mode 100644 index 0000000..80a3bc7 --- /dev/null +++ b/Skills/FactorGraphs/Variable.cs @@ -0,0 +1,54 @@ +using System; + +namespace Moserware.Skills.FactorGraphs +{ + public class Variable + { + private readonly string _Name; + private readonly TValue _Prior; + + public Variable(string name, TValue prior) + { + _Name = "Variable[" + name + "]"; + _Prior = prior; + ResetToPrior(); + } + + public virtual TValue Value { get; set; } + + public void ResetToPrior() + { + Value = _Prior; + } + + public override string ToString() + { + return _Name; + } + } + + public class DefaultVariable : Variable + { + public DefaultVariable() + : base("Default", default(TValue)) + { + } + + public override TValue Value + { + get { return default(TValue); } + set { throw new NotSupportedException(); } + } + } + + public class KeyedVariable : Variable + { + public KeyedVariable(TKey key, string name, TValue prior) + : base(name, prior) + { + Key = key; + } + + public TKey Key { get; private set; } + } +} \ No newline at end of file diff --git a/Skills/FactorGraphs/VariableFactory.cs b/Skills/FactorGraphs/VariableFactory.cs new file mode 100644 index 0000000..d07b5ff --- /dev/null +++ b/Skills/FactorGraphs/VariableFactory.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace Moserware.Skills.FactorGraphs +{ + public class VariableFactory + { + // using a Func to encourage fresh copies in case it's overwritten + private readonly Func _VariablePriorInitializer; + + public VariableFactory(Func variablePriorInitializer) + { + _VariablePriorInitializer = variablePriorInitializer; + } + + public Variable CreateBasicVariable(string nameFormat, params object[] args) + { + var newVar = new Variable( + String.Format(nameFormat, args), + _VariablePriorInitializer()); + + return newVar; + } + + public KeyedVariable CreateKeyedVariable(TKey key, string nameFormat, params object[] args) + { + var newVar = new KeyedVariable( + key, + String.Format(nameFormat, args), + _VariablePriorInitializer()); + + return newVar; + } + } +} \ No newline at end of file diff --git a/Skills/GameInfo.cs b/Skills/GameInfo.cs new file mode 100644 index 0000000..cfdddc4 --- /dev/null +++ b/Skills/GameInfo.cs @@ -0,0 +1,49 @@ +namespace Moserware.Skills +{ + /// + /// Parameters about the game for calculating the TrueSkill. + /// + 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); + } + } + } +} \ No newline at end of file diff --git a/Skills/Guard.cs b/Skills/Guard.cs new file mode 100644 index 0000000..deda637 --- /dev/null +++ b/Skills/Guard.cs @@ -0,0 +1,36 @@ +using System; + +namespace Moserware.Skills +{ + /// + /// Verifies argument contracts. + /// + /// 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 + 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); + } + } + } +} \ No newline at end of file diff --git a/Skills/ISupportPartialPlay.cs b/Skills/ISupportPartialPlay.cs new file mode 100644 index 0000000..9b781a6 --- /dev/null +++ b/Skills/ISupportPartialPlay.cs @@ -0,0 +1,13 @@ +namespace Moserware.Skills +{ + /// + /// Indicates support for allowing partial play (where a player only plays a part of the time). + /// + public interface ISupportPartialPlay + { + /// + /// 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. + /// + double PartialPlayPercentage { get; } + } +} \ No newline at end of file diff --git a/Skills/ISupportPartialUpdate.cs b/Skills/ISupportPartialUpdate.cs new file mode 100644 index 0000000..f202c61 --- /dev/null +++ b/Skills/ISupportPartialUpdate.cs @@ -0,0 +1,10 @@ +namespace Moserware.Skills +{ + public interface ISupportPartialUpdate + { + /// + /// 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. + /// + double PartialUpdatePercentage { get; } + } +} \ No newline at end of file diff --git a/Skills/License.txt b/Skills/License.txt new file mode 100644 index 0000000..f6c1a69 --- /dev/null +++ b/Skills/License.txt @@ -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 + +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. \ No newline at end of file diff --git a/Skills/Numerics/GaussianDistribution.cs b/Skills/Numerics/GaussianDistribution.cs new file mode 100644 index 0000000..d303b1b --- /dev/null +++ b/Skills/Numerics/GaussianDistribution.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/Skills/Numerics/Matrix.cs b/Skills/Numerics/Matrix.cs new file mode 100644 index 0000000..feadcb9 --- /dev/null +++ b/Skills/Numerics/Matrix.cs @@ -0,0 +1,524 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Moserware.Numerics +{ + /// + /// Represents an MxN matrix with double precision values. + /// + internal class Matrix + { + // Anything smaller than this will be assumed to be rounding error in terms of equality matching + private const int FractionalDigitsToRoundTo = 10; + private static readonly double ErrorTolerance = Math.Pow(0.1, FractionalDigitsToRoundTo); // e.g. 1/10^10 + + 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> 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; + } + + 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++) + { + double cellValue = _MatrixRowValues[currentRow][currentColumn]; + double roundedValue = Math.Round(cellValue, FractionalDigitsToRoundTo); + result += multiplier*roundedValue; + } + } + } + + // 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 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 vectorValues) + : base(vectorValues.Count, 1, new IEnumerable[] {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; + } + } +} \ No newline at end of file diff --git a/Skills/Numerics/Range.cs b/Skills/Numerics/Range.cs new file mode 100644 index 0000000..7d5b581 --- /dev/null +++ b/Skills/Numerics/Range.cs @@ -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 where T : Range, 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); + } + } +} \ No newline at end of file diff --git a/Skills/PairwiseComparison.cs b/Skills/PairwiseComparison.cs new file mode 100644 index 0000000..7848eb3 --- /dev/null +++ b/Skills/PairwiseComparison.cs @@ -0,0 +1,15 @@ +namespace Moserware.Skills +{ + /// + /// Represents a comparison between two players. + /// + /// + /// The actual values for the enum were chosen so that the also correspond to the multiplier for updates to means. + /// + public enum PairwiseComparison + { + Win = 1, + Draw = 0, + Lose = -1 + } +} \ No newline at end of file diff --git a/Skills/PartialPlay.cs b/Skills/PartialPlay.cs new file mode 100644 index 0000000..b24d08f --- /dev/null +++ b/Skills/PartialPlay.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Skills/Player.cs b/Skills/Player.cs new file mode 100644 index 0000000..74bcad6 --- /dev/null +++ b/Skills/Player.cs @@ -0,0 +1,129 @@ +namespace Moserware.Skills +{ + /// + /// Represents a player who has a . + /// + public class Player : 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; + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + public Player(T id) + : this(id, DefaultPartialPlayPercentage, DefaultPartialUpdatePercentage) + { + } + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + /// The weight percentage to give this player when calculating a new rank. + public Player(T id, double partialPlayPercentage) + : this(id, partialPlayPercentage, DefaultPartialUpdatePercentage) + { + } + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + /// The weight percentage to give this player when calculating a new rank. + /// /// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update. + 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; + } + + /// + /// The identifier for the player, such as a name. + /// + public T Id + { + get { return _Id; } + } + + #region ISupportPartialPlay Members + + /// + /// 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. + /// + public double PartialPlayPercentage + { + get { return _PartialPlayPercentage; } + } + + #endregion + + #region ISupportPartialUpdate Members + + /// + /// 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. + /// + public double PartialUpdatePercentage + { + get { return _PartialUpdatePercentage; } + } + + #endregion + + public override string ToString() + { + if (Id != null) + { + return Id.ToString(); + } + + return base.ToString(); + } + } + + /// + /// Represents a player who has a . + /// + public class Player : Player + { + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + public Player(object id) + : base(id) + { + } + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + /// The weight percentage to give this player when calculating a new rank. + /// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update. + public Player(object id, double partialPlayPercentage) + : base(id, partialPlayPercentage) + { + } + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + /// The weight percentage to give this player when calculating a new rank. + /// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update. + public Player(object id, double partialPlayPercentage, double partialUpdatePercentage) + : base(id, partialPlayPercentage, partialUpdatePercentage) + { + } + } +} \ No newline at end of file diff --git a/Skills/PlayersRange.cs b/Skills/PlayersRange.cs new file mode 100644 index 0000000..b3c2963 --- /dev/null +++ b/Skills/PlayersRange.cs @@ -0,0 +1,22 @@ +using Moserware.Skills.Numerics; + +namespace Moserware.Skills +{ + public class PlayersRange : Range + { + 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); + } + } +} \ No newline at end of file diff --git a/Skills/Properties/AssemblyInfo.cs b/Skills/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5f3b598 --- /dev/null +++ b/Skills/Properties/AssemblyInfo.cs @@ -0,0 +1,41 @@ +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")] +[assembly: InternalsVisibleTo("UnitTests")] diff --git a/Skills/README.txt b/Skills/README.txt new file mode 100644 index 0000000..fcc9b11 --- /dev/null +++ b/Skills/README.txt @@ -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 \ No newline at end of file diff --git a/Skills/RankSorter.cs b/Skills/RankSorter.cs new file mode 100644 index 0000000..c3dc1f6 --- /dev/null +++ b/Skills/RankSorter.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Moserware.Skills +{ + /// + /// Helper class to sort ranks in non-decreasing order. + /// + internal static class RankSorter + { + /// + /// Performs an in-place sort of the in according to the in non-decreasing order. + /// + /// The types of items to sort. + /// The items to sort according to the order specified by . + /// The ranks for each item where 1 is first place. + public static void Sort(ref IEnumerable 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 itemsInList = teams.ToList(); + + // item -> rank + var itemToRank = new Dictionary(); + + 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; + } + } +} \ No newline at end of file diff --git a/Skills/Rating.cs b/Skills/Rating.cs new file mode 100644 index 0000000..429f51f --- /dev/null +++ b/Skills/Rating.cs @@ -0,0 +1,94 @@ +using System; +using Moserware.Numerics; + +namespace Moserware.Skills +{ + /// + /// Container for a player's rating. + /// + public class Rating + { + private const int ConservativeStandardDeviationMultiplier = 3; + private readonly double _ConservativeStandardDeviationMultiplier; + private readonly double _Mean; + private readonly double _StandardDeviation; + + /// + /// Constructs a rating. + /// + /// The statistical mean value of the rating (also known as μ). + /// The standard deviation of the rating (also known as σ). + public Rating(double mean, double standardDeviation) + : this(mean, standardDeviation, ConservativeStandardDeviationMultiplier) + { + } + + /// + /// Constructs a rating. + /// + /// The statistical mean value of the rating (also known as μ). + /// The standard deviation (the spread) of the rating (also known as σ). + /// The number of s to subtract from the to achieve a conservative rating. + public Rating(double mean, double standardDeviation, double conservativeStandardDeviationMultiplier) + { + _Mean = mean; + _StandardDeviation = standardDeviation; + _ConservativeStandardDeviationMultiplier = conservativeStandardDeviationMultiplier; + } + + /// + /// The statistical mean value of the rating (also known as μ). + /// + public double Mean + { + get { return _Mean; } + } + + /// + /// The standard deviation (the spread) of the rating. This is also known as σ. + /// + public double StandardDeviation + { + get { return _StandardDeviation; } + } + + /// + /// A conservative estimate of skill based on the mean and standard deviation. + /// + 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); + } + } +} \ No newline at end of file diff --git a/Skills/SkillCalculator.cs b/Skills/SkillCalculator.cs new file mode 100644 index 0000000..6f70b6e --- /dev/null +++ b/Skills/SkillCalculator.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; + +namespace Moserware.Skills +{ + /// + /// Base class for all skill calculator implementations. + /// + 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; + } + + /// + /// Calculates new ratings based on the prior ratings and team ranks. + /// + /// The underlying type of the player. + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2) + /// All the players and their new ratings. + public abstract IDictionary CalculateNewRatings(GameInfo gameInfo, + IEnumerable + > + teams, + params int[] teamRanks); + + /// + /// Calculates the match quality as the likelihood of all teams drawing. + /// + /// The underlying type of the player. + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The quality of the match between the teams as a percentage (0% = bad, 100% = well matched). + public abstract double CalculateMatchQuality(GameInfo gameInfo, + IEnumerable> teams); + + public bool IsSupported(SupportedOptions option) + { + return (_SupportedOptions & option) == option; + } + + /// + /// Helper function to square the . + /// + /// * + protected static double Square(double value) + { + return value*value; + } + + protected void ValidateTeamCountAndPlayersCountPerTeam(IEnumerable> teams) + { + ValidateTeamCountAndPlayersCountPerTeam(teams, _TotalTeamsAllowed, _PlayersPerTeamAllowed); + } + + private static void ValidateTeamCountAndPlayersCountPerTeam( + IEnumerable> 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(); + } + } + } +} \ No newline at end of file diff --git a/Skills/Skills.csproj b/Skills/Skills.csproj new file mode 100644 index 0000000..4290fde --- /dev/null +++ b/Skills/Skills.csproj @@ -0,0 +1,117 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {15AD1345-984C-48ED-AF9A-2EAB44E5AA2B} + Library + Properties + Moserware.Skills + Moserware.Skills + v3.5 + 512 + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Skills/Skills.suo b/Skills/Skills.suo new file mode 100644 index 0000000..2d273c6 Binary files /dev/null and b/Skills/Skills.suo differ diff --git a/Skills/Team.cs b/Skills/Team.cs new file mode 100644 index 0000000..85cfad6 --- /dev/null +++ b/Skills/Team.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Moserware.Skills +{ + /// + /// Helper class for working with a single team. + /// + public class Team + { + private readonly Dictionary _PlayerRatings = new Dictionary(); + + /// + /// Constructs a new team. + /// + public Team() + { + } + + /// + /// Constructs a and populates it with the specified . + /// + /// The player to add. + /// The rating of the . + public Team(TPlayer player, Rating rating) + { + AddPlayer(player, rating); + } + + /// + /// Adds the to the team. + /// + /// The player to add. + /// The rating of the . + /// The instance of the team (for chaining convenience). + public Team AddPlayer(TPlayer player, Rating rating) + { + _PlayerRatings[player] = rating; + return this; + } + + /// + /// Returns the as a simple dictionary. + /// + /// The as a simple dictionary. + public IDictionary AsDictionary() + { + return _PlayerRatings; + } + } + + /// + /// Helper class for working with a single team. + /// + public class Team : Team + { + /// + /// Constructs a new team. + /// + public Team() + { + } + + /// + /// Constructs a and populates it with the specified . + /// + /// The player to add. + /// The rating of the . + public Team(Player player, Rating rating) + : base(player, rating) + { + } + } + + /// + /// Helper class for working with multiple teams. + /// + public static class Teams + { + /// + /// Concatenates multiple teams into a list of teams. + /// + /// The teams to concatenate together. + /// A sequence of teams. + public static IEnumerable> Concat(params Team[] teams) + { + return teams.Select(t => t.AsDictionary()); + } + } +} \ No newline at end of file diff --git a/Skills/TeamsRange.cs b/Skills/TeamsRange.cs new file mode 100644 index 0000000..22c40e7 --- /dev/null +++ b/Skills/TeamsRange.cs @@ -0,0 +1,22 @@ +using Moserware.Skills.Numerics; + +namespace Moserware.Skills +{ + public class TeamsRange : Range + { + 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); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/DrawMargin.cs b/Skills/TrueSkill/DrawMargin.cs new file mode 100644 index 0000000..25335d5 --- /dev/null +++ b/Skills/TrueSkill/DrawMargin.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs b/Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs new file mode 100644 index 0000000..8a291a0 --- /dev/null +++ b/Skills/TrueSkill/FactorGraphTrueSkillCalculator.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moserware.Numerics; + +namespace Moserware.Skills.TrueSkill +{ + /// + /// Calculates TrueSkill using a full factor graph. + /// + internal class FactorGraphTrueSkillCalculator : SkillCalculator + { + public FactorGraphTrueSkillCalculator() + : base(SupportedOptions.PartialPlay | SupportedOptions.PartialUpdate, TeamsRange.AtLeast(2), PlayersRange.AtLeast(1)) + { + } + + public override IDictionary CalculateNewRatings(GameInfo gameInfo, + IEnumerable> teams, + params int[] teamRanks) + { + Guard.ArgumentNotNull(gameInfo, "gameInfo"); + ValidateTeamCountAndPlayersCountPerTeam(teams); + + RankSorter.Sort(ref teams, ref teamRanks); + + var factorGraph = new TrueSkillFactorGraph(gameInfo, teams, teamRanks); + factorGraph.BuildGraph(); + factorGraph.RunSchedule(); + + double probabilityOfOutcome = factorGraph.GetProbabilityOfRanking(); + + return factorGraph.GetUpdatedRatings(); + } + + + public override double CalculateMatchQuality(GameInfo gameInfo, + IEnumerable> teams) + { + // We need to create the A matrix which is the player team assigments. + List> 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( + IEnumerable> teamAssignmentsList) + { + // A simple vector of all the player means. + return new Vector(GetPlayerRatingValues(teamAssignmentsList, rating => rating.Mean)); + } + + private static Matrix GetPlayerCovarianceMatrix( + IEnumerable> 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 GetPlayerRatingValues( + IEnumerable> teamAssignmentsList, Func playerRatingFunction) + { + var playerRatingValues = new List(); + + foreach (var currentTeam in teamAssignmentsList) + { + foreach (Rating currentRating in currentTeam.Values) + { + playerRatingValues.Add(playerRatingFunction(currentRating)); + } + } + + return playerRatingValues; + } + + private static Matrix CreatePlayerTeamAssignmentMatrix( + IList> 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>(); + int totalPreviousPlayers = 0; + + for (int i = 0; i < teamAssignmentsList.Count - 1; i++) + { + IDictionary 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(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 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianFactor.cs b/Skills/TrueSkill/Factors/GaussianFactor.cs new file mode 100644 index 0000000..3417627 --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianFactor.cs @@ -0,0 +1,33 @@ +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Factors +{ + public abstract class GaussianFactor : Factor + { + protected GaussianFactor(string name) + : base(name) + { + } + + /// Sends the factor-graph message with and returns the log-normalization constant + protected override double SendMessage(Message message, + Variable variable) + { + GaussianDistribution marginal = variable.Value; + GaussianDistribution messageValue = message.Value; + double logZ = GaussianDistribution.LogProductNormalization(marginal, messageValue); + variable.Value = marginal*messageValue; + return logZ; + } + + public override Message CreateVariableToMessageBinding( + Variable variable) + { + return CreateVariableToMessageBinding(variable, + new Message( + GaussianDistribution.FromPrecisionMean(0, 0), + "message from {0} to {1}", this, variable)); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs b/Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs new file mode 100644 index 0000000..31c4d4a --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianGreaterThanFactor.cs @@ -0,0 +1,75 @@ +using System; +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Factors +{ + /// + /// Factor representing a team difference that has exceeded the draw margin. + /// + /// See the accompanying math paper for more details. + public class GaussianGreaterThanFactor : GaussianFactor + { + private readonly double _Epsilon; + + public GaussianGreaterThanFactor(double epsilon, Variable 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 message, + Variable 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs b/Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs new file mode 100644 index 0000000..95d4a6d --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianLikelihoodFactor.cs @@ -0,0 +1,72 @@ +using System; +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Factors +{ + /// + /// Connects two variables and adds uncertainty. + /// + /// See the accompanying math paper for more details. + public class GaussianLikelihoodFactor : GaussianFactor + { + private readonly double _Precision; + + public GaussianLikelihoodFactor(double betaSquared, Variable variable1, + Variable 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 message1, Message message2, + Variable variable1, Variable 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(); + } + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianPriorFactor.cs b/Skills/TrueSkill/Factors/GaussianPriorFactor.cs new file mode 100644 index 0000000..3921880 --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianPriorFactor.cs @@ -0,0 +1,39 @@ +using System; +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Factors +{ + /// + /// Supplies the factor graph with prior information. + /// + /// See the accompanying math paper for more details. + public class GaussianPriorFactor : GaussianFactor + { + private readonly GaussianDistribution _NewMessage; + + public GaussianPriorFactor(double mean, double variance, Variable variable) + : base(String.Format("Prior value going to {0}", variable)) + { + _NewMessage = new GaussianDistribution(mean, Math.Sqrt(variance)); + CreateVariableToMessageBinding(variable, + new Message( + GaussianDistribution.FromPrecisionMean(0, 0), "message from {0} to {1}", + this, variable)); + } + + protected override double UpdateMessage(Message message, + Variable variable) + { + GaussianDistribution oldMarginal = variable.Value.Clone(); + Message 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs b/Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs new file mode 100644 index 0000000..d836574 --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianWeightedSumFactor.cs @@ -0,0 +1,251 @@ +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 +{ + /// + /// Factor that sums together multiple Gaussians. + /// + /// See the accompanying math paper for more details. + public class GaussianWeightedSumFactor : GaussianFactor + { + private readonly List _VariableIndexOrdersForWeights = new List(); + + // 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 sumVariable, + Variable[] variablesToSum) + : this(sumVariable, + variablesToSum, + variablesToSum.Select(v => 1.0).ToArray()) // By default, set the weight to 1.0 + { + } + + public GaussianWeightedSumFactor(Variable sumVariable, + Variable[] 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++) + { + 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> vars = Variables; + ReadOnlyCollection> 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> messages, + IList> 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> allMessages = Messages; + ReadOnlyCollection> allVariables = Variables; + + Guard.ArgumentIsValidIndex(messageIndex, allMessages.Count, "messageIndex"); + + var updatedMessages = new List>(); + var updatedVariables = new List>(); + + 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 sumVariable, + IList> 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(); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Factors/GaussianWithinFactor.cs b/Skills/TrueSkill/Factors/GaussianWithinFactor.cs new file mode 100644 index 0000000..71113b7 --- /dev/null +++ b/Skills/TrueSkill/Factors/GaussianWithinFactor.cs @@ -0,0 +1,73 @@ +using System; +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Factors +{ + /// + /// Factor representing a team difference that has not exceeded the draw margin. + /// + /// See the accompanying math paper for more details. + public class GaussianWithinFactor : GaussianFactor + { + private readonly double _Epsilon; + + public GaussianWithinFactor(double epsilon, Variable 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 message, + Variable 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs b/Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs new file mode 100644 index 0000000..94da9ba --- /dev/null +++ b/Skills/TrueSkill/Layers/IteratedTeamDifferencesInnerLayer.cs @@ -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 : + TrueSkillFactorGraphLayer + , GaussianWeightedSumFactor, Variable> + { + private readonly TeamDifferencesComparisonLayer _TeamDifferencesComparisonLayer; + + private readonly TeamPerformancesToTeamPerformanceDifferencesLayer + _TeamPerformancesToTeamPerformanceDifferencesLayer; + + public IteratedTeamDifferencesInnerLayer(TrueSkillFactorGraph parentGraph, + TeamPerformancesToTeamPerformanceDifferencesLayer + teamPerformancesToPerformanceDifferences, + TeamDifferencesComparisonLayer teamDifferencesComparisonLayer) + : base(parentGraph) + { + _TeamPerformancesToTeamPerformanceDifferencesLayer = teamPerformancesToPerformanceDifferences; + _TeamDifferencesComparisonLayer = teamDifferencesComparisonLayer; + } + + public override IEnumerable> 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 CreatePriorSchedule() + { + Schedule 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( + "inner schedule", + new[] + { + loop, + new ScheduleStep( + "teamPerformanceToPerformanceDifferenceFactors[0] @ 1", + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0], 1), + new ScheduleStep( + String.Format("teamPerformanceToPerformanceDifferenceFactors[teamTeamDifferences = {0} - 1] @ 2", + totalTeamDifferences), + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[totalTeamDifferences - 1], 2) + } + ); + + return innerSchedule; + } + + private Schedule CreateTwoTeamInnerPriorLoopSchedule() + { + return ScheduleSequence( + new[] + { + new ScheduleStep( + "send team perf to perf differences", + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[0], + 0), + new ScheduleStep( + "send to greater than or within factor", + _TeamDifferencesComparisonLayer.LocalFactors[0], + 0) + }, + "loop of just two teams inner sequence"); + } + + private Schedule CreateMultipleTeamInnerPriorLoopSchedule() + { + int totalTeamDifferences = _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors.Count; + + var forwardScheduleList = new List>(); + + for (int i = 0; i < totalTeamDifferences - 1; i++) + { + Schedule currentForwardSchedulePiece = + ScheduleSequence( + new Schedule[] + { + new ScheduleStep( + String.Format("team perf to perf diff {0}", + i), + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[i], 0), + new ScheduleStep( + String.Format("greater than or within result factor {0}", + i), + _TeamDifferencesComparisonLayer.LocalFactors[i], + 0), + new ScheduleStep( + 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( + "forward schedule", + forwardScheduleList); + + var backwardScheduleList = new List>(); + + for (int i = 0; i < totalTeamDifferences - 1; i++) + { + var currentBackwardSchedulePiece = new ScheduleSequence( + "current backward schedule piece", + new Schedule[] + { + new ScheduleStep( + String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 0", + i), + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[ + totalTeamDifferences - 1 - i], 0), + new ScheduleStep( + String.Format("greaterThanOrWithinResultFactors[totalTeamDifferences - 1 - {0}] @ 0", + i), + _TeamDifferencesComparisonLayer.LocalFactors[totalTeamDifferences - 1 - i], 0), + new ScheduleStep( + String.Format("teamPerformanceToPerformanceDifferenceFactors[totalTeamDifferences - 1 - {0}] @ 1", + i), + _TeamPerformancesToTeamPerformanceDifferencesLayer.LocalFactors[ + totalTeamDifferences - 1 - i], 1) + } + ); + backwardScheduleList.Add(currentBackwardSchedulePiece); + } + + var backwardSchedule = + new ScheduleSequence( + "backward schedule", + backwardScheduleList); + + var forwardBackwardScheduleToLoop = + new ScheduleSequence( + "forward Backward Schedule To Loop", + new Schedule[] + { + forwardSchedule, backwardSchedule + }); + + const double initialMaxDelta = 0.0001; + + var loop = new ScheduleLoop( + String.Format("loop with max delta of {0}", + initialMaxDelta), + forwardBackwardScheduleToLoop, + initialMaxDelta); + + return loop; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.cs b/Skills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.cs new file mode 100644 index 0000000..2aae362 --- /dev/null +++ b/Skills/TrueSkill/Layers/PlayerPerformancesToTeamPerformancesLayer.cs @@ -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 : + TrueSkillFactorGraphLayer + , GaussianWeightedSumFactor, + Variable> + { + public PlayerPerformancesToTeamPerformancesLayer(TrueSkillFactorGraph parentGraph) + : base(parentGraph) + { + } + + public override void BuildLayer() + { + foreach (var currentTeam in InputVariablesGroups) + { + Variable teamPerformance = CreateOutputVariable(currentTeam); + AddLayerFactor(CreatePlayerToTeamSumFactor(currentTeam, teamPerformance)); + + // REVIEW: Does it make sense to have groups of one? + OutputVariablesGroups.Add(new[] {teamPerformance}); + } + } + + public override Schedule CreatePriorSchedule() + { + return ScheduleSequence( + from weightedSumFactor in LocalFactors + select new ScheduleStep("Perf to Team Perf Step", weightedSumFactor, 0), + "all player perf to team perf schedule"); + } + + protected GaussianWeightedSumFactor CreatePlayerToTeamSumFactor( + IList> teamMembers, Variable sumVariable) + { + return new GaussianWeightedSumFactor(sumVariable, teamMembers.ToArray(), + teamMembers.Select(v => PartialPlay.GetPartialPlayPercentage(v.Key)). + ToArray()); + } + + public override Schedule CreatePosteriorSchedule() + { + return ScheduleSequence(from currentFactor in LocalFactors + from currentIteration in + Enumerable.Range(1, currentFactor.NumberOfMessages - 1) + select new ScheduleStep( + "team sum perf @" + currentIteration, + currentFactor, + currentIteration), + "all of the team's sum iterations"); + } + + private Variable CreateOutputVariable( + IList> team) + { + string teamMemberNames = String.Join(", ", team.Select(teamMember => teamMember.Key.ToString()).ToArray()); + + return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team[{0}]'s performance", teamMemberNames); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs b/Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs new file mode 100644 index 0000000..cec4c27 --- /dev/null +++ b/Skills/TrueSkill/Layers/PlayerPriorValuesToSkillsLayer.cs @@ -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 : + TrueSkillFactorGraphLayer + , GaussianPriorFactor, + KeyedVariable> + { + private readonly IEnumerable> _Teams; + + public PlayerPriorValuesToSkillsLayer(TrueSkillFactorGraph parentGraph, + IEnumerable> teams) + : base(parentGraph) + { + _Teams = teams; + } + + public override void BuildLayer() + { + foreach (var currentTeam in _Teams) + { + var currentTeamSkills = new List>(); + + foreach (var currentTeamPlayer in currentTeam) + { + KeyedVariable playerSkill = + CreateSkillOutputVariable(currentTeamPlayer.Key); + AddLayerFactor(CreatePriorFactor(currentTeamPlayer.Key, currentTeamPlayer.Value, playerSkill)); + currentTeamSkills.Add(playerSkill); + } + + OutputVariablesGroups.Add(currentTeamSkills); + } + } + + public override Schedule CreatePriorSchedule() + { + return ScheduleSequence( + from prior in LocalFactors + select new ScheduleStep("Prior to Skill Step", prior, 0), + "All priors"); + } + + private GaussianPriorFactor CreatePriorFactor(TPlayer player, Rating priorRating, + Variable skillsVariable) + { + return new GaussianPriorFactor(priorRating.Mean, + Square(priorRating.StandardDeviation) + + Square(ParentFactorGraph.GameInfo.DynamicsFactor), skillsVariable); + } + + private KeyedVariable CreateSkillOutputVariable(TPlayer key) + { + return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s skill", key); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs b/Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs new file mode 100644 index 0000000..1899092 --- /dev/null +++ b/Skills/TrueSkill/Layers/PlayerSkillsToPerformancesLayer.cs @@ -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 : + TrueSkillFactorGraphLayer + , GaussianLikelihoodFactor, + KeyedVariable> + { + public PlayerSkillsToPerformancesLayer(TrueSkillFactorGraph parentGraph) + : base(parentGraph) + { + } + + public override void BuildLayer() + { + foreach (var currentTeam in InputVariablesGroups) + { + var currentTeamPlayerPerformances = new List>(); + + foreach (var playerSkillVariable in currentTeam) + { + KeyedVariable playerPerformance = + CreateOutputVariable(playerSkillVariable.Key); + AddLayerFactor(CreateLikelihood(playerSkillVariable, playerPerformance)); + currentTeamPlayerPerformances.Add(playerPerformance); + } + + OutputVariablesGroups.Add(currentTeamPlayerPerformances); + } + } + + private GaussianLikelihoodFactor CreateLikelihood(KeyedVariable playerSkill, + KeyedVariable playerPerformance) + { + return new GaussianLikelihoodFactor(Square(ParentFactorGraph.GameInfo.Beta), playerPerformance, playerSkill); + } + + private KeyedVariable CreateOutputVariable(TPlayer key) + { + return ParentFactorGraph.VariableFactory.CreateKeyedVariable(key, "{0}'s performance", key); + } + + public override Schedule CreatePriorSchedule() + { + return ScheduleSequence( + from likelihood in LocalFactors + select new ScheduleStep("Skill to Perf step", likelihood, 0), + "All skill to performance sending"); + } + + public override Schedule CreatePosteriorSchedule() + { + return ScheduleSequence( + from likelihood in LocalFactors + select new ScheduleStep("name", likelihood, 1), + "All skill to performance sending"); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs b/Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs new file mode 100644 index 0000000..748aec0 --- /dev/null +++ b/Skills/TrueSkill/Layers/TeamDifferencesComparisonLayer.cs @@ -0,0 +1,38 @@ +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; +using Moserware.Skills.TrueSkill.Factors; + +namespace Moserware.Skills.TrueSkill.Layers +{ + internal class TeamDifferencesComparisonLayer : + TrueSkillFactorGraphLayer + , GaussianFactor, DefaultVariable> + { + private readonly double _Epsilon; + private readonly int[] _TeamRanks; + + public TeamDifferencesComparisonLayer(TrueSkillFactorGraph 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 teamDifference = InputVariablesGroups[i][0]; + + GaussianFactor factor = + isDraw + ? (GaussianFactor) new GaussianWithinFactor(_Epsilon, teamDifference) + : new GaussianGreaterThanFactor(_Epsilon, teamDifference); + + AddLayerFactor(factor); + } + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.cs b/Skills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.cs new file mode 100644 index 0000000..cec409d --- /dev/null +++ b/Skills/TrueSkill/Layers/TeamPerformancesToTeamPerformanceDifferencesLayer.cs @@ -0,0 +1,43 @@ +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; +using Moserware.Skills.TrueSkill.Factors; + +namespace Moserware.Skills.TrueSkill.Layers +{ + internal class TeamPerformancesToTeamPerformanceDifferencesLayer : + TrueSkillFactorGraphLayer + , GaussianWeightedSumFactor, Variable> + { + public TeamPerformancesToTeamPerformanceDifferencesLayer(TrueSkillFactorGraph parentGraph) + : base(parentGraph) + { + } + + public override void BuildLayer() + { + for (int i = 0; i < InputVariablesGroups.Count - 1; i++) + { + Variable strongerTeam = InputVariablesGroups[i][0]; + Variable weakerTeam = InputVariablesGroups[i + 1][0]; + + Variable 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 strongerTeam, Variable weakerTeam, + Variable output) + { + return new GaussianWeightedSumFactor(output, new[] {strongerTeam, weakerTeam}, new[] {1.0, -1.0}); + } + + private Variable CreateOutputVariable() + { + return ParentFactorGraph.VariableFactory.CreateBasicVariable("Team performance difference"); + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs b/Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs new file mode 100644 index 0000000..fedb182 --- /dev/null +++ b/Skills/TrueSkill/Layers/TrueSkillFactorGraphLayer.cs @@ -0,0 +1,20 @@ +using Moserware.Numerics; +using Moserware.Skills.FactorGraphs; + +namespace Moserware.Skills.TrueSkill.Layers +{ + internal abstract class TrueSkillFactorGraphLayer + : + FactorGraphLayer + , GaussianDistribution, Variable, TInputVariable, + TFactor, TOutputVariable> + where TInputVariable : Variable + where TFactor : Factor + where TOutputVariable : Variable + { + public TrueSkillFactorGraphLayer(TrueSkillFactorGraph parentGraph) + : base(parentGraph) + { + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/TrueSkillFactorGraph.cs b/Skills/TrueSkill/TrueSkillFactorGraph.cs new file mode 100644 index 0000000..ffe5d0a --- /dev/null +++ b/Skills/TrueSkill/TrueSkillFactorGraph.cs @@ -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 : + FactorGraph, GaussianDistribution, Variable> + { + private readonly List> _Layers; + private readonly PlayerPriorValuesToSkillsLayer _PriorLayer; + + public TrueSkillFactorGraph(GameInfo gameInfo, IEnumerable> teams, int[] teamRanks) + { + _PriorLayer = new PlayerPriorValuesToSkillsLayer(this, teams); + GameInfo = gameInfo; + VariableFactory = + new VariableFactory(() => GaussianDistribution.FromPrecisionMean(0, 0)); + + _Layers = new List> + { + _PriorLayer, + new PlayerSkillsToPerformancesLayer(this), + new PlayerPerformancesToTeamPerformancesLayer(this), + new IteratedTeamDifferencesInnerLayer( + this, + new TeamPerformancesToTeamPerformanceDifferencesLayer(this), + new TeamDifferencesComparisonLayer(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 fullSchedule = CreateFullSchedule(); + double fullScheduleDelta = fullSchedule.Visit(); + } + + public double GetProbabilityOfRanking() + { + var factorList = new FactorList(); + + foreach (var currentLayer in _Layers) + { + foreach (var currentFactor in currentLayer.UntypedFactors) + { + factorList.AddFactor(currentFactor); + } + } + + double logZ = factorList.LogNormalization; + return Math.Exp(logZ); + } + + private Schedule CreateFullSchedule() + { + var fullSchedule = new List>(); + + foreach (var currentLayer in _Layers) + { + Schedule currentPriorSchedule = currentLayer.CreatePriorSchedule(); + if (currentPriorSchedule != null) + { + fullSchedule.Add(currentPriorSchedule); + } + } + + // Casting to IEnumerable to get the LINQ Reverse() + IEnumerable> allLayers = _Layers; + + foreach (var currentLayer in allLayers.Reverse()) + { + Schedule currentPosteriorSchedule = currentLayer.CreatePosteriorSchedule(); + if (currentPosteriorSchedule != null) + { + fullSchedule.Add(currentPosteriorSchedule); + } + } + + return new ScheduleSequence("Full schedule", fullSchedule); + } + + public IDictionary GetUpdatedRatings() + { + var result = new Dictionary(); + foreach (var currentTeam in _PriorLayer.OutputVariablesGroups) + { + foreach (var currentPlayer in currentTeam) + { + result[currentPlayer.Key] = new Rating(currentPlayer.Value.Mean, + currentPlayer.Value.StandardDeviation); + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs b/Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs new file mode 100644 index 0000000..252ee6a --- /dev/null +++ b/Skills/TrueSkill/TruncatedGaussianCorrectionFunctions.cs @@ -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. + + /// + /// The "V" function where the team performance difference is greater than the draw margin. + /// + /// In the reference F# implementation, this is referred to as "the additive + /// correction of a single-sided truncated Gaussian with unit variance." + /// + /// In the paper, it's referred to as just "ε". + /// + 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; + } + + /// + /// The "W" function where the team performance difference is greater than the draw margin. + /// + /// In the reference F# implementation, this is referred to as "the multiplicative + /// correction of a single-sided truncated Gaussian with unit variance." + /// + /// + /// + /// + 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs b/Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs new file mode 100644 index 0000000..e494474 --- /dev/null +++ b/Skills/TrueSkill/TwoPlayerTrueSkillCalculator.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moserware.Skills.Numerics; + +namespace Moserware.Skills.TrueSkill +{ + /// + /// Calculates the new ratings for only two players. + /// + /// + /// 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. + /// + public class TwoPlayerTrueSkillCalculator : SkillCalculator + { + public TwoPlayerTrueSkillCalculator() + : base(SupportedOptions.None, Range.Exactly(2), Range.Exactly(1)) + { + } + + /// + public override IDictionary CalculateNewRatings(GameInfo gameInfo, + IEnumerable + > + 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> teamList = teams.ToList(); + + // Since we verified that each team has one player, we know the player is the first one + IDictionary winningTeam = teamList[0]; + TPlayer winner = winningTeam.Keys.First(); + Rating winnerPreviousRating = winningTeam[winner]; + + IDictionary losingTeam = teamList[1]; + TPlayer loser = losingTeam.Keys.First(); + Rating loserPreviousRating = losingTeam[loser]; + + bool wasDraw = (teamRanks[0] == teamRanks[1]); + + var results = new Dictionary(); + 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); + } + + /// + public override double CalculateMatchQuality(GameInfo gameInfo, + IEnumerable> 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs b/Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs new file mode 100644 index 0000000..8a493b8 --- /dev/null +++ b/Skills/TrueSkill/TwoTeamTrueSkillCalculator.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Moserware.Skills.Numerics; + +namespace Moserware.Skills.TrueSkill +{ + /// + /// Calculates new ratings for only two teams where each team has 1 or more players. + /// + /// + /// When you only have two teams, the math is still simple: no factor graphs are used yet. + /// + public class TwoTeamTrueSkillCalculator : SkillCalculator + { + public TwoTeamTrueSkillCalculator() + : base(SupportedOptions.None, Range.Exactly(2), Range.AtLeast(1)) + { + } + + /// + public override IDictionary CalculateNewRatings(GameInfo gameInfo, + IEnumerable + > + teams, params int[] teamRanks) + { + Guard.ArgumentNotNull(gameInfo, "gameInfo"); + ValidateTeamCountAndPlayersCountPerTeam(teams); + + RankSorter.Sort(ref teams, ref teamRanks); + + IDictionary team1 = teams.First(); + IDictionary team2 = teams.Last(); + + bool wasDraw = (teamRanks[0] == teamRanks[1]); + + var results = new Dictionary(); + + 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(GameInfo gameInfo, + IDictionary newPlayerRatings, + IDictionary selfTeam, + IDictionary 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); + } + } + + /// + public override double CalculateMatchQuality(GameInfo gameInfo, + IEnumerable> teams) + { + Guard.ArgumentNotNull(gameInfo, "gameInfo"); + ValidateTeamCountAndPlayersCountPerTeam(teams); + + // We've verified that there's just two teams + ICollection team1 = teams.First().Values; + int team1Count = team1.Count(); + + ICollection 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; + } + } +} \ No newline at end of file diff --git a/Skills/TrueSkillCalculator.cs b/Skills/TrueSkillCalculator.cs new file mode 100644 index 0000000..9f3d05d --- /dev/null +++ b/Skills/TrueSkillCalculator.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using Moserware.Skills.TrueSkill; + +namespace Moserware.Skills +{ + /// + /// Calculates a TrueSkill rating using . + /// + public static class TrueSkillCalculator + { + // Keep a singleton around + private static readonly SkillCalculator _Calculator + = new FactorGraphTrueSkillCalculator(); + + /// + /// Calculates new ratings based on the prior ratings and team ranks. + /// + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2) + /// All the players and their new ratings. + public static IDictionary CalculateNewRatings(GameInfo gameInfo, + IEnumerable + > teams, + params int[] teamRanks) + { + // Just punt the work to the full implementation + return _Calculator.CalculateNewRatings(gameInfo, teams, teamRanks); + } + + /// + /// Calculates the match quality as the likelihood of all teams drawing. + /// + /// The underlying type of the player. + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The match quality as a percentage (between 0.0 and 1.0). + public static double CalculateMatchQuality(GameInfo gameInfo, + IEnumerable> teams) + { + // Just punt the work to the full implementation + return _Calculator.CalculateMatchQuality(gameInfo, teams); + } + } +} \ No newline at end of file diff --git a/Skills/bin/Debug/Moserware.Skills.dll b/Skills/bin/Debug/Moserware.Skills.dll new file mode 100644 index 0000000..b00b103 Binary files /dev/null and b/Skills/bin/Debug/Moserware.Skills.dll differ diff --git a/Skills/bin/Debug/Moserware.Skills.pdb b/Skills/bin/Debug/Moserware.Skills.pdb new file mode 100644 index 0000000..67814ce Binary files /dev/null and b/Skills/bin/Debug/Moserware.Skills.pdb differ diff --git a/Skills/bin/Release/Moserware.Skills.dll b/Skills/bin/Release/Moserware.Skills.dll new file mode 100644 index 0000000..0b7a2dc Binary files /dev/null and b/Skills/bin/Release/Moserware.Skills.dll differ diff --git a/Skills/bin/Release/Moserware.Skills.pdb b/Skills/bin/Release/Moserware.Skills.pdb new file mode 100644 index 0000000..64309d5 Binary files /dev/null and b/Skills/bin/Release/Moserware.Skills.pdb differ diff --git a/UnitTests/Elo/DuellingEloTest.cs b/UnitTests/Elo/DuellingEloTest.cs new file mode 100644 index 0000000..baf3ff1 --- /dev/null +++ b/UnitTests/Elo/DuellingEloTest.cs @@ -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); + } + } +} diff --git a/UnitTests/Elo/EloAssert.cs b/UnitTests/Elo/EloAssert.cs new file mode 100644 index 0000000..2242120 --- /dev/null +++ b/UnitTests/Elo/EloAssert.cs @@ -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); + } + } +} diff --git a/UnitTests/Elo/FideEloCalculatorTest.cs b/UnitTests/Elo/FideEloCalculatorTest.cs new file mode 100644 index 0000000..56febae --- /dev/null +++ b/UnitTests/Elo/FideEloCalculatorTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/UnitTests/Elo/GaussianEloCalculatorTest.cs b/UnitTests/Elo/GaussianEloCalculatorTest.cs new file mode 100644 index 0000000..a6325e5 --- /dev/null +++ b/UnitTests/Elo/GaussianEloCalculatorTest.cs @@ -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); + } + } +} diff --git a/UnitTests/Numerics/GaussianDistributionTests.cs b/UnitTests/Numerics/GaussianDistributionTests.cs new file mode 100644 index 0000000..c1e3b12 --- /dev/null +++ b/UnitTests/Numerics/GaussianDistributionTests.cs @@ -0,0 +1,110 @@ +using System; +using Moserware.Numerics; +using NUnit.Framework; + +namespace UnitTests.Numerics +{ + [TestFixture] + public class GaussianDistributionTests + { + private const double ErrorTolerance = 0.000001; + + [Test] + public void CumulativeToTests() + { + // Verified with WolframAlpha + // (e.g. http://www.wolframalpha.com/input/?i=CDF%5BNormalDistribution%5B0%2C1%5D%2C+0.5%5D ) + Assert.AreEqual(0.691462, GaussianDistribution.CumulativeTo(0.5), ErrorTolerance); + } + + [Test] + public void AtTests() + { + // Verified with WolframAlpha + // (e.g. http://www.wolframalpha.com/input/?i=PDF%5BNormalDistribution%5B0%2C1%5D%2C+0.5%5D ) + Assert.AreEqual(0.352065, GaussianDistribution.At(0.5), ErrorTolerance); + } + + [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 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 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() + { + // Verified with Ralf Herbrich's F# implementation + var standardNormal = new GaussianDistribution(0, 1); + var lpn = GaussianDistribution.LogProductNormalization(standardNormal, standardNormal); + Assert.AreEqual(-1.2655121234846454, lpn, ErrorTolerance); + + var m1s2 = new GaussianDistribution(1, 2); + var m3s4 = new GaussianDistribution(3, 4); + var lpn2 = GaussianDistribution.LogProductNormalization(m1s2, m3s4); + Assert.AreEqual(-2.5168046699816684, lpn2, ErrorTolerance); + } + + [Test] + public void LogRatioNormalizationTests() + { + // Verified with Ralf Herbrich's F# implementation + var m1s2 = new GaussianDistribution(1, 2); + var m3s4 = new GaussianDistribution(3, 4); + var lrn = GaussianDistribution.LogRatioNormalization(m1s2, m3s4); + Assert.AreEqual(2.6157405972171204, lrn, ErrorTolerance); + } + + [Test] + public void AbsoluteDifferenceTests() + { + // Verified with Ralf Herbrich's F# implementation + var standardNormal = new GaussianDistribution(0, 1); + var absDiff = GaussianDistribution.AbsoluteDifference(standardNormal, standardNormal); + Assert.AreEqual(0.0, absDiff, ErrorTolerance); + + var m1s2 = new GaussianDistribution(1, 2); + var m3s4 = new GaussianDistribution(3, 4); + var absDiff2 = GaussianDistribution.AbsoluteDifference(m1s2, m3s4); + Assert.AreEqual(0.4330127018922193, absDiff2, ErrorTolerance); + } + } +} \ No newline at end of file diff --git a/UnitTests/Numerics/MatrixTests.cs b/UnitTests/Numerics/MatrixTests.cs new file mode 100644 index 0000000..41e4497 --- /dev/null +++ b/UnitTests/Numerics/MatrixTests.cs @@ -0,0 +1,196 @@ +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 rounding (thanks to nsp on GitHub for finding this case) + var g = new SquareMatrix(1, 2.00000000000001, + 3, 4); + + var h = new SquareMatrix(1, 2, + 3, 4); + + Assert.IsTrue(g == h); + Assert.AreEqual(g, h); + Assert.AreEqual(g.GetHashCode(), h.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); + } + } +} \ No newline at end of file diff --git a/UnitTests/Properties/AssemblyInfo.cs b/UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..5e1fe1e --- /dev/null +++ b/UnitTests/Properties/AssemblyInfo.cs @@ -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")] diff --git a/UnitTests/README.txt b/UnitTests/README.txt new file mode 100644 index 0000000..d8eea37 --- /dev/null +++ b/UnitTests/README.txt @@ -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. \ No newline at end of file diff --git a/UnitTests/RankSorterTest.cs b/UnitTests/RankSorterTest.cs new file mode 100644 index 0000000..82ed457 --- /dev/null +++ b/UnitTests/RankSorterTest.cs @@ -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 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 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); + } + } +} \ No newline at end of file diff --git a/UnitTests/TrueSkill/DrawMarginTest.cs b/UnitTests/TrueSkill/DrawMarginTest.cs new file mode 100644 index 0000000..831c9c6 --- /dev/null +++ b/UnitTests/TrueSkill/DrawMarginTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs b/UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs new file mode 100644 index 0000000..1f15f23 --- /dev/null +++ b/UnitTests/TrueSkill/FactorGraphTrueSkillCalculatorTests.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/UnitTests/TrueSkill/TrueSkillCalculatorTests.cs b/UnitTests/TrueSkill/TrueSkillCalculatorTests.cs new file mode 100644 index 0000000..33c0307 --- /dev/null +++ b/UnitTests/TrueSkill/TrueSkillCalculatorTests.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs b/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs new file mode 100644 index 0000000..89bd38c --- /dev/null +++ b/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.cs @@ -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 + } + } +} \ No newline at end of file diff --git a/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs b/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs new file mode 100644 index 0000000..af334b5 --- /dev/null +++ b/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..ffde503 --- /dev/null +++ b/UnitTests/UnitTests.csproj @@ -0,0 +1,80 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {6F80946D-AC8B-4063-8588-96841C18BF0A} + Library + Properties + UnitTests + UnitTests + v3.5 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + 3.5 + + + 3.5 + + + 3.5 + + + + + + + + + + + + + + + + + + + + + + {15AD1345-984C-48ED-AF9A-2EAB44E5AA2B} + Skills + + + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.csproj.user b/UnitTests/UnitTests.csproj.user new file mode 100644 index 0000000..68c3597 --- /dev/null +++ b/UnitTests/UnitTests.csproj.user @@ -0,0 +1,9 @@ + + + Program + C:\Program Files (x86)\NUnit 2.5.2\bin\net-2.0\nunit.exe + + + UnitTests.dll + + \ No newline at end of file diff --git a/cmake/modules/CMakeCSharpCompiler.cmake.in b/cmake/modules/CMakeCSharpCompiler.cmake.in new file mode 100644 index 0000000..7d17148 --- /dev/null +++ b/cmake/modules/CMakeCSharpCompiler.cmake.in @@ -0,0 +1,7 @@ +set(CMAKE_CSharp_COMPILER "@CMAKE_CSharp_COMPILER@") +set(CMAKE_CSharp_COMPILER_LOADED @CMAKE_CSharp_COMPILER_LOADED@) +set(GAC_DIR @GAC_DIR@) + +set(_csc_default_lib_path @_csc_default_lib_path@ CACHE INTERNAL "") + +set(CMAKE_CSharp_COMPILER_ENV_VAR "CSC") diff --git a/cmake/modules/CMakeCSharpInformation.cmake b/cmake/modules/CMakeCSharpInformation.cmake new file mode 100644 index 0000000..ecb6585 --- /dev/null +++ b/cmake/modules/CMakeCSharpInformation.cmake @@ -0,0 +1,321 @@ +# copyright (c) 2007, 2009 Arno Rehn arno@arnorehn.de +# copyright (c) 2008 Helio castro helio@kde.org +# +# Redistribution and use is allowed according to the terms of the GPL license. + +# This file adds support for the C# language to cmake. +# +# It adds the following functions: +# +# csharp_add_executable ( [UNSAFE] [WINEXE] [REFERENCES ] +# [COMPILE_FLAGS ] +# [COMPILE_DEFINITIONS ] ) +# +# csharp_add_library ( [UNSAFE] [REFERENCES ] +# [COMPILE_FLAGS ] +# [COMPILE_DEFINITIONS ] ) +# +# install_assembly ( DESTINATION +# [PACKAGE ] ) +# The assembly destination directory is only used if we compile with Visual C# and thus can't use gacutil. +# If a package is specified and a file called .pc.cmake exists in the current source directory, +# this function will configure the template file. All occurences of @assembly@ will be replaced with +# the path to the assembly. The resulting .pc file will be installed to +# /lib/pkgconfig/ . If you want to have a different basename for the template file, +# set the 'pkg-config_template_basename' property of the target with set_property. +# +# Example: +# ------------------------------ +# cmake code: +# ------------------------------ +# csharp_add_library(foo foo.cs) +# install_assembly(foo DESTINATION lib) +# +# ------------------------------ +# contents of foo.pc.cmake file: +# ------------------------------ +# Name: Foo +# Description: Foo library +# Version: 1.0 +# Libs: -r:@assembly@ + +# ----- support macros ----- +macro(GET_LIBRARY_OUTPUT_DIR var) + if (NOT LIBRARY_OUTPUT_PATH) + set(${var} ${CMAKE_CURRENT_BINARY_DIR}) + else (NOT LIBRARY_OUTPUT_PATH) + set(${var} ${LIBRARY_OUTPUT_PATH}) + endif (NOT LIBRARY_OUTPUT_PATH) +endmacro(GET_LIBRARY_OUTPUT_DIR) + +macro(GET_EXECUTABLE_OUTPUT_DIR var) + if (NOT EXECUTABLE_OUTPUT_PATH) + set(${var} ${CMAKE_CURRENT_BINARY_DIR}) + else (NOT EXECUTABLE_OUTPUT_PATH) + set(${var} ${EXECUTABLE_OUTPUT_PATH}) + endif (NOT EXECUTABLE_OUTPUT_PATH) +endmacro(GET_EXECUTABLE_OUTPUT_DIR) + +# This does just not always work... why?! +# macro(MAKE_PROPER_FILE_LIST var) +# foreach(file ${ARGN}) +# if (IS_ABSOLUTE "${file}") +# file(GLOB globbed "${file}") +# else (IS_ABSOLUTE "${file}") +# file(GLOB globbed "${CMAKE_CURRENT_SOURCE_DIR}/${file}") +# endif (IS_ABSOLUTE "${file}") +# +# foreach (glob ${globbed}) +# file(TO_NATIVE_PATH "${glob}" native) +# list(APPEND proper_file_list "${native}") +# endforeach (glob ${globbed}) +# endforeach(file ${ARGN}) +# endmacro(MAKE_PROPER_FILE_LIST) + +# ----- actual functions ----- + +# ----- add an executable ----- +function(csharp_add_executable target) + set(current "s") + set(dotnet_target "exe") + + foreach (arg ${ARGN}) + file(TO_NATIVE_PATH ${arg} native_path) + + if (arg STREQUAL "UNSAFE") + set (unsafe "/unsafe") + elseif (arg STREQUAL "WINEXE") + set (dotnet_target "winexe") + elseif (arg STREQUAL "REFERENCES") + set (current "r") + elseif (arg STREQUAL "COMPILE_FLAGS") + set (current "flags") + elseif (arg STREQUAL "COMPILE_DEFINITIONS") + set (current "defs") + else (arg STREQUAL "UNSAFE") + if (current STREQUAL "s") + # source file + list(APPEND sources ${native_path}) + elseif (current STREQUAL "r") + # reference + if (TARGET ${arg}) + # this is an existing target - get the target assembly + get_property(prop TARGET ${arg} PROPERTY _assembly) + list(APPEND references "/r:${prop}") + list(APPEND deps ${arg}) + else (TARGET ${arg}) + # something different (e.g. assembly name in the gac) + list(APPEND references "/r:${native_path}") + endif (TARGET ${arg}) + elseif (current STREQUAL "flags") + list(APPEND _csc_opts "${arg}") + elseif (current STREQUAL "defs") + list(APPEND _csc_opts "/define:${arg}") + endif (current STREQUAL "s") + endif (arg STREQUAL "UNSAFE") + endforeach (arg ${ARGN}) + + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND _csc_opts "/define:DEBUG") + list(APPEND _csc_opts "/debug") + endif (CMAKE_BUILD_TYPE STREQUAL "Debug") + + get_executable_output_dir(outdir) + if (NOT IS_ABSOLUTE "${outdir}") + message(FATAL_ERROR "Directory \"${outdir}\" is not an absolute path!") + endif (NOT IS_ABSOLUTE "${outdir}") + + file(RELATIVE_PATH relative_path "${CMAKE_BINARY_DIR}" "${outdir}/${target}.exe") + file(TO_NATIVE_PATH "${outdir}/${target}" native_target) + + # inlined - this doesn't work as a macro :( + foreach(file ${sources}) + file(TO_CMAKE_PATH "${file}" cmake_file) + + if (IS_ABSOLUTE "${cmake_file}") + file(GLOB globbed "${cmake_file}") + else (IS_ABSOLUTE "${cmake_file}") + file(GLOB globbed "${CMAKE_CURRENT_SOURCE_DIR}/${cmake_file}") + endif (IS_ABSOLUTE "${cmake_file}") + + foreach (glob ${globbed}) + file(TO_CMAKE_PATH "${glob}" cmake_path) + list(APPEND cmake_file_list "${cmake_path}") + endforeach (glob ${globbed}) + if (NOT globbed) + list(APPEND cmake_file_list "${cmake_file}") + endif (NOT globbed) + list(APPEND compiler_file_list ${file}) + endforeach(file ${sources}) + + get_directory_property(compile_definitions COMPILE_DEFINITIONS) + foreach (def ${compile_definitions}) + # macros with values aren't supported by C# + if (NOT def MATCHES ".*=.*") + list(APPEND _csc_opts "/define:${def}") + endif (NOT def MATCHES ".*=.*") + endforeach (def ${compile_definitions}) + + get_directory_property(link_dirs LINK_DIRECTORIES) + foreach (dir ${link_dirs}) + list(APPEND _csc_opts "/lib:${dir}") + endforeach (dir ${link_dirs}) + + add_custom_command(OUTPUT "${outdir}/${target}.stubexe" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${outdir}" # create the output dir + COMMAND "${CMAKE_CSharp_COMPILER}" /nologo /target:${dotnet_target} "/out:${native_target}.exe" # build the executable + ${_csc_opts} ${unsafe} ${references} ${compiler_file_list} + COMMAND "${CMAKE_COMMAND}" -E touch "${outdir}/${target}.stubexe" # create the stub so that DEPENDS will work + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" # working directory is the source directory, so we don't have to care about relative paths + DEPENDS ${cmake_file_list} + COMMENT "Building ${relative_path}" VERBATIM) # nice comment + add_custom_target(${target} ALL DEPENDS "${outdir}/${target}.stubexe" SOURCES ${cmake_file_list}) # create the actual target + if (deps) + add_dependencies(${target} ${deps}) + endif(deps) +endfunction(csharp_add_executable) + +# ----- add a library ----- +function(csharp_add_library target) + set(current "s") + + foreach (arg ${ARGN}) + file(TO_NATIVE_PATH ${arg} native_path) + + if (arg STREQUAL "UNSAFE") + set (unsafe "/unsafe") + elseif (arg STREQUAL "REFERENCES") + set (current "r") + elseif (arg STREQUAL "COMPILE_FLAGS") + set (current "flags") + elseif (arg STREQUAL "COMPILE_DEFINITIONS") + set (current "defs") + else (arg STREQUAL "UNSAFE") + if (current STREQUAL "s") + # source file + list(APPEND sources ${native_path}) + elseif (current STREQUAL "r") + # reference + if (TARGET ${arg}) + # this is an existing target - get the target assembly + get_property(prop TARGET ${arg} PROPERTY _assembly) + list(APPEND references "/r:${prop}") + list(APPEND deps ${arg}) + else (TARGET ${arg}) + # something different (e.g. assembly name in the gac) + list(APPEND references "/r:${native_path}") + endif (TARGET ${arg}) + elseif (current STREQUAL "flags") + list(APPEND _csc_opts "${arg}") + elseif (current STREQUAL "defs") + list(APPEND _csc_opts "/define:${arg}") + endif (current STREQUAL "s") + endif (arg STREQUAL "UNSAFE") + endforeach (arg ${ARGN}) + + if (CMAKE_BUILD_TYPE STREQUAL "Debug") + list(APPEND _csc_opts "/define:DEBUG") + list(APPEND _csc_opts "/debug") + endif (CMAKE_BUILD_TYPE STREQUAL "Debug") + + get_library_output_dir(outdir) + if (NOT IS_ABSOLUTE "${outdir}") + message(FATAL_ERROR "Directory \"${outdir}\" is not an absolute path!") + endif (NOT IS_ABSOLUTE "${outdir}") + + file(RELATIVE_PATH relative_path "${CMAKE_BINARY_DIR}" "${outdir}/${target}.dll") + file(TO_NATIVE_PATH "${outdir}/${target}" native_target) + + # inlined - this doesn't work as a macro :( + foreach(file ${sources}) + file(TO_CMAKE_PATH "${file}" cmake_file) + + if (IS_ABSOLUTE "${cmake_file}") + file(GLOB globbed "${cmake_file}") + else (IS_ABSOLUTE "${cmake_file}") + file(GLOB globbed "${CMAKE_CURRENT_SOURCE_DIR}/${cmake_file}") + endif (IS_ABSOLUTE "${cmake_file}") + + foreach (glob ${globbed}) + file(TO_CMAKE_PATH "${glob}" cmake_path) + list(APPEND cmake_file_list "${cmake_path}") + endforeach (glob ${globbed}) + if (NOT globbed) + list(APPEND cmake_file_list "${cmake_file}") + endif (NOT globbed) + list(APPEND compiler_file_list ${file}) + endforeach(file ${sources}) + +# message("CMake File List for target ${target}: ${cmake_file_list}") + + get_directory_property(compile_definitions COMPILE_DEFINITIONS) + foreach (def ${compile_definitions}) + # macros with values aren't supported by C# + if (NOT def MATCHES ".*=.*") + list(APPEND _csc_opts "/define:${def}") + endif (NOT def MATCHES ".*=.*") + endforeach (def ${compile_definitions}) + + get_directory_property(link_dirs LINK_DIRECTORIES) + foreach (dir ${link_dirs}) + list(APPEND _csc_opts "/lib:${dir}") + endforeach (dir ${link_dirs}) + + add_custom_command(OUTPUT "${outdir}/${target}.dll" + COMMAND "${CMAKE_COMMAND}" -E make_directory "${outdir}" # create the output dir + COMMAND "${CMAKE_CSharp_COMPILER}" /nologo /target:library "/out:${native_target}.dll" # build the executable + ${_csc_opts} ${unsafe} ${references} ${compiler_file_list} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" # working directory is the source directory, so we don't have to care about relative paths + DEPENDS ${cmake_file_list} + COMMENT "Building ${relative_path}" VERBATIM) # nice comment + add_custom_target(${target} ALL DEPENDS "${outdir}/${target}.dll" SOURCES ${cmake_file_list}) # create the actual target + set_property(TARGET ${target} PROPERTY _assembly "${native_target}.dll") + if (deps) + add_dependencies(${target} ${deps}) + endif(deps) +endfunction(csharp_add_library) + +# ----- install a library assembly ----- +function(install_assembly target DESTINATION destination_dir) + # retrieve the absolute path of the generated assembly + get_property(filename TARGET ${target} PROPERTY _assembly) + get_property(pc_file TARGET ${target} PROPERTY pkg-config_template_basename) + if (NOT pc_file) + set (pc_file ${target}) + endif (NOT pc_file) + + if (NOT filename) + message(FATAL_ERROR "Couldn't retrieve the assembly filename for target ${target}! Are you sure the target is a .NET library assembly?") + endif (NOT filename) + + if (NOT MONO_FOUND) + install(FILES "${filename}" DESTINATION ${destination_dir}) + if (EXISTS "${filename}.config") + install(FILES "${filename}.config" DESTINATION ${destination_dir}) + endif (EXISTS "${filename}.config") + return() + endif (NOT MONO_FOUND) + + if (ARGV3 STREQUAL "PACKAGE" AND ARGV4) + set (package_option "-package ${ARGV4}") + + if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${pc_file}.pc.cmake") + set(assembly "${GAC_DIR}/${ARGV4}/${target}.dll") + configure_file ("${CMAKE_CURRENT_SOURCE_DIR}/${pc_file}.pc.cmake" "${CMAKE_CURRENT_BINARY_DIR}/${pc_file}.pc") + + if (NOT LIB_INSTALL_DIR) + set (LIB_INSTALL_DIR ${CMAKE_INSTALL_PREFIX}/lib) + endif (NOT LIB_INSTALL_DIR) + install(FILES "${CMAKE_CURRENT_BINARY_DIR}/${pc_file}.pc" DESTINATION ${LIB_INSTALL_DIR}/pkgconfig) + endif (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${pc_file}.pc.cmake") + + endif (ARGV3 STREQUAL "PACKAGE" AND ARGV4) + + # So we have the mono runtime and we can use gacutil (it has the -root option, which the MS version doesn't have). + install(CODE "execute_process(COMMAND ${GACUTIL_EXECUTABLE} -i ${filename} ${package_option} -root ${CMAKE_CURRENT_BINARY_DIR}/tmp_gac)") + file(REMOVE_RECURSE ${CMAKE_CURRENT_BINARY_DIR}/tmp_gac/mono) + file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tmp_gac/mono) + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/tmp_gac/mono/ DESTINATION ${GAC_DIR} ) +endfunction(install_assembly) + +set(CMAKE_CSharp_INFORMATION_LOADED 1) diff --git a/cmake/modules/CMakeDetermineCSharpCompiler.cmake b/cmake/modules/CMakeDetermineCSharpCompiler.cmake new file mode 100644 index 0000000..b0fdd61 --- /dev/null +++ b/cmake/modules/CMakeDetermineCSharpCompiler.cmake @@ -0,0 +1,85 @@ +# copyright (c) 2007, 2009 Arno Rehn arno@arnorehn.de +# copyright (c) 2008 Helio castro helio@kde.org +# +# Redistribution and use is allowed according to the terms of the GPL license. + +# determine the compiler to use for C# programs +# NOTE, a generator may set CMAKE_CSharp_COMPILER before +# loading this file to force a compiler. + +if(NOT CMAKE_CSharp_COMPILER) + # prefer the environment variable CSC + if($ENV{CSC} MATCHES ".+") + if (EXISTS $ENV{CSC}) + message(STATUS "Found compiler set in environment variable CSC: $ENV{CSC}.") + set(CMAKE_CSharp_COMPILER $ENV{CSC}) + else (EXISTS $ENV{CSC}) + message(SEND_ERROR "Could not find compiler set in environment variable CSC:\n$ENV{CSC}.") + endif (EXISTS $ENV{CSC}) + endif($ENV{CSC} MATCHES ".+") + + # if no compiler has been specified yet, then look for one + if (NOT CMAKE_CSharp_COMPILER) + find_package(Mono) + set (CMAKE_CSharp_COMPILER "${GMCS_EXECUTABLE}") + + # still not found, try csc.exe + if (NOT CMAKE_CSharp_COMPILER) + get_filename_component(dotnet_path "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\.NETFramework;InstallRoot]" PATH) + find_program(CMAKE_CSharp_COMPILER NAMES csc PATHS "${dotnet_path}/Framework/v2.0.50727") + file(TO_NATIVE_PATH "${dotnet_path}/Framework/v2.0.50727" native_path) + message(STATUS "Looking for csc: ${CMAKE_CSharp_COMPILER}") + + # give up + if (NOT CMAKE_CSharp_COMPILER) + message (STATUS "Couldn't find a valid C# compiler. Set either CMake_CSharp_COMPILER or the CSC environment variable to a valid path.") + endif (NOT CMAKE_CSharp_COMPILER) + endif (NOT CMAKE_CSharp_COMPILER) + endif (NOT CMAKE_CSharp_COMPILER) + +endif(NOT CMAKE_CSharp_COMPILER) + +# now try to find the gac location +if (CMAKE_CSharp_COMPILER AND NOT GAC_DIR AND MONO_FOUND) + find_package(PkgConfig) + + if (PKG_CONFIG_FOUND) + pkg_search_module(MONO_CECIL mono-cecil) + if(MONO_CECIL_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} mono-cecil --variable=assemblies_dir OUTPUT_VARIABLE GAC_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) + endif(MONO_CECIL_FOUND) + + pkg_search_module(CECIL cecil) + if(CECIL_FOUND) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} cecil --variable=assemblies_dir OUTPUT_VARIABLE GAC_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) + endif(CECIL_FOUND) + + if (NOT GAC_DIR) + execute_process(COMMAND ${PKG_CONFIG_EXECUTABLE} mono --variable=libdir OUTPUT_VARIABLE MONO_LIB_DIR OUTPUT_STRIP_TRAILING_WHITESPACE) + if (MONO_LIB_DIR) + set (GAC_DIR "${MONO_LIB_DIR}/mono") + message (STATUS "Could not find cecil, guessing GAC dir from mono prefix: ${GAC_DIR}") + endif (MONO_LIB_DIR) + endif (NOT GAC_DIR) + endif (PKG_CONFIG_FOUND) + + if (NOT GAC_DIR) + set (GAC_DIR "/usr/lib/mono") + message(STATUS "Could not find cecil or mono. Using default GAC dir: ${GAC_DIR}") + endif (NOT GAC_DIR) +endif (CMAKE_CSharp_COMPILER AND NOT GAC_DIR AND MONO_FOUND) + +# Create a cache entry so the user can modify this. +set(GAC_DIR "${GAC_DIR}" CACHE PATH "Location of the GAC") +message(STATUS "Using GAC dir: ${GAC_DIR}") + +mark_as_advanced(CMAKE_CSharp_COMPILER) + +if (CMAKE_CSharp_COMPILER) + set (CMAKE_CSharp_COMPILER_LOADED 1) +endif (CMAKE_CSharp_COMPILER) + +# configure variables set in this file for fast reload later on +configure_file(${CMAKE_SOURCE_DIR}/cmake/modules/CMakeCSharpCompiler.cmake.in + ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeCSharpCompiler.cmake IMMEDIATE @ONLY) +set(CMAKE_CSharp_COMPILER_ENV_VAR "CSC") diff --git a/cmake/modules/FindMono.cmake b/cmake/modules/FindMono.cmake new file mode 100644 index 0000000..7a87c09 --- /dev/null +++ b/cmake/modules/FindMono.cmake @@ -0,0 +1,36 @@ +# - Try to find the mono, mcs, gmcs and gacutil +# +# defines +# +# MONO_FOUND - system has mono, mcs, gmcs and gacutil +# MONO_PATH - where to find 'mono' +# GMCS_PATH - where to find 'gmcs' +# GACUTIL_PATH - where to find 'gacutil' +# +# copyright (c) 2007 Arno Rehn arno@arnorehn.de +# +# Redistribution and use is allowed according to the terms of the GPL license. + +FIND_PROGRAM (MONO_EXECUTABLE mono) +FIND_PROGRAM (GMCS_EXECUTABLE gmcs) +FIND_PROGRAM (GACUTIL_EXECUTABLE gacutil) + +SET (MONO_FOUND FALSE CACHE INTERNAL "") + +IF (MONO_EXECUTABLE AND GMCS_EXECUTABLE AND GACUTIL_EXECUTABLE) + SET (MONO_FOUND TRUE CACHE INTERNAL "") +ENDIF (MONO_EXECUTABLE AND GMCS_EXECUTABLE AND GACUTIL_EXECUTABLE) + +IF (NOT Mono_FIND_QUIETLY) + MESSAGE(STATUS "Path of mono: ${MONO_EXECUTABLE}") + MESSAGE(STATUS "Path of gmcs: ${GMCS_EXECUTABLE}") + MESSAGE(STATUS "Path of gacutil: ${GACUTIL_EXECUTABLE}") +ENDIF (NOT Mono_FIND_QUIETLY) + +IF (NOT MONO_FOUND) + IF (Mono_FIND_REQUIRED) + MESSAGE(FATAL_ERROR "Could not find one or more of the following programs: mono, gmcs, gacutil") + ENDIF (Mono_FIND_REQUIRED) +ENDIF (NOT MONO_FOUND) + +MARK_AS_ADVANCED(MONO_EXECUTABLE GMCS_EXECUTABLE GACUTIL_EXECUTABLE)