public string DebugPlayer(Account player) { if (!RatingSystems.Initialized) { return(""); } if (!players.ContainsKey(RatingSystems.GetRatingId(player.AccountID))) { return("Unknown player"); } string debugString = ""; foreach (PlayerDay d in players[RatingSystems.GetRatingId(player.AccountID)].days) { debugString += d.day + ";" + d.getElo() + ";" + d.uncertainty * 100 + ";" + d.wonGames.Select(g => g.whitePlayers.Select(p => p.id.ToString()).Aggregate("", (x, y) => x + "," + y) + "/" + g.blackPlayers.Select(p => p.id.ToString()).Aggregate("", (x, y) => x + "," + y) + "/" + (g.blackWins ? "Second" : "First") + "/" + g.id ).Aggregate("", (x, y) => x + "|" + y) + "\r\n"; } return(debugString); }
//optimized selector method for high player counts public List <Account> GetTopPlayersIn(int count, Dictionary <int, Account> accounts) { lock (updateLockInternal) { int counter = 0; List <Account> retval = new List <Account>(); Account acc; foreach (var pair in sortedPlayers) { if (!accounts.ContainsKey(pair.Value)) { continue; } acc = accounts[pair.Value]; if (playerRatings[RatingSystems.GetRatingId(acc.AccountID)].Rank < int.MaxValue) { if (counter++ >= count) { break; } retval.Add(acc); } } return(retval); } }
public static float GetRankProgress(Account acc) { float bestProgress = 0; bool isActive = false; foreach (var ratingSystem in RatingSystems.GetRatingSystems()) { if (ratingSystem.GetActivePlayers() < 50) { continue; } var rating = ratingSystem.GetPlayerRating(acc.AccountID); if (rating.Rank == int.MaxValue) { continue; } isActive = true; var stdev = Math.Min(10000, rating.Uncertainty); var bracket = ratingSystem.GetPercentileBracket(acc.Rank); var rankCeil = bracket.UpperEloLimit + stdev; var rankFloor = bracket.LowerEloLimit - stdev; bestProgress = Math.Max(bestProgress, Math.Min(1, (rating.RealElo - rankFloor) / (rankCeil - rankFloor))); //Trace.TraceInformation(acc.Name + ": bracket(" + bracket.LowerEloLimit + ", " + bracket.UpperEloLimit + ") requirements (" + rankFloor + ", " + rankCeil + ") current: " + rating.RealElo + " -> progress: " + bestProgress); } if (!isActive) { return(0.001f); } return(bestProgress); }
public List <Account> GetTopPlayers(int count, Func <Account, bool> selector) { lock (updateLockInternal) { int counter = 0; List <Account> retval = new List <Account>(); using (ZkDataContext db = new ZkDataContext()) { foreach (var pair in sortedPlayers) { Account acc = db.Accounts .Where(a => (a.AccountID) == pair.Value) .Include(a => a.Clan) .Include(a => a.Faction) .FirstOrDefault(); if (playerRatings[RatingSystems.GetRatingId(acc.AccountID)].Rank < int.MaxValue && selector.Invoke(acc)) { if (counter++ >= count) { break; } retval.Add(acc); } } } return(retval); } }
public Dictionary <DateTime, float> GetPlayerRatingHistory(int AccountID) { if (!players.ContainsKey(RatingSystems.GetRatingId(AccountID))) { return(new Dictionary <DateTime, float>()); } return(players[RatingSystems.GetRatingId(AccountID)].days.ToDictionary(day => RatingSystems.ConvertDaysToDate(day.day), day => day.getElo() + RatingOffset)); }
public Dictionary <DateTime, float> GetPlayerLadderRatingHistory(int AccountID) { if (!players.ContainsKey(RatingSystems.GetRatingId(AccountID))) { return(new Dictionary <DateTime, float>()); } return(players[RatingSystems.GetRatingId(AccountID)].days.ToDictionary(day => RatingSystems.ConvertDaysToDate(day.day), day => day.GetElo() + RatingOffset - day.GetEloStdev() * GlobalConst.RatingConfidenceSigma)); }
public List <float> PredictOutcome(IEnumerable <IEnumerable <Account> > teams, DateTime time) { return(teams.Select(t => SetupGame(t.Select(x => RatingSystems.GetRatingId(x.AccountID)).ToList(), teams.Where(t2 => !t2.Equals(t)).SelectMany(t2 => t2.Select(x => RatingSystems.GetRatingId(x.AccountID))).ToList(), true, RatingSystems.ConvertDateToDays(time), -1 ).getBlackWinProbability() * 2 / teams.Count()).ToList()); }
public List <float> PredictOutcome(List <ICollection <Account> > teams) { return(teams.Select(t => SetupGame(t.Select(x => x.AccountID).ToList(), teams.Where(t2 => !t2.Equals(t)).SelectMany(t2 => t2.Select(x => x.AccountID)).ToList(), true, RatingSystems.ConvertDateToDays(DateTime.Now), -1 ).getBlackWinProbability() * 2 / teams.Count).ToList()); }
public void ProcessBattle(SpringBattle battle, bool removeBattle = false) { ICollection <int> winners = battle.SpringBattlePlayers.Where(p => p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).ToList(); ICollection <int> losers = battle.SpringBattlePlayers.Where(p => !p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).ToList(); int date = RatingSystems.ConvertDateToDays(battle.StartTime); if (removeBattle) { if (ProcessedBattles.Contains(battle.SpringBattleID) && RatingSystems.Initialized) { Trace.TraceInformation("WHR " + category + " removing battle " + battle.SpringBattleID + " from " + battle.StartTime); var game = SetupGame(losers, winners, false, date, battle.SpringBattleID); losers.Union(winners).Select(x => getPlayerById(x)).ForEach(x => x.RemoveGame(game)); ProcessedBattles.Remove(battle.SpringBattleID); battlesRegistered--; latestBattle = battle; Trace.TraceInformation(battlesRegistered + " battles registered for WHR " + category + ", latest Battle: " + battle.SpringBattleID); UpdateRatings(); } return; } if (winners.Count > 0 && losers.Count > 0 && winners.Intersect(losers).Count() == 0) { if (ProcessedBattles.Contains(battle.SpringBattleID)) { return; } battlesRegistered++; ProcessedBattles.Add(battle.SpringBattleID); if (firstBattle == null) { firstBattle = battle; } latestBattle = battle; if (date > RatingSystems.ConvertDateToDays(DateTime.UtcNow)) { Trace.TraceWarning("WHR " + category + ": Tried to register battle " + battle.SpringBattleID + " which is from the future " + (date) + " > " + RatingSystems.ConvertDateToDays(DateTime.UtcNow)); } else { createGame(losers, winners, false, date, battle.SpringBattleID); if (RatingSystems.Initialized) { lastBattleRanked = true; Trace.TraceInformation(battlesRegistered + " battles registered for WHR " + category + ", latest Battle: " + battle.SpringBattleID); UpdateRatings(); } } } }
public List <float> PredictOutcome(IEnumerable <IEnumerable <Account> > teams, DateTime time) { var predictions = teams.Select(t => SetupGame(t.Select(x => (x.AccountID)).Distinct().ToList(), teams.Where(t2 => !t2.Equals(t)).Select(t2 => (ICollection <int>)t2.Select(x => (x.AccountID)).Distinct().ToList()).ToList(), RatingSystems.ConvertDateToDays(time), -1, true ).GetWinProbability()).ToList(); return(predictions); }
private void runIterations(int count) { for (int i = 0; i < count; i++) { runSingleIteration(); } foreach (Player p in players.Values) { p.updateUncertainty(); } RatingSystems.BackupToDB(this); }
public PlayerRating GetPlayerRating(int accountID) { if (!completelyInitialized) { return(cachedDbRatings.GetOrAdd(accountID, id => { using (var db = new ZkDataContext()) return db.AccountRatings.FirstOrDefault(x => x.AccountID == id && x.RatingCategory == category) ?.ToPlayerRating() ?? DefaultRating; })); } return(playerRatings.ContainsKey(RatingSystems.GetRatingId(accountID)) ? playerRatings[RatingSystems.GetRatingId(accountID)] : DefaultRating); }
public void ProcessBattle(SpringBattle battle) { latestBattle = battle; ICollection <int> winners = battle.SpringBattlePlayers.Where(p => p.IsInVictoryTeam && !p.IsSpectator).Select(p => p.AccountID).ToList(); ICollection <int> losers = battle.SpringBattlePlayers.Where(p => !p.IsInVictoryTeam && !p.IsSpectator).Select(p => p.AccountID).ToList(); if (winners.Count > 0 && losers.Count > 0) { battlesRegistered++; createGame(losers, winners, false, RatingSystems.ConvertDateToDays(battle.StartTime), battle.SpringBattleID); if (RatingSystems.Initialized) { Trace.TraceInformation(battlesRegistered + " battles registered for WHR"); UpdateRatings(); } } }
public static float GetRankProgress(Account acc) { float bestProgress = 0; bool isActive = false; foreach (var ratingSystem in RatingSystems.GetRatingSystems()) { var progress = GetRankProgress(acc, ratingSystem); if (progress != null) { isActive = true; bestProgress = Math.Max(bestProgress, progress.ProgressRatio); } } if (!isActive) { return(0.001f); } return(bestProgress); }
public float GetAverageRecentWinChance(int AccountID) { if (!players.ContainsKey((AccountID))) { return(0.4f); } var recentGames = players[AccountID].days .Where(day => day.day >= RatingSystems.ConvertDateToDays(DateTime.UtcNow.AddDays(-1))) .SelectMany(day => day.games.SelectMany(g => g)) .OrderByDescending(g => g.id) .Take(5); float recentWinChance = 0.4f; if (recentGames.Count() > 0) { recentWinChance = recentGames.Select(x => x.winnerPlayers.Contains(players[AccountID]) ? x.GetWinProbability() : (1 - x.GetWinProbability())).Average(); } //Trace.TraceInformation($"The {recentGames.Count()} most recent games for {AccountID} are {string.Join(", ", recentGames.Select(x => x.id))}, the average win chance is {recentWinChance}."); return(recentWinChance); }
private void UpdateLadderRatings() { float avgElo = 0; float weightSum = 0; float varSum = 0; int minDay = RatingSystems.ConvertDateToDays(DateTime.UtcNow) - GlobalConst.LadderActivityDays; for (int i = 0; i < days.Count; i++) { if (days[i].day >= minDay) { weightSum += days[i].weight; varSum += days[i].weight * days[i].GetEloStdev() * days[i].GetEloStdev(); avgElo += days[i].GetElo() * days[i].weight; } } avgElo /= weightSum; varSum /= weightSum; this.avgEloVar = varSum; this.avgElo = avgElo; }
public void ProcessBattle(SpringBattle battle) { ICollection <int> winners = battle.SpringBattlePlayers.Where(p => p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).Distinct().ToList(); ICollection <int> losers = battle.SpringBattlePlayers.Where(p => !p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).Distinct().ToList(); int date = RatingSystems.ConvertDateToDays(battle.StartTime); if (RatingSystems.Initialized) { if (winners.Intersect(losers).Any()) { Trace.TraceWarning("WHR B" + battle.SpringBattleID + " has winner loser intersection"); } if (ProcessedBattles.Contains(battle.SpringBattleID)) { Trace.TraceWarning("WHR B" + battle.SpringBattleID + " has already been processed"); } if (winners.Count == 0) { Trace.TraceWarning("WHR B" + battle.SpringBattleID + " has no winner"); } if (losers.Count == 0) { Trace.TraceWarning("WHR B" + battle.SpringBattleID + " has no loser"); } } if (!winners.Intersect(losers).Any() && !ProcessedBattles.Contains(battle.SpringBattleID) && winners.Count > 0 && losers.Count > 0) { battlesRegistered++; ProcessedBattles.Add(battle.SpringBattleID); if (date > RatingSystems.ConvertDateToDays(DateTime.UtcNow)) { Trace.TraceWarning("WHR " + category + ": Tried to register battle " + battle.SpringBattleID + " which is from the future " + (date) + " > " + RatingSystems.ConvertDateToDays(DateTime.UtcNow)); } else { CreateGame(losers, winners, false, date, battle.SpringBattleID); futureDebriefings.ForEach(u => pendingDebriefings.TryAdd(u.Key, u.Value)); futureDebriefings.Clear(); if (RatingSystems.Initialized) { Trace.TraceInformation(battlesRegistered + " battles registered for WHR " + category + ", latest Battle: " + battle.SpringBattleID); UpdateRatings(); } } } else { PendingDebriefing debriefing; futureDebriefings.TryGetValue(battle.SpringBattleID, out debriefing); if (debriefing == null) { pendingDebriefings.TryGetValue(battle.SpringBattleID, out debriefing); } if (debriefing != null) { Trace.TraceWarning("Battle " + battle.SpringBattleID + " was processed before attaching pending report"); debriefing.debriefingConsumer.Invoke(debriefing.partialDebriefing); } } }
//private //Runs in O(N log(N)) for all players private void UpdateRankings(IEnumerable <Player> players) { var debriefings = new Dictionary <int, PendingDebriefing>(pendingDebriefings); int matched = 0; try { //check for ladder elo updates using (var db = new ZkDataContext()) { var battleIDs = debriefings.Keys.ToList(); foreach (var battleId in debriefings.Keys) { List <SpringBattlePlayer> lastBattlePlayers = db.SpringBattlePlayers.Where(p => p.SpringBattleID == battleId && !p.IsSpectator).Include(x => x.Account).DistinctBy(x => x.AccountID).ToList(); Dictionary <int, float> oldRatings = lastBattlePlayers.ToDictionary(p => (p.AccountID), p => GetPlayerRating(p.AccountID).LadderElo); lastBattlePlayers.Where(p => !playerRatings.ContainsKey((p.AccountID))).ForEach(p => playerRatings[(p.AccountID)] = new PlayerRating(DefaultRating)); Dictionary <int, float> winChances = db.SpringBattles.Where(p => p.SpringBattleID == battleId).First().GetAllyteamWinChances(); lastBattlePlayers.ForEach(p => { float eloChange = (p.IsInVictoryTeam ? (1f - winChances[p.AllyNumber]) : (-winChances[p.AllyNumber])) * GlobalConst.LadderEloClassicEloK / lastBattlePlayers.Count(x => x.AllyNumber == p.AllyNumber); playerRatings[p.AccountID].LadderElo = Ranks.UpdateLadderRating(p.Account, category, getPlayerById(p.AccountID).avgElo + RatingOffset, p.IsInVictoryTeam, !p.IsInVictoryTeam, eloChange, db); }); lastBattlePlayers.Where(p => !p.EloChange.HasValue).ForEach(p => { p.EloChange = playerRatings[(p.AccountID)].LadderElo - oldRatings[(p.AccountID)]; db.SpringBattlePlayers.Attach(p); db.Entry(p).Property(x => x.EloChange).IsModified = true; }); db.SaveChanges(); } } //update ladders int currentDay = RatingSystems.ConvertDateToDays(DateTime.UtcNow); int playerCount = 0; using (var db = new ZkDataContext()) { foreach (var p in players) { if (p.days.Count == 0) { Trace.TraceError("WHR " + category + " has invalid player " + p.id + " with no days(games)"); continue; } float elo = p.days.Last().GetElo() + RatingOffset; float lastNaturalRatingVar = p.days.Last().naturalRatingVariance; var lastDay = p.days.Last(); float ladderElo; if (playerRatings.ContainsKey(p.id)) { ladderElo = playerRatings[p.id].LadderElo; } else { ladderElo = (float?)db.AccountRatings.Where(x => x.AccountID == p.id && x.RatingCategory == category).FirstOrDefault()?.LadderElo ?? DefaultRating.LadderElo; } playerRatings[p.id] = new PlayerRating(int.MaxValue, 1, elo, lastNaturalRatingVar, GlobalConst.NaturalRatingVariancePerDay(lastDay.totalWeight), lastDay.day, currentDay, ladderElo, !float.IsNaN(p.avgElo)); float rating = -playerRatings[p.id].LadderElo; if (playerKeys.ContainsKey(p.id)) { sortedPlayers.Remove(playerKeys[p.id]); } while (sortedPlayers.ContainsKey(rating)) { rating += 0.01f; } playerKeys[p.id] = rating; sortedPlayers[rating] = p.id; if (playerRatings[p.id].Ranked) { playerCount++; } } } this.activePlayers = playerCount; int rank = 0; List <int> newTopPlayers = new List <int>(); List <float> newPercentileBrackets = new List <float>(); newPercentileBrackets.Add(playerRatings[sortedPlayers.First().Value].LadderElo); float percentile; float[] percentilesRev = Ranks.Percentiles.Reverse().ToArray(); foreach (var pair in sortedPlayers) { if (playerRatings[pair.Value].Ranked) { newTopPlayers.Add(pair.Value); if (rank == matched && rank < topPlayers.Count && topPlayers[rank] == pair.Value) { matched++; } rank++; percentile = (float)rank / activePlayers; if (newPercentileBrackets.Count <= Ranks.Percentiles.Length && percentile > percentilesRev[newPercentileBrackets.Count - 1]) { newPercentileBrackets.Add(playerRatings[pair.Value].LadderElo); } playerRatings[pair.Value].ApplyLadderUpdate(rank, percentile, currentDay, true); } else if (playerRatings[pair.Value].Rank < int.MaxValue) { playerRatings[pair.Value].ApplyLadderUpdate(int.MaxValue, 1, currentDay, false); } } if (rank != playerCount) { Trace.TraceWarning("WHR has " + playerCount + " active players, but " + rank + " sorted active players"); } while (newPercentileBrackets.Count < Ranks.Percentiles.Length + 1) { newPercentileBrackets.Add(playerRatings[sortedPlayers.Last().Value].LadderElo); } PercentileBrackets = newPercentileBrackets.Select(x => x).Reverse().ToArray(); topPlayers = newTopPlayers; laddersCache = new List <Account>(); Trace.TraceInformation("WHR " + category + " Ladders updated with " + topPlayers.Count + "/" + this.players.Count + " entries. Brackets are now: " + string.Join(", ", PercentileBrackets)); var playerIds = players.Select(x => x.id).ToList(); if (playerIds.Count() < 100) { SaveToDB(playerIds); } else { SaveToDB(); } } catch (Exception ex) { Trace.TraceError("WHR " + category + ": Failed to update rankings: " + ex); PendingDebriefing discard2; debriefings.ForEach(x => pendingDebriefings.TryRemove(x.Key, out discard2)); return; } try { //check for rank updates if (debriefings.Any()) { Trace.TraceInformation("WHR Filling in Debriefings for Battles: " + debriefings.Keys.Select(x => "B" + x).StringJoin()); using (var db = new ZkDataContext()) { foreach (var battleId in debriefings.Keys) { List <SpringBattlePlayer> lastBattlePlayers = db.SpringBattlePlayers.Where(p => p.SpringBattleID == battleId && !p.IsSpectator).Include(x => x.Account).DistinctBy(x => x.AccountID).ToList(); Dictionary <int, SpringBattlePlayer> involvedPlayers = lastBattlePlayers.ToDictionary(p => p.AccountID, p => p); Trace.TraceInformation("WHR Debriefing players: " + involvedPlayers.Values.Select(x => x.Account.Name).StringJoin()); Dictionary <int, int> oldRanks = lastBattlePlayers.ToDictionary(p => p.AccountID, p => p.Account.Rank); Dictionary <int, Account> updatedRanks = lastBattlePlayers.Where(p => Ranks.UpdateRank(p.Account, p.IsInVictoryTeam, !p.IsInVictoryTeam, db)).Select(x => x.Account).ToDictionary(p => p.AccountID, p => p); updatedRanks.Values.ForEach(p => { db.Accounts.Attach(p); db.Entry(p).Property(x => x.Rank).IsModified = true; }); List <int> playersWithRatingChange = lastBattlePlayers.Select(x => x.AccountID).ToList(); db.SaveChanges(); //Publish new results only after saving new stats to db. debriefings[battleId].partialDebriefing.DebriefingUsers.Values.ForEach(user => { try { user.EloChange = involvedPlayers[user.AccountID].EloChange ?? 0; user.IsRankup = updatedRanks.ContainsKey(user.AccountID) && oldRanks[user.AccountID] < updatedRanks[user.AccountID].Rank; user.IsRankdown = updatedRanks.ContainsKey(user.AccountID) && oldRanks[user.AccountID] > updatedRanks[user.AccountID].Rank; var prog = Ranks.GetRankProgress(involvedPlayers[user.AccountID].Account, this); if (prog == null) { Trace.TraceWarning("User " + user.AccountID + " is wrongfully unranked"); } user.NextRankElo = prog.RankCeilElo; user.PrevRankElo = prog.RankFloorElo; user.NewElo = prog.CurrentElo; } catch (Exception ex) { Trace.TraceError("Unable to complete debriefing for user " + user.AccountID + ": " + ex); } }); debriefings[battleId].partialDebriefing.RatingCategory = category.ToString(); debriefings[battleId].debriefingConsumer.Invoke(debriefings[battleId].partialDebriefing); RatingsUpdated(this, new RatingUpdate() { affectedPlayers = playersWithRatingChange }); } } } //check for topX updates GetTopPlayers(GlobalConst.LadderSize); foreach (var listener in topPlayersUpdateListeners) { if (matched < listener.Value) { listener.Key.TopPlayersUpdated(GetTopPlayers(listener.Value)); } } } catch (Exception ex) { Trace.TraceError("WHR " + category + ": Failed to process battles for rankings: " + ex); } PendingDebriefing discard; debriefings.ForEach(x => pendingDebriefings.TryRemove(x.Key, out discard)); }
public PlayerRating GetPlayerRating(int accountID) { return(playerRatings.ContainsKey(RatingSystems.GetRatingId(accountID)) ? playerRatings[RatingSystems.GetRatingId(accountID)] : DefaultRating); }
public PlayerDay GetInternalRating(int accountID, DateTime time) { return(players[accountID].days.Find(x => x.day == RatingSystems.ConvertDateToDays(time))); }
public void UpdateRatings() { if (!RatingSystems.Initialized) { return; } if (battlesRegistered == 0) { Trace.TraceWarning("No battles registered for WHR " + category); return; } lock (updateLock) { Action updateAction = null; if (!completelyInitialized) { updateAction = (() => { Trace.TraceInformation("Initializing WHR " + category + " ratings for " + battlesRegistered + " battles, this will take some time.."); runIterations(75); UpdateRankings(players.Values); playerOldRatings = new Dictionary <int, PlayerRating>(playerRatings); completelyInitialized = true; cachedDbRatings.Clear(); }); } else if (DateTime.UtcNow.Subtract(lastUpdateTime).TotalHours >= GlobalConst.LadderUpdatePeriod) { updateAction = (() => { Trace.TraceInformation("Updating all WHR " + category + " ratings"); runIterations(1); UpdateRankings(players.Values); }); lastUpdateTime = DateTime.UtcNow; } else { updateAction = (() => { Trace.TraceInformation("Updating WHR " + category + " ratings for pending battles: " + pendingDebriefings.Keys.Select(x => "B" + x).StringJoin()); IEnumerable <Player> players = pendingDebriefings.Values.SelectMany(x => x.battle.SpringBattlePlayers).Where(p => !p.IsSpectator).Select(p => getPlayerById(RatingSystems.GetRatingId(p.AccountID))); players.ForEach(p => p.RunOneNewtonIteration(true)); UpdateRankings(players); }); } Task.Factory.StartNew(() => { try { lock (updateLockInternal) { DateTime start = DateTime.Now; updateAction.Invoke(); Trace.TraceInformation("WHR " + category + " Ratings updated in " + DateTime.Now.Subtract(start).TotalSeconds + " seconds, " + (GC.GetTotalMemory(false) / (1 << 20)) + "MiB total memory allocated"); } } catch (Exception ex) { Trace.TraceError("Thread error while updating WHR " + category + " " + ex); } }, CancellationToken.None, TaskCreationOptions.None, PriorityScheduler.BelowNormal); } }
//private //Runs in O(N log(N)) for all players private void UpdateRankings(IEnumerable <Player> players) { try { Dictionary <int, float> oldRatings = new Dictionary <int, float>(); //check for ladder elo updates using (var db = new ZkDataContext()) { var battleIDs = pendingDebriefings.Keys.ToList(); var lastBattlePlayers = db.SpringBattlePlayers.Where(p => battleIDs.Contains(p.SpringBattleID) && !p.IsSpectator).Include(x => x.Account).ToList(); oldRatings = lastBattlePlayers.ToDictionary(p => p.AccountID, p => playerRatings[p.AccountID].LadderElo); lastBattlePlayers.ForEach(p => playerRatings[p.AccountID].LadderElo = Ranks.UpdateLadderRating(p.Account, category, this.players[p.AccountID].avgElo + RatingOffset, p.IsInVictoryTeam, !p.IsInVictoryTeam, db)); } //update ladders int currentDay = RatingSystems.ConvertDateToDays(DateTime.UtcNow); int playerCount = 0; using (var db = new ZkDataContext()) { foreach (var p in players) { if (p.days.Count == 0) { Trace.TraceError("WHR " + category + " has invalid player " + p.id + " with no days(games)"); continue; } float elo = p.days.Last().GetElo() + RatingOffset; float lastNaturalRatingVar = p.avgEloVar * GlobalConst.EloToNaturalRatingMultiplierSquared; var lastDay = p.days.Last(); float ladderElo; if (playerRatings.ContainsKey(p.id)) { ladderElo = playerRatings[p.id].LadderElo; } else { ladderElo = (float?)db.AccountRatings.Where(x => x.AccountID == p.id && x.RatingCategory == category).FirstOrDefault()?.LadderElo ?? DefaultRating.LadderElo; } playerRatings[p.id] = new PlayerRating(int.MaxValue, 1, elo, lastNaturalRatingVar, GlobalConst.NaturalRatingVariancePerDay(lastDay.totalWeight), lastDay.day, currentDay, ladderElo, !float.IsNaN(p.avgElo)); float rating = -playerRatings[p.id].LadderElo + 0.001f * (float)rand.NextDouble(); if (playerKeys.ContainsKey(p.id)) { sortedPlayers.Remove(playerKeys[p.id]); } playerKeys[p.id] = rating; sortedPlayers[rating] = p.id; if (playerRatings[p.id].Ranked) { playerCount++; } } } this.activePlayers = playerCount; int rank = 0; List <int> newTopPlayers = new List <int>(); int matched = 0; List <float> newPercentileBrackets = new List <float>(); newPercentileBrackets.Add(playerRatings[sortedPlayers.First().Value].LadderElo + 420); float percentile; float[] percentilesRev = Ranks.Percentiles.Reverse().ToArray(); foreach (var pair in sortedPlayers) { if (playerRatings[pair.Value].Ranked) { newTopPlayers.Add(pair.Value); if (rank == matched && rank < topPlayers.Count && topPlayers[rank] == pair.Value) { matched++; } rank++; percentile = (float)rank / activePlayers; if (newPercentileBrackets.Count <= Ranks.Percentiles.Length && percentile > percentilesRev[newPercentileBrackets.Count - 1]) { newPercentileBrackets.Add(playerRatings[pair.Value].LadderElo); } playerRatings[pair.Value].ApplyLadderUpdate(rank, percentile, currentDay, true); } else if (playerRatings[pair.Value].Rank < int.MaxValue) { playerRatings[pair.Value].ApplyLadderUpdate(int.MaxValue, 1, currentDay, false); } } newPercentileBrackets.Add(newPercentileBrackets.Last() - 420); PercentileBrackets = newPercentileBrackets.Select(x => x).Reverse().ToArray(); topPlayers = newTopPlayers; laddersCache = new List <Account>(); Trace.TraceInformation("WHR " + category + " Ladders updated with " + topPlayers.Count + "/" + this.players.Count + " entries. Brackets are now: " + string.Join(", ", PercentileBrackets)); var playerIds = players.Select(x => x.id).ToList(); if (playerIds.Count() < 100) { SaveToDB(playerIds); } else { SaveToDB(); } //check for rank updates if (pendingDebriefings.Any()) { List <int> playersWithRatingChange = new List <int>(); Dictionary <int, int> oldRanks = new Dictionary <int, int>(); Dictionary <int, Account> updatedRanks = new Dictionary <int, Account>(); Dictionary <int, Account> involvedAccounts = new Dictionary <int, Account>(); Trace.TraceInformation("WHR Filling in Debriefings for Battles: " + pendingDebriefings.Keys.Select(x => "B" + x).StringJoin()); using (var db = new ZkDataContext()) { var battleIDs = pendingDebriefings.Keys.ToList(); var lastBattlePlayers = db.SpringBattlePlayers.Where(p => battleIDs.Contains(p.SpringBattleID) && !p.IsSpectator).Include(x => x.Account).ToList(); involvedAccounts = lastBattlePlayers.ToDictionary(p => p.AccountID, p => p.Account); Trace.TraceInformation("WHR Debriefing players: " + involvedAccounts.Values.Select(x => x.Name).StringJoin()); oldRanks = lastBattlePlayers.ToDictionary(p => p.AccountID, p => p.Account.Rank); updatedRanks = lastBattlePlayers.Where(p => Ranks.UpdateRank(p.Account, p.IsInVictoryTeam, !p.IsInVictoryTeam, db)).Select(x => x.Account).ToDictionary(p => p.AccountID, p => p); updatedRanks.Values.ForEach(p => db.Entry(p).State = EntityState.Modified); playersWithRatingChange = lastBattlePlayers.Select(x => x.AccountID).ToList(); lastBattlePlayers.Where(p => playerOldRatings.ContainsKey(RatingSystems.GetRatingId(p.AccountID)) && !p.EloChange.HasValue).ForEach(p => { //p.EloChange = playerRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo - playerOldRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo; p.EloChange = playerRatings[p.AccountID].LadderElo - oldRatings[p.AccountID]; }); db.SpringBattlePlayers.Where(p => battleIDs.Contains(p.SpringBattleID) && !p.IsSpectator).ToList().ForEach(x => playerOldRatings[RatingSystems.GetRatingId(x.AccountID)] = playerRatings[RatingSystems.GetRatingId(x.AccountID)]); db.SaveChanges(); } //Publish new results only after saving new stats to db. pendingDebriefings.ForEach(pair => { pair.Value.partialDebriefing.DebriefingUsers.Values.ForEach(user => { try { user.EloChange = playerRatings[user.AccountID].LadderElo - oldRatings[user.AccountID]; user.IsRankup = updatedRanks.ContainsKey(user.AccountID) && oldRanks[user.AccountID] < updatedRanks[user.AccountID].Rank; user.IsRankdown = updatedRanks.ContainsKey(user.AccountID) && oldRanks[user.AccountID] > updatedRanks[user.AccountID].Rank; var prog = Ranks.GetRankProgress(involvedAccounts[user.AccountID], this); if (prog == null) { Trace.TraceWarning("User " + user.AccountID + " is wrongfully unranked"); } user.NextRankElo = prog.RankCeilElo; user.PrevRankElo = prog.RankFloorElo; user.NewElo = prog.CurrentElo; } catch (Exception ex) { Trace.TraceError("Unable to complete debriefing for user " + user.AccountID + ": " + ex); } }); pair.Value.partialDebriefing.RatingCategory = category.ToString(); pair.Value.debriefingConsumer.Invoke(pair.Value.partialDebriefing); }); RatingsUpdated(this, new RatingUpdate() { affectedPlayers = playersWithRatingChange }); pendingDebriefings.Clear(); } //check for topX updates GetTopPlayers(GlobalConst.LadderSize); foreach (var listener in topPlayersUpdateListeners) { if (matched < listener.Value) { listener.Key.TopPlayersUpdated(GetTopPlayers(listener.Value)); } } } catch (Exception ex) { string dbg = "WHR " + category + ": Failed to update rankings " + ex + "\nPlayers: "; foreach (var p in players) { dbg += p.id + " (" + p.days.Count + " days), "; } Trace.TraceError(dbg); } }
public void UpdateRatings() { if (!RatingSystems.Initialized) { return; } if (latestBattle == null) { //Trace.TraceInformation("WHR " + category +": No battles to evaluate"); return; } lock (updateLock) { Action updateAction = null; if (lastUpdate == null) { updateAction = (() => { Trace.TraceInformation("Initializing WHR " + category + " ratings for " + battlesRegistered + " battles, this will take some time.. From B" + firstBattle?.SpringBattleID + " to B" + latestBattle?.SpringBattleID); runIterations(75); UpdateRankings(players.Values); playerOldRatings = new Dictionary <int, PlayerRating>(playerRatings); completelyInitialized = true; cachedDbRatings.Clear(); }); } else if (DateTime.UtcNow.Subtract(lastUpdateTime).TotalHours >= GlobalConst.LadderUpdatePeriod) { updateAction = (() => { Trace.TraceInformation("Updating all WHR " + category + " ratings"); runIterations(1); UpdateRankings(players.Values); }); lastUpdateTime = DateTime.UtcNow; } else if (!latestBattle.Equals(lastUpdate)) { updateAction = (() => { Trace.TraceInformation("Updating WHR " + category + " ratings for last Battle: " + latestBattle.SpringBattleID); IEnumerable <Player> players = latestBattle.SpringBattlePlayers.Where(p => !p.IsSpectator).Select(p => getPlayerById(RatingSystems.GetRatingId(p.AccountID))); players.ForEach(p => p.runOneNewtonIteration()); players.ForEach(p => p.updateUncertainty()); UpdateRankings(players); }); } else { //Trace.TraceInformation("No WHR " + category +" ratings to update"); return; } var lastUpdateEx = lastUpdate; Task.Factory.StartNew(() => { try { lock (updateLockInternal) { DateTime start = DateTime.Now; updateAction.Invoke(); Trace.TraceInformation("WHR " + category + " Ratings updated in " + DateTime.Now.Subtract(start).TotalSeconds + " seconds, " + (GC.GetTotalMemory(false) / (1 << 20)) + "MiB total memory allocated"); } } catch (Exception ex) { Trace.TraceError("Thread error while updating WHR " + category + " " + ex); } }, CancellationToken.None, TaskCreationOptions.None, PriorityScheduler.BelowNormal); lastUpdate = latestBattle; } }
public void UpdateRatings() { if (!RatingSystems.Initialized) { return; } if (latestBattle == null) { //Trace.TraceInformation("WHR " + category +": No battles to evaluate"); return; } lock (updateLock) { Action updateAction = null; if (lastUpdate == null) { updateAction = (() => { Trace.TraceInformation("Initializing WHR " + category + " ratings for " + battlesRegistered + " battles, this will take some time.. From B" + firstBattle?.SpringBattleID + " to B" + latestBattle?.SpringBattleID); runIterations(75); UpdateRankings(players.Values); playerOldRatings = new Dictionary <int, PlayerRating>(playerRatings); }); } else if (DateTime.UtcNow.Subtract(lastUpdateTime).TotalHours >= GlobalConst.LadderUpdatePeriod) { updateAction = (() => { Trace.TraceInformation("Updating all WHR " + category + " ratings"); runIterations(1); UpdateRankings(players.Values); }); lastUpdateTime = DateTime.UtcNow; } else if (!latestBattle.Equals(lastUpdate)) { updateAction = (() => { Trace.TraceInformation("Updating WHR " + category + " ratings for last Battle: " + latestBattle.SpringBattleID); IEnumerable <Player> players = latestBattle.SpringBattlePlayers.Where(p => !p.IsSpectator).Select(p => getPlayerById(RatingSystems.GetRatingId(p.AccountID))); players.ForEach(p => p.runOneNewtonIteration()); players.ForEach(p => p.updateUncertainty()); UpdateRankings(players); }); } else { //Trace.TraceInformation("No WHR " + category +" ratings to update"); return; } var lastUpdateEx = lastUpdate; Task.Factory.StartNew(() => { try { lock (updateLockInternal) { DateTime start = DateTime.Now; updateAction.Invoke(); Trace.TraceInformation("WHR " + category + " Ratings updated in " + DateTime.Now.Subtract(start).TotalSeconds + " seconds, " + (GC.GetTotalMemory(false) / (1 << 20)) + "MiB total memory allocated"); IEnumerable <Account> updatedRanks = new List <Account>(); using (var db = new ZkDataContext()) { var lastBattlePlayers = db.SpringBattlePlayers.Where(p => p.SpringBattleID == latestBattle.SpringBattleID && !p.IsSpectator).Include(x => x.Account).ToList(); if (latestBattle.GetRatingCategory() == category && lastBattleRanked) { lastBattleRanked = false; lastBattlePlayers.Where(p => playerOldRatings.ContainsKey(RatingSystems.GetRatingId(p.AccountID)) && !p.EloChange.HasValue).ForEach(p => { p.EloChange = playerRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo - playerOldRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo; }); updatedRanks = lastBattlePlayers.Where(p => Ranks.UpdateRank(p.Account, p.IsInVictoryTeam, !p.IsInVictoryTeam, db)).Select(x => x.Account).ToList(); updatedRanks.ForEach(p => db.Entry(p).State = EntityState.Modified); } db.SpringBattlePlayers.Where(p => p.SpringBattleID == latestBattle.SpringBattleID && !p.IsSpectator).ToList().ForEach(x => playerOldRatings[RatingSystems.GetRatingId(x.AccountID)] = playerRatings[RatingSystems.GetRatingId(x.AccountID)]); db.SaveChanges(); } RatingsUpdated(this, new RatingUpdate() { affectedPlayers = updatedRanks.Select(p => RatingSystems.GetRatingId(p.AccountID)) }); } } catch (Exception ex) { Trace.TraceError("Thread error while updating WHR " + category + " " + ex); } }, CancellationToken.None, TaskCreationOptions.None, PriorityScheduler.BelowNormal); lastUpdate = latestBattle; } }
//private //Runs in O(N log(N)) for all players private void UpdateRankings(IEnumerable <Player> players) { try { int currentDay = RatingSystems.ConvertDateToDays(DateTime.UtcNow); foreach (var p in players) { if (p.days.Count == 0) { Trace.TraceError("WHR " + category + " has invalid player " + p.id + " with no days(games)"); continue; } float elo = p.days.Last().getElo() + RatingOffset; float lastUncertainty = p.days.Last().uncertainty * 100; int lastDay = p.days.Last().day; playerRatings[p.id] = new PlayerRating(int.MaxValue, 1, elo, lastUncertainty, lastDay, currentDay); float rating = -playerRatings[p.id].Elo + 0.001f * (float)rand.NextDouble(); if (playerKeys.ContainsKey(p.id)) { sortedPlayers.Remove(playerKeys[p.id]); } playerKeys[p.id] = rating; sortedPlayers[rating] = p.id; } float[] playerUncertainties = new float[playerRatings.Count]; int index = 0; float DynamicMaxUncertainty = GlobalConst.MinimumDynamicMaxLadderUncertainty; int maxAge = GlobalConst.LadderActivityDays; foreach (var pair in playerRatings) { if (currentDay - pair.Value.LastGameDate > maxAge) { playerUncertainties[index++] = 9999 + index; //don't use infinity because i'm doing shady floating point things } else { playerUncertainties[index++] = (float)pair.Value.Uncertainty; } } Array.Sort(playerUncertainties); DynamicMaxUncertainty = Math.Max(DynamicMaxUncertainty, playerUncertainties[Math.Min(playerUncertainties.Length, GlobalConst.LadderSize) - 1] + 0.01f); int activePlayers = Math.Max(1, ~Array.BinarySearch(playerUncertainties, DynamicMaxUncertainty)); int rank = 0; List <int> newTopPlayers = new List <int>(); int matched = 0; List <float> newPercentileBrackets = new List <float>(); newPercentileBrackets.Add(3000); float percentile; float[] percentilesRev = Ranks.Percentiles.Reverse().ToArray(); foreach (var pair in sortedPlayers) { if (playerRatings[pair.Value].Uncertainty <= DynamicMaxUncertainty && currentDay - playerRatings[pair.Value].LastGameDate <= maxAge) { newTopPlayers.Add(pair.Value); if (rank == matched && rank < topPlayers.Count && topPlayers[rank] == pair.Value) { matched++; } rank++; percentile = (float)rank / activePlayers; if (newPercentileBrackets.Count <= Ranks.Percentiles.Length && percentile > percentilesRev[newPercentileBrackets.Count - 1]) { newPercentileBrackets.Add(playerRatings[pair.Value].Elo); } playerRatings[pair.Value].ApplyLadderUpdate(rank, percentile, currentDay); } else if (playerRatings[pair.Value].Rank < int.MaxValue) { playerRatings[pair.Value].ApplyLadderUpdate(int.MaxValue, 1, currentDay); } } this.activePlayers = rank; newPercentileBrackets.Add(0); PercentileBrackets = newPercentileBrackets.Select(x => x).Reverse().ToArray(); topPlayers = newTopPlayers; laddersCache = new List <Account>(); Trace.TraceInformation("WHR " + category + " Ladders updated with " + topPlayers.Count + "/" + this.players.Count + " entries, max uncertainty selected: " + DynamicMaxUncertainty + " brackets are now: " + string.Join(", ", PercentileBrackets)); var playerIds = players.Select(x => x.id).ToList(); if (playerIds.Count() < 100) { SaveToDB(playerIds); } else { SaveToDB(); } //check for rank updates List <int> playersWithRatingChange = new List <int>(); using (var db = new ZkDataContext()) { var lastBattlePlayers = db.SpringBattlePlayers.Where(p => p.SpringBattleID == latestBattle.SpringBattleID && !p.IsSpectator).Include(x => x.Account).ToList(); if (latestBattle.GetRatingCategory() == category && lastBattleRanked) { lastBattleRanked = false; lastBattlePlayers.Where(p => playerOldRatings.ContainsKey(RatingSystems.GetRatingId(p.AccountID)) && !p.EloChange.HasValue).ForEach(p => { p.EloChange = playerRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo - playerOldRatings[RatingSystems.GetRatingId(p.AccountID)].RealElo; }); var updatedRanks = lastBattlePlayers.Where(p => Ranks.UpdateRank(p.Account, p.IsInVictoryTeam, !p.IsInVictoryTeam, db)).Select(x => x.Account).ToList(); updatedRanks.ForEach(p => db.Entry(p).State = EntityState.Modified); playersWithRatingChange = lastBattlePlayers.Select(x => x.AccountID).ToList(); } db.SpringBattlePlayers.Where(p => p.SpringBattleID == latestBattle.SpringBattleID && !p.IsSpectator).ToList().ForEach(x => playerOldRatings[RatingSystems.GetRatingId(x.AccountID)] = playerRatings[RatingSystems.GetRatingId(x.AccountID)]); db.SaveChanges(); } if (latestBattle.GetRatingCategory() == category && lastBattleRanked) { //Publish new results only after saving new stats to db. RatingsUpdated(this, new RatingUpdate() { affectedPlayers = playersWithRatingChange }); } //check for topX updates GetTopPlayers(GlobalConst.LadderSize); foreach (var listener in topPlayersUpdateListeners) { if (matched < listener.Value) { listener.Key.TopPlayersUpdated(GetTopPlayers(listener.Value)); } } } catch (Exception ex) { string dbg = "WHR " + category + ": Failed to update rankings " + ex + "\nPlayers: "; foreach (var p in players) { dbg += p.id + " (" + p.days.Count + " days), "; } Trace.TraceError(dbg); } }
//private //Runs in O(N log(N)) for all players private void UpdateRankings(IEnumerable <Player> players) { try { int currentDay = RatingSystems.ConvertDateToDays(DateTime.UtcNow); foreach (var p in players) { if (p.days.Count == 0) { Trace.TraceError("WHR " + category + " has invalid player " + p.id + " with no days(games)"); continue; } float elo = p.days.Last().getElo() + RatingOffset; float lastUncertainty = p.days.Last().uncertainty * 100; int lastDay = p.days.Last().day; playerRatings[p.id] = new PlayerRating(int.MaxValue, 1, elo, lastUncertainty, lastDay, currentDay); float rating = -playerRatings[p.id].Elo + 0.001f * (float)rand.NextDouble(); if (playerKeys.ContainsKey(p.id)) { sortedPlayers.Remove(playerKeys[p.id]); } playerKeys[p.id] = rating; sortedPlayers[rating] = p.id; } float[] playerUncertainties = new float[playerRatings.Count]; int index = 0; float DynamicMaxUncertainty = GlobalConst.MinimumDynamicMaxLadderUncertainty; int maxAge = GlobalConst.LadderActivityDays; foreach (var pair in playerRatings) { if (currentDay - pair.Value.LastGameDate > maxAge) { playerUncertainties[index++] = 9999 + index; //don't use infinity because i'm doing shady floating point things } else { playerUncertainties[index++] = (float)pair.Value.Uncertainty; } } Array.Sort(playerUncertainties); DynamicMaxUncertainty = Math.Max(DynamicMaxUncertainty, playerUncertainties[Math.Min(playerUncertainties.Length, GlobalConst.LadderSize) - 1] + 0.01f); int activePlayers = Math.Max(1, ~Array.BinarySearch(playerUncertainties, DynamicMaxUncertainty)); int rank = 0; List <int> newTopPlayers = new List <int>(); int matched = 0; List <float> newPercentileBrackets = new List <float>(); newPercentileBrackets.Add(3000); float percentile; float[] percentilesRev = Ranks.Percentiles.Reverse().ToArray(); foreach (var pair in sortedPlayers) { if (playerRatings[pair.Value].Uncertainty <= DynamicMaxUncertainty && currentDay - playerRatings[pair.Value].LastGameDate <= maxAge) { newTopPlayers.Add(pair.Value); if (rank == matched && rank < topPlayers.Count && topPlayers[rank] == pair.Value) { matched++; } rank++; percentile = (float)rank / activePlayers; if (newPercentileBrackets.Count <= Ranks.Percentiles.Length && percentile > percentilesRev[newPercentileBrackets.Count - 1]) { newPercentileBrackets.Add(playerRatings[pair.Value].RealElo); } playerRatings[pair.Value].ApplyLadderUpdate(rank, percentile, currentDay); } else if (playerRatings[pair.Value].Rank < int.MaxValue) { playerRatings[pair.Value].ApplyLadderUpdate(int.MaxValue, 1, currentDay); } } this.activePlayers = rank; newPercentileBrackets.Add(0); PercentileBrackets = newPercentileBrackets.Select(x => x).Reverse().ToArray(); topPlayers = newTopPlayers; laddersCache = new List <Account>(); Trace.TraceInformation("WHR " + category + " Ladders updated with " + topPlayers.Count + "/" + this.players.Count + " entries, max uncertainty selected: " + DynamicMaxUncertainty); var playerIds = players.Select(x => x.id).ToList(); if (playerIds.Count() < 100) { SaveToDB(playerIds); } else { SaveToDB(); } //check for topX updates GetTopPlayers(GlobalConst.LadderSize); foreach (var listener in topPlayersUpdateListeners) { if (matched < listener.Value) { listener.Key.TopPlayersUpdated(GetTopPlayers(listener.Value)); } } RatingsUpdated(this, new RatingUpdate() { affectedPlayers = players.Select(x => x.id) }); } catch (Exception ex) { string dbg = "WHR " + category + ": Failed to update rankings " + ex + "\nPlayers: "; foreach (var p in players) { dbg += p.id + " (" + p.days.Count + " days), "; } Trace.TraceError(dbg); } }
public PlayerRating(int Rank, float Percentile, float Elo, float Uncertainty) : this(Rank, Percentile, Elo, Uncertainty, RatingSystems.ConvertDateToDays(DateTime.Now)) { }
private static void FillApplicableRatings(SpringBattle battle, SpringBattleContext result) { battle.ApplicableRatings = 0; if (battle.HasBots) { return; } if (battle.IsMission) { return; } if (battle.SpringBattlePlayers?.Where(x => !x.IsSpectator).Select(x => x.AllyNumber).Distinct().Count() < 2) { return; } if (battle.ResourceByMapResourceID?.MapIsSpecial == true) { return; } //only count balanced custom matches for elo if (battle.Mode == AutohostMode.None && battle.SpringBattlePlayers?.Where(x => !x.IsSpectator).GroupBy(x => x.AllyNumber).Select(x => x.Count()).Distinct().Count() > 1) { return; } if (battle.Duration < GlobalConst.MinDurationForElo) { return; } //don't mark battles for ratings if they can't be rated ICollection <int> winners = battle.SpringBattlePlayers.Where(p => p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).Distinct().ToList(); ICollection <int> losers = battle.SpringBattlePlayers.Where(p => !p.IsInVictoryTeam && !p.IsSpectator).Select(p => RatingSystems.GetRatingId(p.AccountID)).Distinct().ToList(); if (winners.Count == 0 || losers.Count == 0 || winners.Intersect(losers).Count() != 0) { return; } battle.ApplicableRatings |= (RatingCategoryFlags)result.LobbyStartContext.ApplicableRating; //Optionally add other flags here, like a casual or overall rating }