diff --git a/server/ladders-local.ts b/server/ladders-local.ts index ec24da06198af..1c4745dc03595 100644 --- a/server/ladders-local.ts +++ b/server/ladders-local.ts @@ -172,30 +172,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 = this.calculateElo(elo, score, foeElo); row[1] = elo; if (score > 0.6) { @@ -334,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 72ecdefee9fba..65d0ebe094bc4 100644 --- a/server/ladders-remote.ts +++ b/server/ladders-remote.ts @@ -70,25 +70,43 @@ 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 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(this.calculateElo(p1OldElo, p1score, p2OldElo)); + const p2NewElo = Math.round(this.calculateElo(p2OldElo, 1 - p1score, p1OldElo)); + + 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 ladderUpdatePromise; 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 +126,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]; } /** @@ -149,4 +137,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); + } }