"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var ladders_local_exports = {}; __export(ladders_local_exports, { LadderStore: () => LadderStore }); module.exports = __toCommonJS(ladders_local_exports); var import_lib = require("../lib"); /** * Ladder library * Pokemon Showdown - http://pokemonshowdown.com/ * * This file handles ladders for all servers other than * play.pokemonshowdown.com. * * Specifically, this is the file that handles calculating and keeping * track of players' Elo ratings for all formats. * * Matchmaking is currently still implemented in rooms.ts. * * @license MIT */ const ladderCaches = /* @__PURE__ */ new Map(); const _LadderStore = class { constructor(formatid) { this.formatid = formatid; this.ladder = null; this.ladderPromise = null; this.saving = false; } getLadder() { if (!this.ladderPromise) this.ladderPromise = this.load(); return this.ladderPromise; } /** * Internal function, returns a Promise for a ladder */ async load() { const cachedLadder = ladderCaches.get(this.formatid); if (cachedLadder) { if (cachedLadder.then) { const ladder = await cachedLadder; return this.ladder = ladder; } return this.ladder = cachedLadder; } try { const data = await (0, import_lib.FS)("config/ladders/" + this.formatid + ".tsv").readIfExists(); const ladder = []; for (const dataLine of data.split("\n").slice(1)) { const line = dataLine.trim(); if (!line) continue; const row = line.split(" "); ladder.push([toID(row[1]), Number(row[0]), row[1], Number(row[2]), Number(row[3]), Number(row[4]), row[5]]); } ladderCaches.set(this.formatid, this.ladder = ladder); return this.ladder; } catch { } ladderCaches.set(this.formatid, this.ladder = []); return this.ladder; } /** * Saves the ladder in config/ladders/[formatid].tsv * * Called automatically by updateRating, so you don't need to manually * call this. */ async save() { if (this.saving) return; this.saving = true; const ladder = await this.getLadder(); if (!ladder.length) { this.saving = false; return; } const stream = (0, import_lib.FS)(`config/ladders/${this.formatid}.tsv`).createWriteStream(); void stream.write("Elo Username W L T Last update\r\n"); for (const row of ladder) { void stream.write(row.slice(1).join(" ") + "\r\n"); } void stream.writeEnd(); this.saving = false; } /** * Gets the index of a user in the ladder array. * * If createIfNeeded is true, the user will be created and added to * the ladder array if it doesn't already exist. */ indexOfUser(username, createIfNeeded = false) { if (!this.ladder) throw new Error(`Must be called with ladder loaded`); const userid = toID(username); for (const [i, user] of this.ladder.entries()) { if (user[0] === userid) return i; } if (createIfNeeded) { const index = this.ladder.length; this.ladder.push([userid, 1e3, username, 0, 0, 0, ""]); return index; } return -1; } /** * Returns [formatid, html], where html is an the HTML source of a * ladder toplist, to be displayed directly in the ladder tab of the * client. */ async getTop(prefix) { const formatid = this.formatid; const name = Dex.formats.get(formatid).name; const ladder = await this.getLadder(); let buf = `

${name} Top 100

`; buf += ``; buf += ``; for (const [i, row] of ladder.entries()) { if (prefix && !row[0].startsWith(prefix)) continue; buf += ``; } return [formatid, buf]; } /** * Returns a Promise for the Elo rating of a user */ async getRating(userid) { const formatid = this.formatid; const user = Users.getExact(userid); if (user?.mmrCache[formatid]) { return user.mmrCache[formatid]; } const ladder = await this.getLadder(); const index = this.indexOfUser(userid); let rating = 1e3; if (index >= 0) { rating = ladder[index][1]; } if (user && user.id === userid) { user.mmrCache[formatid] = rating; } return rating; } /** * Internal method. Update the Elo rating of a user. */ updateRow(row, score, foeElo) { let elo = row[1]; elo = this.calculateElo(elo, score, foeElo); row[1] = elo; if (score > 0.6) { row[3]++; } else if (score < 0.4) { row[4]++; } else { row[5]++; } row[6] = `${new Date()}`; } /** * Update the Elo rating for two players after a battle, and display * the results in the passed room. */ async updateRating(p1name, p2name, p1score, room) { if (Ladders.disabled) { room.addRaw(`Ratings not updated. The ladders are currently disabled.`).update(); return [p1score, null, null]; } const formatid = this.formatid; let p2score = 1 - p1score; if (p1score < 0) { p1score = 0; p2score = 0; } const ladder = await this.getLadder(); let p1newElo; let p2newElo; try { const p1index = this.indexOfUser(p1name, true); const p1elo = ladder[p1index][1]; let p2index = this.indexOfUser(p2name, true); const p2elo = ladder[p2index][1]; this.updateRow(ladder[p1index], p1score, p2elo); this.updateRow(ladder[p2index], p2score, p1elo); p1newElo = ladder[p1index][1]; p2newElo = ladder[p2index][1]; let newIndex = p1index; while (newIndex > 0 && ladder[newIndex - 1][1] <= p1newElo) newIndex--; while (newIndex === p1index || ladder[newIndex] && ladder[newIndex][1] > p1newElo) newIndex++; if (newIndex !== p1index && newIndex !== p1index + 1) { const row = ladder.splice(p1index, 1)[0]; if (newIndex > p1index) newIndex--; if (p2index > p1index) p2index--; ladder.splice(newIndex, 0, row); if (p2index >= newIndex) p2index++; } newIndex = p2index; while (newIndex > 0 && ladder[newIndex - 1][1] <= p2newElo) newIndex--; while (newIndex === p2index || ladder[newIndex] && ladder[newIndex][1] > p2newElo) newIndex++; if (newIndex !== p2index && newIndex !== p2index + 1) { const row = ladder.splice(p2index, 1)[0]; if (newIndex > p2index) newIndex--; ladder.splice(newIndex, 0, row); } const p1 = Users.getExact(p1name); if (p1) p1.mmrCache[formatid] = +p1newElo; const p2 = Users.getExact(p2name); if (p2) p2.mmrCache[formatid] = +p2newElo; void this.save(); if (!room.battle) { Monitor.warn(`room expired before ladder update was received`); return [p1score, null, null]; } let reasons = `${Math.round(p1newElo) - Math.round(p1elo)} for ${p1score > 0.9 ? "winning" : p1score < 0.1 ? "losing" : "tying"}`; if (!reasons.startsWith("-")) reasons = "+" + reasons; room.addRaw( import_lib.Utils.html`${p1name}'s rating: ${Math.round(p1elo)} → ${Math.round(p1newElo)}
(${reasons})` ); reasons = `${Math.round(p2newElo) - Math.round(p2elo)} for ${p2score > 0.9 ? "winning" : p2score < 0.1 ? "losing" : "tying"}`; if (!reasons.startsWith("-")) reasons = "+" + reasons; room.addRaw( import_lib.Utils.html`${p2name}'s rating: ${Math.round(p2elo)} → ${Math.round(p2newElo)}
(${reasons})` ); room.update(); } catch (e) { if (!room.battle) return [p1score, null, null]; room.addRaw(`There was an error calculating rating changes:`); room.add(e.stack); room.update(); } return [p1score, p1newElo, p2newElo]; } /** * Returns a promise for a with all ratings for the current format. */ async visualize(username) { const ladder = await this.getLadder(); const index = this.indexOfUser(username, false); if (index < 0) return ""; const ratings = ladder[index]; const output = ``; return `${output}`; } /** * Calculates Elo based on a match result */ calculateElo(oldElo, score, foeElo) { let K = 50; if (oldElo < 1200) { if (score < 0.5) { K = 10 + (oldElo - 1e3) * 40 / 200; } else if (score > 0.5) { K = 90 - (oldElo - 1e3) * 40 / 200; } } else if (oldElo > 1350 && oldElo <= 1600) { K = 40; } else { K = 32; } const E = 1 / (1 + 10 ** ((foeElo - oldElo) / 400)); const newElo = oldElo + K * (score - E); return Math.max(newElo, 1e3); } /** * Returns a Promise for an array of strings of s for ladder ratings of the user */ static visualizeAll(username) { const ratings = []; for (const format of Dex.formats.all()) { if (format.searchShow) { ratings.push(new _LadderStore(format.id).visualize(username)); } } return Promise.all(ratings); } }; let LadderStore = _LadderStore; LadderStore.formatsListPrefix = "|,LL"; LadderStore.ladderCaches = ladderCaches; //# sourceMappingURL=ladders-local.js.map
` + ["", "Username", 'Elo', "W", "L", "T"].join(``) + `
` + [ i + 1, row[2], `${Math.round(row[1])}`, row[3], row[4], row[5] ].join(``) + `
${this.formatid}${Math.round(ratings[1])}${ratings[3]}${ratings[4]}${ratings[3] + ratings[4]}