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