PvP Matchmaking Algorithm
Ratings[edit]
At the heart of PvP matchmaking algorithm is the Glicko2 matchmaking rating (MMR). This rating, which is an approximation of your skill level, helps match you with other players with similar skill level. In addition to two core ratings (one for unranked and ranked arena), a rating is also kept for each profession, but the profession ratings are not currently used for matchmaking.
Glicko was chosen over its main alternative, Elo. Like Elo, Glicko tracks MMR for each player and updates that rating over time as you play the game. Glicko's main improvement over its predecessor is the inclusion of a ratings deviation (RD), which measures the reliability of the rating. By using RD, the matchmaking algorithm can compensate for players it has little or incomplete information about.
A volatility measurement is also included to indicate the degree of fluctuation in a player's rating. The higher the volatility, the more the rating fluctuates. Volatility changes over time in response to how you play the game. During periods of stability, your volatility should remain low, and reciprocally. The point of this is to allow the system to hone in on your appropriate rating as quickly as possible.
The system is also set up to increase your RD after periods of inactivity, just in case you're a little rusty. (See Ratings/@period and Ratings/@max-periods below).
Configuration[edit]
<Ratings> <Rating default="1200" min="100" max="5000" max-change="300" profession-ratio="0.0"/> <Deviation default="350" min="30" max="350" period="3d" max-periods="20"/> <Volatility default="0.06" min="0.04" max="0.08" system-constant="0.5"/> </Ratings> <Ratings type="Ranked" use-season="true" reset="2013-11-26T08:00:00-08:00" partial-reset="2015-01-27T16:30:00-08:00"> <Decay points="700" grace-period="3d" decay-period="1w" recover-per-game="1d"/> <Placement type="LinearScale" param1="1200" param2="0.5"/> </Ratings> <Ratings type="Unranked" reset="2013-11-26T08:00:00-08:00" partial-reset="2015-01-27T16:30:00-08:00"/>
Element (XPath) | Description |
---|---|
Ratings/@type | If specified, indicates this element contains per-type overrides. |
Ratings/@use-season | Season games use their own instance of this rating instead of the account-wide version. |
Ratings/@reset | Any ratings data with a timestamp before this date is reset to the default. |
Ratings/@partial-reset | Any ratings data with a timestamp before this date is partially (deviation only) reset to the default. |
Rating/@default | Default rating that is used when no data is available. |
Rating/@min | Minimum rating allowed, anything below will be clamped to this value. |
Rating/@max | Maximum rating allowed, anything above will be clamped to this value. |
Rating/@max-change | The maximum, absolute amount a rating can change from a single game. |
Rating/@profession-ratio | Controls the balance between profession rating and core rating. '0' means only the core rating is used, while '1' means only the profession rating is used. |
Deviation/@default | Default ratings deviation that is used when no data is available. |
Deviation/@min | Minimum deviation allowed, anything below will be clamped to this value. |
Deviation/@max | Maximum deviation allowed, anything above will be clamped to this value. |
Deviation/@period | Length of inactivity for a single period. |
Deviation/@max-periods | Number of periods of inactivity before a player's deviation to go from the minimum to the maximum amount. |
Volatility/@default | Default ratings volatility that is used when no data is available. |
Volatility/@min | Minimum volatility allowed, anything below will be clamped to this value. |
Volatility/@max | Maximum volatility allowed, anything above will be clamped to this value. |
Volatility/@system-constant | Glicko2 tuning parameter that constrains the change in volatility over time. |
Decay/@points | The maximum amount of decay subtracted from effective rating. |
Decay/@decay-period | The amount of time before the maximum decay value is reached. |
Decay/@grace-period | The amount of time before decay starts affected the rating. |
Decay/@recover-per-game | The amount of time removed from active decay per PvP game with this rating. |
Placement/@type | Formula to use when calculated new season ratings from previous |
Placement/@param1 | Configuration for placement formula. In the case of "LinearScale", param1 is the center point to scale ratings towards. |
Placement/@param2 | Configuration for placement formula. In the case of "LinearScale", param2 is the ratio to scale ratings with. |
Matchmaking[edit]
Matchmaking is the process of organizing players in such a way as to encourage competitive and fun gameplay. The system uses a two-phase, score-based search method that takes into consideration several metrics. A score-based search method was used over other methods because it's a good compromise between the often competing goals of match quality and short wait times.
At the start of matchmaking, the system attempts to find a match customized for the first Filter/Iteration/@rosters rosters (party) in the queue. If no match can be created, these players will be put at the end of the queue to ensure other players have a chance at a match customized for them. While this may seem unfair at first, this has actually been shown to decrease wait times for all players.
The first phase, called filtering, gathers players based on their current MMR. The primary purpose of this phase is to both reduce the number of players being considered for a match, and to ensure that the match is appropriate given each player's skill level. Over time, padding is added to your player rating. While this may decrease match quality, it helps ensure that outliers still receive matches.
The second phase of the algorithm is the scoring phase. During this phase each player is scored against every other player being considered for matchmaking. The metrics used during this phase include: rating, rank, games played, party size, profession, and dishonor. With each metric the system is looking for players that are as close as possible to the average of those already selected. The system also attempts to keep the number of duplicate professions to a minimum.
Configuration[edit]
<Arena name="Unranked Arena"> <Queue> <RosterSize min="1" max="5"/> <Iteration interval="30s" rosters="50" limit="50ms"/> <Potentials min="20" max="500" falloff="0.16" start="1m" end="3m"/> <Rating start="3m" end="10m" max="1200" min="25"/> <Power curve="1" percent="1"/> <Rank min="0"/> </Queue> <Matcher type="Team"> <Age seconds="2"/> <RosterSize max-diff="3" distance="-100" perfect-fit="0"/> <Rank distance="0"/> <Rating distance="-10"/> <Profession max="2" common="-100" unique="0" matching="100"/> <Dishonor distance="-100" stack="-50"/> <GuildTeam affinity="50"/> <Games max="500" distance="-0.25"/> </Matcher > </Arena> <Arena name="Ranked Arena"> <Queue> <RosterSize min="1" max="2"/> <Iteration interval="30s" rosters="100" limit="250ms"/> <Potentials min="20" max="500" falloff="0.375" start="1m" end="3m"/> <Rating start="5m" end="10m" max="1200" min="25"/> <Power curve="1" percent="1"/> <Rank min="20"/> </Queue> <Matcher type="Team"> <Age seconds="2"/> <RosterSize max-diff="3" distance="-100" perfect-fit="0"/> <Rank distance="0"/> <Rating distance="-10"/> <Profession max="2" common="-100" unique="0" matching="100"/> <Dishonor distance="-100" stack="-50"/> <GuildTeam affinity="50"/> <Games max="500" distance="-0.25"/> </Matcher> </Arena> <Arena name="Ranked Arena Off-Season"> <Queue> <RosterSize min="1" max="5"/> <Iteration interval="30s" rosters="100" limit="250ms"/> <Potentials min="20" max="500" falloff="0.375" start="1m" end="3m"/> <Ladder start="0ms" end="0ms" max="0" min="0"/> <Rating start="5m" end="10m" max="1200" min="25"/> <Power curve="1" percent="1"/> <Rank min="20"/> </Queue> <Matcher type="Team"> <Age seconds="2"/> <RosterSize max-diff="3" distance="-100" perfect-fit="0"/> <Rank distance="0"/> <Rating distance="-10"/> <Profession max="2" common="-100" unique="0" matching="100"/> <Dishonor distance="-100" stack="-50"/> <GuildTeam affinity="50"/> <Games max="500" distance="-0.25"/> </Matcher> </Arena>
Element (XPath) | Description |
---|---|
Filter/RosterSize/@min | The minimum number of players that must be in a roster in order to queue. |
Filter/RosterSize/@max | The maximum number of players that must be in a roster in order to queue. |
Filter/Iteration/@rosters | The number of rosters the filtering phase will try to form custom matches for. |
Filter/Iteration/@limit | The maximum amount of time the server should spend trying to create matches per iteration. This is a performance fail-safe to keep the server responsive. |
Filter/Iteration/@interval | The time interval between matchmaking attempts. |
Filter/Potentials/@min | The minimum number of rosters that must pass the filter phase before attempting to create a match. |
Filter/Potentials/@max | The maximum number of rosters the filter phase should gather. This is a performance fail-safe to keep the server responsive. |
Filter/Potentials/@falloff | The number of potentials @max should be reduced by per-second. |
Filter/Potentials/@start | The amount of time that must pass before the @max potential @falloff begins. |
Filter/Potentials/@end | The amount of time that must pass before the @max potential @falloff ends. |
Filter/Rating/@padding | Padding is added every second you wait in the queue after Filter/Rating/@start has passed. This is an outlier fail-safe to ensure everyone gets a match.
|
Filter/Rating/@start | No padding is added to a roster's ratings range until this length of time has passed. |
Filter/Rating/@end | No additional padding will be added after this length of time has passed. This is a fail-safe to prevent match quality from degrading further than preferred. |
Filter/Rating/@Min | The maximum rating difference between rosters the filter starts at. |
Filter/Rating/@Max | The maximum rating difference between rosters that can exist after padding is applied. |
Filter/Power/@percent | The percent a roster's rating is inflated due to the number of players in the roster. |
Filter/Power/@curve | The exponent used to curve the effect the number of players has on the roster's power inflation. |
Scoring/@type | The type of scoring algorithm to use. Team will score rosters on a per-team basis, i.e. will only check for duplicate profession on the team, not the entire match.
|
Scoring/Age/@seconds | Score added or removed for every second a roster has been waiting. Outlier fail-safe to ensure no one waits too long. |
Scoring/GuildTeam/@affinity | Score added when comparing two guild teams for a potential match. |
Scoring/RosterSize/@max-diff | The maximum allowed roster size difference between teams. |
Scoring/RosterSize/@distance | Score added or removed based on the distance between the potential roster's size and the max roster size of all selected rosters, including both teams. |
Scoring/RosterSize/@perfect-fit | Score added or removed if the potential roster's size matches the exact number of players needed to fill empty spots on the team. |
Scoring/Rank/@distance | Score added or removed based on the distance between the potential roster's average rank and the average rank of all selected rosters, including both teams. |
Scoring/Rating/@distance | Score added or removed based on the distance between the potential roster's average effective rating (i.e. rating - deviation) and the average effective rating of all selected rosters, including both teams. |
Scoring/Profession/@max | Target maximum number of professions that should exist on a team. |
Scoring/Profession/@unique | Score added or removed for each unique profession, under Scoring/Profession/@max , the potential roster would add to the team.
|
Scoring/Profession/@common | Score added or removed for each duplicate profession, at or above Scoring/Profession/@max , the potential roster would add to the team.
|
Scoring/Profession/@matching | Score added or removed each professions that the other team has more of. This promotes profession balance. |
Scoring/Dishonor/@distance | Score added or removed based on the distance between the potential roster's total dishonor and the total dishonor of all selected rosters, including both teams. |
Scoring/Dishonor/@stack | Score added or removed per stack of dishonor the potential roster has on any of its members. |
Scoring/Games/@distance | Score added or removed per difference in games played. |
Scoring/Games/@max | The maximum difference in games before to consider for adjust score. |
Pseudo-Code (New February 7th 2017)[edit]
A new matchmaker has been written to solve some of the failings of the previous while maintaining a similar flow. This new matcher will score rosters against both teams and the entire match instead of only considering alternating target teams. This is most notable when scoring ratings as a roster's fit is based on how it will balance team ratings instead of just how close it is to the target team's rating. One additional scoring parameter includes a bonus for balancing profession counts.
def createMatches(queue, config): rosters = queue failed = [] # try to make a match for each roster in the queue while len(rosters) > 0: roster = rosters.pop() queue.remove(roster) if not tryMakeMatch(roster, queue, config): failed.append(roster) # move rosters we couldn't find match for to the end of the queue queue.append(failed) def tryMakeMatch(target, queue, config): # gather rosters that are good potential matches potentials = gatherPotentials(target, queue, config) # enforcing a minimum allows some flexibility in choices if len(potentials) < config.potentials.min: return False team1 = [] team2 = [] match = [] team1.append(target) match.append(target) while len(match) < config.teamSize * 2: bestRoster = None bestTeam1Score = -infinity bestTeam2Score = -infinity for roster in potentials: # score the roster against team1 and the match team1Score = -infinity if canJoinTeam(roster, team1, team2, config): team1Score = scoreRoster(roster, team1, team2, match, config) # score the roster against team2 and the match team2Score = -infinity if canJoinTeam(roster, team2, team1, config): team2Score = scoreRoster(roster, team2, team1, match, config) # found a better roster! if bestRoster is None or max(team1Score, team2Score) > max(bestTeam1Score, bestTeam2Score): bestRoster = roster bestTeam1Score = team1Score bestTeam2Score = team2Score # could not find any roster for the match, abort if bestRoster is None: return False # add this player to whichever team is a better fit match.append(bestRoster) if bestTeam1Score > bestTeam2Score: team1.append(bestRoster) else team2.append(bestRoster) queue.remove(team1) queue.remove(team2) createMatch(team1, team2) return True def gatherPotentials(queue, target, config): potentials = [] for roster in queue: # check conditions where rosters are never allowed to match if roster.gameModes != target.gameModes: continue if roster.rating > target.ratingHigh: continue if roster.rating < target.ratingLow: continue potentials.append(roster) # limit choices for performance if len(potentials) >= config.filter.potentials.max: break return potentials def canJoinTeam(roster, team, otherTeam, match, config): # roster is too big for this team if len(team) + len(roster) > config.teamSize: return False # don't pick rosters that are much different size than what exists if abs(len(roster) - otherTeam.maxRosterSize) > config.rosterSize.maxDiff: return False return True def scoreRoster(roster, team, otherTeam, match, config): score = 0 # adjust score by time queued score += roster.age * config.age.seconds # adjust score by games played difference distance = abs(match.averageGames - roster.games) score += distance * config.rating.distance # adjust score by rank difference distance = abs(match.averageRank - roster.rank) score += distance * config.rank.distance # adjust score by roster size difference distance = abs(len(roster) - otherTeam.maxRosterSize) score += distance * config.rosterSize.distance # adjust score by POTENTIAL rating difference distance = abs(team.ratingWithRoster(roster) - otherTeam.rating) score += distance * config.rating.distance # adjust score by profession counts for profession in allProfessions: # roster has none of these professions if roster.count(profession) == 0: continue # too many of the same profession totalCount = roster.count(profession) + team.count(profession) if totalCount > config.professions.max: score += (totalCount - config.professions.max) * config.professions.common # otherwise favor the variety elif team.count(profession) == 0: score += config.professions.unique # favor matching professions between teams if team.count(profession) < otherTeam.count(profession): score += config.professions.matching return score
Pseudo-Code (Old)[edit]
def createMatches(queue, config): rosters = queue failed = [] # try to make a match for each roster in the queue while len(rosters) > 0: roster = rosters.pop() queue.remove(roster) if not tryMakeMatch(roster, queue, config): failed.append(roster) # move rosters we couldn't find match for to the end of the queue queue.append(failed) def tryMakeMatch(target, queue, config): # gather rosters that are good potential matches potentials = gatherPotentials(target, queue, config) # enforcing a minimum allows some flexibility in choices if len(potentials) < config.potentials.min: return False team1 = [] team2 = [] team1.append(target) # track the biggest roster for scoring purposes maxRosterSize = len(target) while len(team1) < config.teamSize or len(team2) < config.teamSize: # populate smaller team first if len(team1) > len(team2): swap(team1, team2) # otherwise pick a random team if they're equal elif len(team1) == len(team2) and randomChoice(): swap(team1, team2) # pick the best roster for the team we've built so far bestRoster = pickBestRoster(team1, maxRosterSize, potentials, config) # could not find any roster for this team if bestRoster is None: break maxRosterSize = max(maxRosterSize, len(bestRoster)) potentials.remove(bestRoster) team1.append(bestRoster) # could not fill up both teams if len(team1) < config.teamSize or len(team2) < config.teamSize: return False queue.remove(team1) queue.remove(team2) createMatch(team1, team2) return True def gatherPotentials(queue, target, config): potentials = [] for roster in queue: # check conditions where rosters are never allowed to match if roster.gameModes != target.gameModes: continue if roster.ratingLow > target.ratingHigh: continue if roster.ratingHigh < target.ratingLow: continue potentials.append(roster) # limit choices for performance if len(potentials) >= config.filter.potentials.max: break return potentials def pickBestRoster(team, maxRosterSize, potentials, config): bestRoster = None bestScore = -infinity playersNeeded = config.teamSize - len(team) for roster in potentials: # roster is too big for this team if len(roster) > playersNeeded: continue # don't pick rosters that are much bigger than what's chosen so far if abs(len(roster) - maxRosterSize) > config.rosterSize.maxDiff: continue # how well does this roster match the team score = scoreRoster(roster, team, maxRosterSize, config) # found a better roster! if bestRoster is None or bestScore < score: bestRoster = roster bestScore = score return best def scoreRoster(roster, team, maxRosterSize, config): score = 0 # adjust score by time queued score += roster.age * config.age.seconds # adjust score by rating difference distance = abs(team.averageRating - roster.rating) score += distance * config.rating.distance # adjust score by games played difference distance = abs(team.averageGames - roster.games) score += distance * config.rating.distance # adjust score by rank difference distance = abs(team.averageRank - roster.rank) score += distance * config.rank.distance # adjust score by roster size difference distance = abs(maxRosterSize - len(roster)) score += distance * config.rosterSize.distance # adjust score by profession counts for profession in allProfessions: # roster has none of these professions if roster.count(profession) == 0: continue # too many of the same profession totalCount = roster.count(profession) + team.count(profession) if totalCount > config.professions.max: score += (totalCount - config.professions.max) * config.professions.common # otherwise favor the variety elif team.count(profession) == 0: score += config.professions.unique return score
Dishonor[edit]
Dishonor is one of the methods used to encourage good sportsmanship. Behavior is tracked in the medium to long range through stacks. Each stack represents a duration that decays over time. Every time you receive dishonor you also receive a timeout. The length of a timeout increases exponentially based on how many stacks you currently have. In other words, your first offense may yield a short timeout, while your 20th may keep you from playing for a much longer amount of time.
While in timeout, you may not participate in ranked or unranked arena, but you can still play in custom arenas.
Dishonor also impacts matchmaking by preferring to place you with other players that also have dishonor. This isn't a separate queue, but merely a suggestion to the matchmaking system.
It's possible to have stacks of dishonor without having an active timeout because dishonor decays at a much slower rate.
Configuration[edit]
<Dishonor stack-duration="4h" timeout-duration="30s" timeout-exponent="1.5" timeout-rounding="1m"> <Penalty reason="Abandon" stacks="10"/> <Penalty reason="QueueDodge" stacks="3"/> <Penalty reason="Banned" stacks="1000000"/> </Dishonor>
Element (XPath) | Description |
---|---|
Dishonor/@stack-duration | Length of time a single stack of dishonor will last before expiring. |
Dishonor/@timeout-duration | Length of time, per stack of dishonor to the power of @timeout-exponent , to add to the player's timeout.
|
Dishonor/@timeout-exponent | Exponent used when multiplying dishonor stacks by Dishonor/@timeout .
|
Dishonor/@timeout-rounding | Additional timeout is rounded to the nearest @timeout-rounding interval before being added to the player's timeout. Also acts as the minimum length of timeout that can be earned.
|
Penalty/@reason | Reason the dishonor is being awarded. Abandon: leaving a match before the end. QueueDodge: leaving or failing to confirm you're ready for a match. Banned: When a GM determines you may no longer participate in ranked or unranked arena. |
Penalty/@stacks | Number of stacks of dishonor to add to the player. |
Pseudo-Code[edit]
def roundInterval(value, interval): return max(interval, round(value / interval) * interval) def applyDishonor(player, penalty, config): player.stacks += penalty.stacks newTimeout = pow(player.stacks, config.timeoutExponent) * config.timeoutDuration player.timeout += roundInterval(newTimeout, config.timeoutRounding)
Ladder (Deprecated)[edit]
As of 12/13/2016, this feature is no longer used.
The ladder is a list of all players currently participating in competitive play. Your ladder ranking is determined by how many points you are awarded throughout a season.
You are awarded points for playing well, and often, and sometimes even if you lose a game. Even if a comeback may not seem possible, you can still be rewarded for continuing to try your very best. If you find yourself in an uneven match, fear not, you will risk fewer points for losing, and have more points to gain for doing well. Likewise, if you participate in an easy match, don't think you're home free. Performing well might award you points, but performing poorly will take even more away.
(See Match Prediction for more information on how the system determines your odds of victory.)
Configuration[edit]
<PointRules Mode="Conquest"> <PointRule Odds="0"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="0" ScoreRatio="0.4"/> <Checkpoint LadderPoints="1" ScoreRatio="0.6"/> <Checkpoint LadderPoints="2" ScoreRatio="0.8"/> <Checkpoint LadderPoints="3" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.2"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="0" ScoreRatio="0.6"/> <Checkpoint LadderPoints="1" ScoreRatio="0.8"/> <Checkpoint LadderPoints="2" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.4"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.6"> <Checkpoint LadderPoints="-2" ScoreRatio="0"/> <Checkpoint LadderPoints="-1" ScoreRatio="0.6"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.8"> <Checkpoint LadderPoints="-3" ScoreRatio="0"/> <Checkpoint LadderPoints="-2" ScoreRatio="0.4"/> <Checkpoint LadderPoints="-1" ScoreRatio="0.6"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> </PointRules> <PointRules Mode="Stronghold"> <PointRule Odds="0"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="0" ScoreRatio="0.2"/> <Checkpoint LadderPoints="1" ScoreRatio="0.4"/> <Checkpoint LadderPoints="2" ScoreRatio="0.6"/> <Checkpoint LadderPoints="3" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.2"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="0" ScoreRatio="0.4"/> <Checkpoint LadderPoints="1" ScoreRatio="0.6"/> <Checkpoint LadderPoints="2" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.4"> <Checkpoint LadderPoints="-1" ScoreRatio="0"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.6"> <Checkpoint LadderPoints="-2" ScoreRatio="0"/> <Checkpoint LadderPoints="-1" ScoreRatio="0.4"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> <PointRule Odds="0.8"> <Checkpoint LadderPoints="-3" ScoreRatio="0"/> <Checkpoint LadderPoints="-2" ScoreRatio="0.2"/> <Checkpoint LadderPoints="-1" ScoreRatio="0.4"/> <Checkpoint LadderPoints="1" ScoreRatio="1"/> </PointRule> </PointRules> <!-- Season begins Friday March 20, 2015 @ 9 am PDT --> <!-- Season ends Thursday May 14, 2015 @ 9 am PDT --> <Ladder start="2015-03-20T09:00:00-07:00" end="2015-05-14T09:00:00-07:00" grace="60"/>
Element (XPath) | Description |
---|---|
Ladder/@start | Any game results that occur before this date are not included on the ladder. |
Ladder/@end | Any game results that occur on or after this date are not included on the ladder. |
Ladder/@grace | Grace period (in minutes) before the end of the season where new season games cannot begin. |
Ladder/@leaderboard-points | Minimum number of points required before the player's data will be submitted to the Leaderboard. |
PointRules/@Mode | The game mode these point rules are used for. |
PointRule/@Odds | Minimum odds threshold. Paired with Checkpoint/@ScoreRatio to determine the number of ladder points to award.
|
Checkpoint/@ScoreRatio | Minimum score threshold required to earn Checkpoint/@LadderPoints ladder points. Score ratio is relative to the winning score (usually 500).
|
Checkpoint/@LadderPoints | Number of points to award if both PointRule/@odds and Checkpoint/@ScoreRatio thresholds are met. Only the highest thresholds count.
|
Pseudo-Code[edit]
def getPoints(oddsOfVictory, finalScore, config): currentDate = Time.now() if currentDate < config.startDate or currentDate >= config.endDate: return 0 bestMatrix = null for matrix in config.ladderMatrix: if matrix.odds > oddsOfVictory: continue if bestMatrix is null or matrix.odds > bestMatrix.odds: bestMatrix = matrix bestScore = null for score in bestMatrix.scores: if score.min > finalScore: continue if bestScore is null or score.min > bestScore.min bestScore = score return bestScore.points def processGame(player, game, config): oddsOfVictory = predictionToOddsOfVictory(game.prediction, player.team) finalScore = game.score[player.team] if game.result == 'desertion': finalScore = 0 prevPoints = player.ladderPoints pointsAwarded = getPoints(oddsOfVictory, finalScore, config) if game.result == 'victory': pointsAwarded = max(1, pointsAwarded) player.ladderPoints += pointsAwarded if player.ladderPoints >= config.leaderboardPoints: sendLeaderboardUpdate(config.leaderboard, player.id, player.ladderPoints) else if prevPoints >= config.leaderboardPoints: sendLeaderboardRemove(config.leaderboard, player.id)
Match Prediction (Deprecated)[edit]
As of 12/13/2016, this feature is no longer used.
The system attempts to predict the outcome of a match with the same metrics used in matchmaking, though can be configured separately.
Configuration[edit]
<Prediction> <Ladder method="Spread" spread="30" weight="5"/> <Rank method="Spread" spread="40" weight="1"/> <Rating method="Spread" spread="200" weight="0"/> <Roster method="Spread" spread="4" weight="2"/> </Prediction>
Element (XPath) | Description |
---|---|
Prediction/*/@method | Specifies which calculation method to use. |
Prediction/*/@spread | Maximum spread to calculate. |
Prediction/*/@weight | The impact, with higher numbers indicating more impact, this calculation has on the final prediction. |
Prediction/Ladder | If present, each team's average ladder is used for the calculation. |
Prediction/Rank | If present, each team's average rank is used for the calculation. |
Prediction/Rating | If present, each team's average effective rating (i.e. rating - deviation) is used for the calculation. |
Prediction/Roster | If present, each team's maximum roster size is used for the calculation. |
Pseudo-Code[edit]
# Return the spread between two values normalized to -1..1. def calculateSpread (red, blue, maxSpread): spread = (blue - red) / maxSpread return clamp(spread, -1, +1); # Returns a prediction value between -1 and 1, where -1 means red dominate, # and +1 means blue dominate. def predict(red, blue, config): ladder = calculateSpread(red.averageLadder, blue.averageLadder, config.ladderSpread) * config.ladderWeight rank = calculateSpread(red.averageRank, blue.averageRank, config.rankSpread) * config.rankWeight rating = calculateSpread(red.averageRatingLow, blue.averageRatingLow, config.ratingSpread) * config.ratingWeight roster = calculateSpread(red.maxRosterSize, blue.maxRosterSize, config.rosterSpread) * config.rosterWeight totalScore = ladder + rank + rating + roster totalWeight = config.ladderWeight + config.rankWeight + config.ratingWeight + config.rosterWeight return clamp(totalScore / totalWeight, -1, +1) # Returns the team's odds of victory as a ratio of 0..1, where 0 means # minimal chance of victory and 1 means minimal chance of defeat. def predictionToOddsOfVictory (prediction, team): normalized = prediction / 2 + 0.5; if team == 'red': return 1 - normalized else: return normalized
References[edit]
- ^ Finding the perfect match, GuildWars2.com