From 3d0808e89c6fea1e925ef10daf544354d15650a6 Mon Sep 17 00:00:00 2001 From: Dylan Sprague Date: Thu, 28 Jan 2021 22:40:45 -0500 Subject: [PATCH 1/5] Server: Move Elo calculation into separate file --- server/elo.ts | 27 +++++++++++++++++++++++++++ server/ladders-local.ts | 26 ++------------------------ 2 files changed, 29 insertions(+), 24 deletions(-) create mode 100644 server/elo.ts diff --git a/server/elo.ts b/server/elo.ts new file mode 100644 index 000000000000..b8d66f927f0a --- /dev/null +++ b/server/elo.ts @@ -0,0 +1,27 @@ +export function calculateElo(previousUserElo: number, score: number, foeElo: number): number { + // The K factor determines how much your Elo changes when you win or + // lose games. Larger K means more change. + // In the "original" Elo, K is constant, but it's common for K to + // get smaller as your rating goes up + let K = 50; + + // dynamic K-scaling (optional) + if (previousUserElo < 1200) { + if (score < 0.5) { + K = 10 + (previousUserElo - 1000) * 40 / 200; + } else if (score > 0.5) { + K = 90 - (previousUserElo - 1000) * 40 / 200; + } + } else if (previousUserElo > 1350 && previousUserElo <= 1600) { + K = 40; + } else { + K = 32; + } + + // main Elo formula + const E = 1 / (1 + Math.pow(10, (foeElo - previousUserElo) / 400)); + + const newElo = previousUserElo + K * (score - E); + + return Math.max(newElo, 1000); +} diff --git a/server/ladders-local.ts b/server/ladders-local.ts index ec24da06198a..d0a59cd63533 100644 --- a/server/ladders-local.ts +++ b/server/ladders-local.ts @@ -15,6 +15,7 @@ import {FS} from '../lib/fs'; import {Utils} from '../lib/utils'; +import { calculateElo } from './elo'; // ladderCaches = {formatid: ladder OR Promise(ladder)} // Use Ladders(formatid).ladder to guarantee a Promise(ladder). @@ -172,30 +173,7 @@ export class LadderStore { updateRow(row: LadderRow, score: number, foeElo: number) { let elo = row[1]; - // The K factor determines how much your Elo changes when you win or - // lose games. Larger K means more change. - // In the "original" Elo, K is constant, but it's common for K to - // get smaller as your rating goes up - let K = 50; - - // dynamic K-scaling (optional) - if (elo < 1200) { - if (score < 0.5) { - K = 10 + (elo - 1000) * 40 / 200; - } else if (score > 0.5) { - K = 90 - (elo - 1000) * 40 / 200; - } - } else if (elo > 1350 && elo <= 1600) { - K = 40; - } else { - K = 32; - } - - // main Elo formula - const E = 1 / (1 + Math.pow(10, (foeElo - elo) / 400)); - elo += K * (score - E); - - if (elo < 1000) elo = 1000; + elo = calculateElo(elo, score, foeElo); row[1] = elo; if (score > 0.6) { From c5706348ee10465405915aedf31c782b38bd6959 Mon Sep 17 00:00:00 2001 From: Dylan Sprague Date: Thu, 28 Jan 2021 23:02:55 -0500 Subject: [PATCH 2/5] Server: Calculate Elo for display before updating ladder on loginserver --- server/ladders-remote.ts | 74 ++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/server/ladders-remote.ts b/server/ladders-remote.ts index 72ecdefee9fb..d1a77db9638d 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -12,6 +12,7 @@ * @license MIT */ import {Utils} from '../lib/utils'; +import { calculateElo } from './elo'; export class LadderStore { formatid: string; @@ -70,25 +71,48 @@ export class LadderStore { const formatid = this.formatid; const p1 = Users.getExact(p1name); const p2 = Users.getExact(p2name); - room.update(); - room.send(`||Ladder updating...`); - const [data, error] = await LoginServer.request('ladderupdate', { + + + const updatePromise = LoginServer.request('ladderupdate', { p1: p1name, p2: p2name, score: p1score, format: formatid, }); + + + // calculate new Elo scores and display to room while loginserver updates the ladder + const [p1OldElo, p2OldElo] = (await Promise.all([this.getRating(p1!.id), this.getRating(p2!.id)])).map(Math.round); + const p1NewElo = Math.round(calculateElo(p1OldElo, p1score, p2OldElo)); + const p2NewElo = Math.round(calculateElo(p2OldElo, 1 - p1score, p1OldElo)); + + room.update(); + room.send(`||Ladder updating...`); + + const p1Act = (p1score > 0.9 ? `winning` : (p1score < 0.1 ? `losing` : `tying`)); + let p1Reasons = `${p1NewElo - p1OldElo} for ${p1Act}`; + if (!p1Reasons.startsWith('-')) p1Reasons = '+' + p1Reasons; + room.addRaw(Utils.html`${p1name}'s rating: ${p1OldElo} → ${p1NewElo}
(${p1Reasons})`); + + const p2Act = (p1score > 0.9 || p1score < 0 ? `losing` : (p1score < 0.1 ? `winning` : `tying`)); + let p2Reasons = `${p2NewElo - p2OldElo} for ${p2Act}`; + if (!p2Reasons.startsWith('-')) p2Reasons = '+' + p2Reasons; + room.addRaw(Utils.html`${p2name}'s rating: ${p2OldElo} → ${p2NewElo}
(${p2Reasons})`); + + room.rated = Math.min(p1NewElo, p2NewElo); + + if (p1) p1.mmrCache[formatid] = +p1NewElo; + if (p2) p2.mmrCache[formatid] = +p2NewElo; + + room.update(); + + + const [data, error] = await updatePromise; let problem = false; if (error) { - if (error.message === 'stream interrupt') { - room.add(`||Ladder updated, but score could not be retrieved.`); - } else { - room.add(`||Ladder (probably) updated, but score could not be retrieved (${error.message}).`); - } problem = true; } else if (!room.battle) { - Monitor.warn(`room expired before ladder update was received`); problem = true; } else if (!data) { room.add(`|error|Unexpected response ${data} from ladder server.`); @@ -108,37 +132,7 @@ export class LadderStore { return [p1score, null, null]; } - let p1rating; - let p2rating; - try { - p1rating = data!.p1rating; - p2rating = data!.p2rating; - - let oldelo = Math.round(p1rating.oldelo); - let elo = Math.round(p1rating.elo); - let act = (p1score > 0.9 ? `winning` : (p1score < 0.1 ? `losing` : `tying`)); - let reasons = `${elo - oldelo} for ${act}`; - if (!reasons.startsWith('-')) reasons = '+' + reasons; - room.addRaw(Utils.html`${p1name}'s rating: ${oldelo} → ${elo}
(${reasons})`); - let minElo = elo; - - oldelo = Math.round(p2rating.oldelo); - elo = Math.round(p2rating.elo); - act = (p1score > 0.9 || p1score < 0 ? `losing` : (p1score < 0.1 ? `winning` : `tying`)); - reasons = `${elo - oldelo} for ${act}`; - if (!reasons.startsWith('-')) reasons = '+' + reasons; - room.addRaw(Utils.html`${p2name}'s rating: ${oldelo} → ${elo}
(${reasons})`); - if (elo < minElo) minElo = elo; - room.rated = minElo; - - if (p1) p1.mmrCache[formatid] = +p1rating.elo; - if (p2) p2.mmrCache[formatid] = +p2rating.elo; - room.update(); - } catch (e) { - room.addRaw(`There was an error calculating rating changes.`); - room.update(); - } - return [p1score, p1rating, p2rating]; + return [p1score, data!.p1rating, data!.p2rating]; } /** From c52934af7ef74d310ddc0f86ddee58cf69c08a22 Mon Sep 17 00:00:00 2001 From: Dylan Sprague Date: Thu, 28 Jan 2021 23:07:11 -0500 Subject: [PATCH 3/5] Fix linting errors --- server/ladders-local.ts | 2 +- server/ladders-remote.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/server/ladders-local.ts b/server/ladders-local.ts index d0a59cd63533..9580c4ea6c7d 100644 --- a/server/ladders-local.ts +++ b/server/ladders-local.ts @@ -15,7 +15,7 @@ import {FS} from '../lib/fs'; import {Utils} from '../lib/utils'; -import { calculateElo } from './elo'; +import {calculateElo} from './elo'; // ladderCaches = {formatid: ladder OR Promise(ladder)} // Use Ladders(formatid).ladder to guarantee a Promise(ladder). diff --git a/server/ladders-remote.ts b/server/ladders-remote.ts index d1a77db9638d..0b43c7b6a915 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -12,7 +12,7 @@ * @license MIT */ import {Utils} from '../lib/utils'; -import { calculateElo } from './elo'; +import {calculateElo} from './elo'; export class LadderStore { formatid: string; @@ -72,15 +72,13 @@ export class LadderStore { const p1 = Users.getExact(p1name); const p2 = Users.getExact(p2name); - - const updatePromise = LoginServer.request('ladderupdate', { + const ladderUpdatePromise = LoginServer.request('ladderupdate', { p1: p1name, p2: p2name, score: p1score, format: formatid, }); - // calculate new Elo scores and display to room while loginserver updates the ladder const [p1OldElo, p2OldElo] = (await Promise.all([this.getRating(p1!.id), this.getRating(p2!.id)])).map(Math.round); const p1NewElo = Math.round(calculateElo(p1OldElo, p1score, p2OldElo)); @@ -107,7 +105,7 @@ export class LadderStore { room.update(); - const [data, error] = await updatePromise; + const [data, error] = await ladderUpdatePromise; let problem = false; if (error) { From 2c8d3c5da5dd43fc29378c5fa8cf95fc86a2cf03 Mon Sep 17 00:00:00 2001 From: Dylan Sprague Date: Fri, 29 Jan 2021 18:36:27 -0500 Subject: [PATCH 4/5] Server: separate Elo calculation for main server --- server/elo.ts | 27 --------------------------- server/ladders-local.ts | 34 ++++++++++++++++++++++++++++++++-- server/ladders-remote.ts | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 32 deletions(-) delete mode 100644 server/elo.ts diff --git a/server/elo.ts b/server/elo.ts deleted file mode 100644 index b8d66f927f0a..000000000000 --- a/server/elo.ts +++ /dev/null @@ -1,27 +0,0 @@ -export function calculateElo(previousUserElo: number, score: number, foeElo: number): number { - // The K factor determines how much your Elo changes when you win or - // lose games. Larger K means more change. - // In the "original" Elo, K is constant, but it's common for K to - // get smaller as your rating goes up - let K = 50; - - // dynamic K-scaling (optional) - if (previousUserElo < 1200) { - if (score < 0.5) { - K = 10 + (previousUserElo - 1000) * 40 / 200; - } else if (score > 0.5) { - K = 90 - (previousUserElo - 1000) * 40 / 200; - } - } else if (previousUserElo > 1350 && previousUserElo <= 1600) { - K = 40; - } else { - K = 32; - } - - // main Elo formula - const E = 1 / (1 + Math.pow(10, (foeElo - previousUserElo) / 400)); - - const newElo = previousUserElo + K * (score - E); - - return Math.max(newElo, 1000); -} diff --git a/server/ladders-local.ts b/server/ladders-local.ts index 9580c4ea6c7d..1c4745dc0359 100644 --- a/server/ladders-local.ts +++ b/server/ladders-local.ts @@ -15,7 +15,6 @@ import {FS} from '../lib/fs'; import {Utils} from '../lib/utils'; -import {calculateElo} from './elo'; // ladderCaches = {formatid: ladder OR Promise(ladder)} // Use Ladders(formatid).ladder to guarantee a Promise(ladder). @@ -173,7 +172,7 @@ export class LadderStore { updateRow(row: LadderRow, score: number, foeElo: number) { let elo = row[1]; - elo = calculateElo(elo, score, foeElo); + elo = this.calculateElo(elo, score, foeElo); row[1] = elo; if (score > 0.6) { @@ -312,4 +311,35 @@ export class LadderStore { } return Promise.all(ratings); } + + /** + * Calculates Elo based on a match result + */ + private calculateElo(previousUserElo: number, score: number, foeElo: number): number { + // The K factor determines how much your Elo changes when you win or + // lose games. Larger K means more change. + // In the "original" Elo, K is constant, but it's common for K to + // get smaller as your rating goes up + let K = 50; + + // dynamic K-scaling (optional) + if (previousUserElo < 1200) { + if (score < 0.5) { + K = 10 + (previousUserElo - 1000) * 40 / 200; + } else if (score > 0.5) { + K = 90 - (previousUserElo - 1000) * 40 / 200; + } + } else if (previousUserElo > 1350 && previousUserElo <= 1600) { + K = 40; + } else { + K = 32; + } + + // main Elo formula + const E = 1 / (1 + Math.pow(10, (foeElo - previousUserElo) / 400)); + + const newElo = previousUserElo + K * (score - E); + + return Math.max(newElo, 1000); + } } diff --git a/server/ladders-remote.ts b/server/ladders-remote.ts index 0b43c7b6a915..754d3e2b583c 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -12,7 +12,6 @@ * @license MIT */ import {Utils} from '../lib/utils'; -import {calculateElo} from './elo'; export class LadderStore { formatid: string; @@ -81,8 +80,8 @@ export class LadderStore { // calculate new Elo scores and display to room while loginserver updates the ladder const [p1OldElo, p2OldElo] = (await Promise.all([this.getRating(p1!.id), this.getRating(p2!.id)])).map(Math.round); - const p1NewElo = Math.round(calculateElo(p1OldElo, p1score, p2OldElo)); - const p2NewElo = Math.round(calculateElo(p2OldElo, 1 - p1score, p1OldElo)); + const p1NewElo = Math.round(this.calculateElo(p1OldElo, p1score, p2OldElo)); + const p2NewElo = Math.round(this.calculateElo(p2OldElo, 1 - p1score, p1OldElo)); room.update(); room.send(`||Ladder updating...`); @@ -141,4 +140,37 @@ export class LadderStore { static async visualizeAll(username: string) { return [`Please use the official client at play.pokemonshowdown.com`]; } + + /** + * Calculates Elo for quick display, matching the formula on loginserver + */ + // see lib/ntbb-ladder.lib.php in the pokemon-showdown-client repo for the login server implementation + // *intentionally* different from calculation in ladders-local, due to the high activity on the main server + private calculateElo(previousUserElo: number, score: number, foeElo: number): number { + // The K factor determines how much your Elo changes when you win or + // lose games. Larger K means more change. + // In the "original" Elo, K is constant, but it's common for K to + // get smaller as your rating goes up + let K = 50; + + // dynamic K-scaling (optional) + if (previousUserElo < 1100) { + if (score < 0.5) { + K = 20 + (previousUserElo - 1000) * 30 / 100; + } else if (score > 0.5) { + K = 80 - (previousUserElo - 1000) * 30 / 100; + } + } else if (previousUserElo > 1300) { + K = 40; + } else if (previousUserElo > 1600) { + K = 32; + } + + // main Elo formula + const E = 1 / (1 + Math.pow(10, (foeElo - previousUserElo) / 400)); + + const newElo = previousUserElo + K * (score - E); + + return Math.max(newElo, 1000); + } } From ba216fb13d1ccece5a2d1a40b897f72332570209 Mon Sep 17 00:00:00 2001 From: Dylan S Date: Sun, 31 Jan 2021 12:35:04 -0500 Subject: [PATCH 5/5] Remove unnecessary code following PR feedback Co-authored-by: Guangcong Luo --- server/ladders-remote.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/ladders-remote.ts b/server/ladders-remote.ts index 754d3e2b583c..65d0ebe094bc 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -83,9 +83,6 @@ export class LadderStore { const p1NewElo = Math.round(this.calculateElo(p1OldElo, p1score, p2OldElo)); const p2NewElo = Math.round(this.calculateElo(p2OldElo, 1 - p1score, p1OldElo)); - room.update(); - room.send(`||Ladder updating...`); - const p1Act = (p1score > 0.9 ? `winning` : (p1score < 0.1 ? `losing` : `tying`)); let p1Reasons = `${p1NewElo - p1OldElo} for ${p1Act}`; if (!p1Reasons.startsWith('-')) p1Reasons = '+' + p1Reasons;