"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 += `` + ["", "Username", 'Elo', "W", "L", "T"].join(` | `) + ` |
`;
for (const [i, row] of ladder.entries()) {
if (prefix && !row[0].startsWith(prefix))
continue;
buf += `` + [
i + 1,
row[2],
`${Math.round(row[1])}`,
row[3],
row[4],
row[5]
].join(` | `) + ` |
`;
}
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 = `
${this.formatid} | ${Math.round(ratings[1])} | `;
return `${output}${ratings[3]} | ${ratings[4]} | ${ratings[3] + ratings[4]} |
`;
}
/**
* 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