UNPKG

arpad

Version:

An implementation of the ELO Rating System

230 lines (195 loc) 6.52 kB
'use strict'; const DEFAULT_K_FACTOR = 32; /** * Instantiates a new ELO instance * * @param {Number|Object} k_factor The maximum rating change, defaults to 32 * @param {Number} min The minimum value a calculated rating can be * @param {Number} max Integer The maximum value a calculated rating can be */ module.exports = class ELO { /** * This is some magical constant used by the ELO system * * @link http://en.wikipedia.org/wiki/Elo_rating_system#Performance_rating */ static #PERF = 400; static #OUTCOME_LOST = 0; static #OUTCOME_TIED = 0.5; static #OUTCOME_WON = 1; #minimum = -Infinity; #maximum = Infinity; #k_factor = DEFAULT_K_FACTOR; constructor(k_factor, min, max) { if (k_factor) { this.#k_factor = k_factor; } if (typeof min !== 'undefined') { this.#minimum = min; } if (typeof max !== 'undefined') { this.#maximum = max; } } /** * Returns the K-factor depending on the provided rating * * @arg {Number} rating A players rating, e.g. 1200 * @return {Number} The determined K-factor, e.g. 32 */ getKFactor(rating) { let k_factor = null; if (typeof this.#k_factor === 'number') { return this.#k_factor; } if (!rating) { rating = 0; } if (this.#k_factor.default) { k_factor = this.#k_factor.default; } Object.keys(this.#k_factor).forEach((minimum_rating) => { let current_k_factor = this.#k_factor[minimum_rating]; if (minimum_rating <= rating) { k_factor = current_k_factor; } }); return k_factor; } /** * Returns the minimum acceptable rating value * * @return {Number} The minimum rating value, e.g. 100 */ getMin() { return this.#minimum; } /** * Returns the maximum acceptable rating value * * @return {Number} The maximum rating value, e.g. 2700 */ getMax() { return this.#maximum; } /** * When setting the K-factor, you can do one of three things. * Provide a falsey value, and we'll default to using 32 for everything. * Provide a number, and we'll use that for everything. * Provide an object where each key is a numerical lower value. * * @arg {Number|Object} k_factor The K-factor to use * @return {Object} The current object for chaining purposes */ setKFactor (k_factor) { if (k_factor) { this.#k_factor = k_factor; } else { this.#k_factor = DEFAULT_K_FACTOR; } return this; } /** * Sets the minimum acceptable rating * * @arg {Number} minimum The minimum acceptable rating, e.g. 100 * @return {Object} The current object for chaining purposes */ setMin(minimum) { this.#minimum = minimum; return this; } /** * Sets the maximum acceptable rating * * @arg {Number} maximum The maximum acceptable rating, e.g. 2700 * @return {Object} The current object for chaining purposes */ setMax(maximum) { this.#maximum = maximum; return this; } /** * Determines the expected "score" of a match * * @param {Number} rating The rating of the person whose expected score we're looking for, e.g. 1200 * @param {Number} opponent_rating the rating of the challening person, e.g. 1200 * @return {Number} The score we expect the person to recieve, e.g. 0.5 * * @link http://en.wikipedia.org/wiki/Elo_rating_system#Mathematical_details */ expectedScore(rating, opponent_rating) { const difference = opponent_rating - rating; return 1 / (1 + Math.pow(10, difference/ELO.#PERF)); } /** * Returns an array of anticipated scores for both players * * @param {Number} player_1_rating The rating of player 1, e.g. 1200 * @param {Number} player_2_rating The rating of player 2, e.g. 1200 * @return {Array} The anticipated scores, e.g. [0.25, 0.75] */ bothExpectedScores(player_1_rating, player_2_rating) { return [ this.expectedScore(player_1_rating, player_2_rating), this.expectedScore(player_2_rating, player_1_rating) ]; } /** * The calculated new rating based on the expected outcone, actual outcome, and previous score * * @param {Number} expected_score The expected score, e.g. 0.25 * @param {Number} actual_score The actual score, e.g. 1 * @param {Number} previous_rating The previous rating of the player, e.g. 1200 * @return {Number} The new rating of the player, e.g. 1256 */ newRating(expected_score, actual_score, previous_rating) { const difference = actual_score - expected_score; let rating = Math.round(previous_rating + this.getKFactor(previous_rating) * difference); if (rating < this.#minimum) { rating = this.#minimum; } else if (rating > this.#maximum) { rating = this.#maximum; } return rating; } /** * Calculates a new rating from an existing rating and opponents rating if the player won * * This is a convenience method which skips the score concept * * @param {Number} rating The existing rating of the player, e.g. 1200 * @param {Number} opponent_rating The rating of the opponent, e.g. 1300 * @return {Number} The new rating of the player, e.g. 1300 */ newRatingIfWon(rating, opponent_rating) { const odds = this.expectedScore(rating, opponent_rating); return this.newRating(odds, ELO.#OUTCOME_WON, rating); } /** * Calculates a new rating from an existing rating and opponents rating if the player lost * * This is a convenience method which skips the score concept * * @param {Number} rating The existing rating of the player, e.g. 1200 * @param {Number} opponent_rating The rating of the opponent, e.g. 1300 * @return {Number} The new rating of the player, e.g. 1180 */ newRatingIfLost(rating, opponent_rating) { const odds = this.expectedScore(rating, opponent_rating); return this.newRating(odds, ELO.#OUTCOME_LOST, rating); } /** * Calculates a new rating from an existing rating and opponents rating if the player tied * * This is a convenience method which skips the score concept * * @param {Number} rating The existing rating of the player, e.g. 1200 * @param {Number} opponent_rating The rating of the opponent, e.g. 1300 * @return {Number} The new rating of the player, e.g. 1190 */ newRatingIfTied(rating, opponent_rating) { const odds = this.expectedScore(rating, opponent_rating); return this.newRating(odds, ELO.#OUTCOME_TIED, rating); } };