UNPKG

toefungi-elo-calculator

Version:

A package to do all necessary computations to determine ELO rankings

187 lines (165 loc) 6.78 kB
import { RelativeRank } from './types/RelativeRank' import { ScoringBonus } from './enums/ScoringBonus' import { Probabilities } from './types/Probabilities' /** * https://metinmediamath.wordpress.com/2013/11/27/how-to-calculate-the-elo-rating-including-example/ * * EloCalculator class to do calculations to determine the elo of a player after a match. */ class EloCalculator { public readonly kFactor: number = 32 public readonly shouldRound: boolean /** * Constructor method for the EloCalculator class. * * @param {boolean} shouldRound Whether or not the new ELO should be rounded before being returned. * @param {number} kFactor The factor the new ELO is calculated against. */ constructor(shouldRound: boolean = true, kFactor: number = 32) { this.kFactor = kFactor this.shouldRound = shouldRound } /** * @private * * Takes a current ELO score and will convert it to it's equivalent base10 value which can be used * to determine ELO for a win, loss or draw. * * @param {number} elo An ELO score to be converted to base10. * @return {Promise<number>} The equivalent base10 rank of the given ELO. */ private convertEloToBase10(elo: number): Promise<number> { const divide = () => elo / 400 const convertToBase10 = (value: number) => Math.pow(10, value) return Promise.resolve() .then(divide) .then(convertToBase10) } /** * @private * * Calculates the base10 equivalent value of each player's ELO score which can then be used to determine the new ELO * score for the player. * * @param {number} playerElo The initial ELO of the player. * @param {number} opponentElo The initial ELO of the opponent. * @return {Promise<RelativeRank>} An object containing the equivalent base10 value of both player's ELO score. */ private determineRelativeRank(playerElo: number, opponentElo: number): Promise<RelativeRank> { return Promise.all([ this.convertEloToBase10(playerElo), this.convertEloToBase10(opponentElo) ]).then(([ player, opponent ]: [ number, number ]) => ({ player, opponent })) } /** * @private * * Determines the scoring factor of the player based on their rank against their opponents rank. * * @param {number} playerRank The base10 value of the player's ELO score. * @param {number} opponentRank The base10 value of the opponent's ELO score. * @return {Promise<number>} The determined scoring factor. */ private determineScoreFactor(playerRank: number, opponentRank: number): Promise<number> { const add = () => playerRank + opponentRank const divide = (value: number) => playerRank / value return Promise.resolve() .then(add) .then(divide) } /** * @private * * Determines the new ELO of a player based on the score factor, a win or loss and current ELO score. * * @param {number} playerElo A player's current ELO score. * @param {number} scoringFactor A factor derived from the player's relative base10 ELO score with their opponent's . * @param {ScoringBonus} score Enum with [ WIN, LOSS, DRAW ]. * @param {number} scoreDiff The difference in the score as a number. * @return {Promise<number>} The new calculated ELO. */ private determineElo(playerElo: number, scoringFactor: number, score: ScoringBonus, scoreDiff?: number): Promise<number> { const getFactor = (difference?: number): number => { if (!difference) { return this.kFactor } return Math.log(Math.abs(difference) + 1) * this.kFactor } const subtract = () => score - scoringFactor const add = (value: number) => playerElo + value const multiply = (value: number) => value * getFactor(scoreDiff) return Promise.resolve() .then(subtract) .then(multiply) .then(add) } /** * @private * * Rounds off the ELO score before it is returned. * * @param {number} elo A value representing the ELO. * @return {Promise<number>} The rounded off ELO value. */ private rounding(elo: number): Promise<number> { return Promise.resolve() .then(() => { if (!this.shouldRound) { return elo } return Math.round(elo) }) } /** * @private * * Creates a probability object which will be returned to the user. * * @param {number} probability The probability of the `player` to win the game. * @return {Promise<Probabilities>} The probability of a win for either player based on the `player` ELO. */ private createProbability(probability: number): Promise<Probabilities> { return Promise.resolve() .then(() => { const player = Math.round(probability * 100) const opponent = Math.round((1 - probability) * 100) return { player, opponent } }) } /** * Calculate the ELO of a player after a match based on their opponent's ELO and whether or not the player won the game. * * @param {number} playerElo The ELO of the player. * @param {number} opponentElo The ELO of the opponent. * @param {ScoringBonus} score The outcome of the game for the player, enum with [ WIN, LOSS, DRAW ]. * @param {number} scoreDiff The difference in the score as a number. * @return {Promise<number>} The new ELO of the player. */ public calculateElo(playerElo: number, opponentElo: number, score: ScoringBonus, scoreDiff?: number): Promise<number> { return Promise.resolve() .then(() => this.determineRelativeRank(playerElo, opponentElo)) .then((relativeRank: RelativeRank) => this.determineScoreFactor(relativeRank.player, relativeRank.opponent)) .then((scoringFactor: number) => this.determineElo(playerElo, scoringFactor, score, scoreDiff)) .then((elo: number) => this.rounding(elo)) } /** * Calculate the win probability for players in a match up given their respective ELO's. * * @param {number} playerElo The ELO of the player. * @param {number} opponentElo The ELO of the opponent. * @return {Promise<Probabilities>} The win probability for each player respectively. */ public caluclateWinProbability(playerElo: number, opponentElo: number): Promise<Probabilities> { const determineDiff = (): number => opponentElo - playerElo const determineProbability = (elo: number): number => 1 / (1 + elo) return Promise.resolve() .then(determineDiff) .then(this.convertEloToBase10) .then(determineProbability) .then(this.createProbability) } } export { EloCalculator }